diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..efbc938 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,60 @@ +# Git +.git +.gitignore +.github + +# IDE +.idea +.vscode +*.swp +*.swo +*~ + +# Build artifacts +bin/ +dist/ +*.exe +orkes +orkes-test +conductor-cli + +# Documentation (keep only Docker-related docs) +*.md +!docker/README.md +!docker-examples/README.md +!WORKER_STDIO.md + +# Test files +test/ +*_test.go +*.bats + +# macOS +.DS_Store + +# Environment +.env +.env.* + +# Cache +.cache/ +node_modules/ +__pycache__/ +*.pyc + +# Config +config.yaml +config-*.yaml +.conductor-cli/ + +# Claude +.claude/ + +# Examples (keep for Docker context) +# stdio-example/ +# stdio/ + +# Temporary files +*.log +*.tmp +tmp/ diff --git a/docker-examples/README.md b/docker-examples/README.md new file mode 100644 index 0000000..eb05318 --- /dev/null +++ b/docker-examples/README.md @@ -0,0 +1,421 @@ +# Orkes CLI Docker Examples + +Complete examples for deploying Orkes Conductor stdio workers using Docker. + +## Directory Structure + +``` +docker-examples/ +├── workers/ # Example worker scripts +│ ├── python/ # Python examples +│ ├── node/ # Node.js examples +│ ├── java/ # Java examples +│ ├── go/ # Go examples +│ └── dotnet/ # .NET examples +├── docker-compose/ # Docker Compose configurations +│ └── docker-compose.yml # Basic worker example +└── README.md # This file +``` + +## Quick Examples + +### Docker Run - Python Worker + +```bash +cd workers/python +docker run --rm \ + -e CONDUCTOR_SERVER_URL=https://developer.orkescloud.com/api \ + -e CONDUCTOR_AUTH_TOKEN=$AUTH_TOKEN \ + -v $(pwd)/simple_worker.py:/app/worker.py:ro \ + orkes/cli-runner:python \ + worker stdio --type greeting_task python /app/worker.py +``` + +### Docker Run - Node.js Worker + +```bash +cd workers/node +docker run --rm \ + -e CONDUCTOR_SERVER_URL=https://developer.orkescloud.com/api \ + -e CONDUCTOR_AUTH_TOKEN=$AUTH_TOKEN \ + -v $(pwd)/simple_worker.js:/app/worker.js:ro \ + orkes/cli-runner:node \ + worker stdio --type data_transform node /app/worker.js +``` + +### Docker Run - Go Worker + +```bash +cd workers/go +docker run --rm \ + -e CONDUCTOR_SERVER_URL=https://developer.orkescloud.com/api \ + -e CONDUCTOR_AUTH_TOKEN=$AUTH_TOKEN \ + -v $(pwd):/app:ro \ + orkes/cli-runner:go \ + worker stdio --type compute_task go run /app/simple_worker.go +``` + +## Docker Compose Example + +```bash +cd docker-compose +export CONDUCTOR_AUTH_TOKEN=your-token +docker-compose up +``` + +This runs a simple Python worker. + +## Worker Development + +### Python Worker with Dependencies + +Create a custom image with your dependencies: + +```dockerfile +FROM orkes/cli-runner:python +COPY requirements.txt /tmp/ +RUN pip install --no-cache-dir -r /tmp/requirements.txt +WORKDIR /app +COPY worker.py . +CMD ["worker", "stdio", "--type", "my_task", "python", "worker.py"] +``` + +Build and run: + +```bash +docker build -t my-python-worker . +docker run --rm \ + -e CONDUCTOR_SERVER_URL=$SERVER_URL \ + -e CONDUCTOR_AUTH_TOKEN=$AUTH_TOKEN \ + my-python-worker +``` + +### Node.js Worker with Packages + +```dockerfile +FROM orkes/cli-runner:node +WORKDIR /app +COPY package*.json ./ +RUN npm ci --only=production +COPY worker.js . +CMD ["worker", "stdio", "--type", "my_task", "node", "worker.js"] +``` + +### Java Worker (Compiled) + +```dockerfile +FROM orkes/cli-runner:java AS builder +WORKDIR /build +COPY *.java . +RUN javac *.java + +FROM orkes/cli-runner:java +WORKDIR /app +COPY --from=builder /build/*.class . +CMD ["worker", "stdio", "--type", "my_task", "java", "SimpleWorker"] +``` + +### Go Worker (Compiled) + +```dockerfile +FROM orkes/cli-runner:go AS builder +WORKDIR /build +COPY go.mod go.sum ./ +RUN go mod download +COPY *.go ./ +RUN CGO_ENABLED=0 go build -o worker . + +FROM orkes/cli-runner:base +COPY --from=builder /build/worker /usr/local/bin/worker +CMD ["worker", "stdio", "--type", "my_task", "/usr/local/bin/worker"] +``` + +## Common Patterns + +### Pattern 1: High-Throughput Processing + +Use batch polling to process multiple tasks per poll: + +```yaml +services: + worker: + image: orkes/cli-runner:python + command: > + worker stdio + --type high_volume_task + --count 20 + python /app/worker.py + deploy: + replicas: 5 +``` + +This configuration: +- Each worker polls 20 tasks at a time +- 5 worker replicas run in parallel +- Total capacity: 100 tasks per polling cycle + +### Pattern 2: Multiple Task Types + +Run different workers for different task types: + +```yaml +services: + greeting-worker: + image: orkes/cli-runner:python + command: worker stdio --type greeting_task python /app/greeting.py + + email-worker: + image: orkes/cli-runner:python + command: worker stdio --type send_email python /app/email.py + + transform-worker: + image: orkes/cli-runner:node + command: worker stdio --type data_transform node /app/transform.js +``` + +### Pattern 3: Resource Limits + +Set resource limits for workers: + +```yaml +services: + worker: + image: orkes/cli-runner:go + command: worker stdio --type compute_intensive go run /app/worker.go + deploy: + resources: + limits: + cpus: '2.0' + memory: 2G + reservations: + cpus: '1.0' + memory: 1G +``` + +### Pattern 4: Health Checks + +Add health checks to ensure workers are running: + +```yaml +services: + worker: + image: orkes/cli-runner:python + command: worker stdio --type my_task python /app/worker.py + healthcheck: + test: ["CMD", "orkes", "--version"] + interval: 30s + timeout: 10s + retries: 3 +``` + +## Development Tips + +### 1. Test Workers Locally First + +Before containerizing, test your worker locally: + +```bash +echo '{"inputData":{"name":"Test"}}' | python worker.py +``` + +Expected output: +```json +{"status":"COMPLETED","output":{"message":"Hello, Test!"},"logs":["..."]} +``` + +### 2. Use Verbose Mode for Debugging + +Enable verbose output to see task and result JSON: + +```bash +docker run ... orkes/cli-runner:python \ + worker stdio --type my_task --verbose python /app/worker.py +``` + +### 3. Mount Code as Read-Only + +Always mount worker code as read-only: + +```yaml +volumes: + - ./worker.py:/app/worker.py:ro # :ro = read-only +``` + +### 4. Use Environment-Specific Configs + +Use different config files for different environments: + +```bash +# Development +docker run -v ~/.conductor-cli/config-dev.yaml:/home/orkes/.conductor-cli/config.yaml:ro ... + +# Production +docker run -v ~/.conductor-cli/config-prod.yaml:/home/orkes/.conductor-cli/config.yaml:ro ... +``` + +### 5. Worker ID for Tracking + +Set unique worker IDs for tracking in logs: + +```bash +worker stdio --worker-id worker-${HOSTNAME}-${RANDOM} ... +``` + +## Troubleshooting + +### Worker not processing tasks + +**Check:** +```bash +# Test connectivity +docker run --rm -e CONDUCTOR_SERVER_URL=$URL orkes/cli-runner:base curl -v $URL + +# Verify authentication +docker run --rm \ + -e CONDUCTOR_SERVER_URL=$URL \ + -e CONDUCTOR_AUTH_TOKEN=$TOKEN \ + orkes/cli-runner:base \ + workflow list + +# Check task type exists +docker run --rm \ + -e CONDUCTOR_SERVER_URL=$URL \ + -e CONDUCTOR_AUTH_TOKEN=$TOKEN \ + orkes/cli-runner:base \ + task get my_task_type +``` + +### Worker failing with permission errors + +**Fix:** +```bash +# Ensure files are readable by UID 1000 +chmod 644 worker.py + +# Or change ownership +sudo chown 1000:1000 worker.py +``` + +### Worker exits immediately + +**Debug:** +```bash +# Check logs +docker logs + +# Run interactively +docker run -it orkes/cli-runner:python sh + +# Test command manually +echo '{"inputData":{}}' | docker run -i orkes/cli-runner:python python - +``` + +### Dependencies missing + +**Solution:** +```dockerfile +# Create custom image with dependencies +FROM orkes/cli-runner:python +RUN pip install requests pandas numpy +# ... rest of Dockerfile +``` + +### Task timeout errors + +**Fix:** +```bash +# Increase execution timeout (in seconds) +worker stdio --type my_task --exec-timeout 300 python /app/worker.py +``` + +## Performance Tuning + +### Optimize Batch Size + +Experiment with `--count` to find optimal batch size: + +```bash +# Small batches (1-5): Low latency, frequent polling +worker stdio --type my_task --count 1 ... + +# Medium batches (5-20): Balanced +worker stdio --type my_task --count 10 ... + +# Large batches (20-100): High throughput, less frequent polling +worker stdio --type my_task --count 50 ... +``` + +### Scale Horizontally + +Run multiple worker replicas: + +```bash +# Docker Compose +docker-compose up --scale worker=10 +``` + +### Resource Allocation + +Allocate resources based on workload: + +```yaml +# CPU-intensive tasks +resources: + limits: + cpus: '4.0' + memory: 2G + +# Memory-intensive tasks +resources: + limits: + cpus: '1.0' + memory: 8G + +# I/O-intensive tasks +resources: + limits: + cpus: '0.5' + memory: 512M +``` + +## Best Practices + +1. **Security** + - Never commit secrets to version control + - Use environment variables or mounted configs + - Mount worker code as read-only (`:ro`) + - Run with non-root user (default in our images) + +2. **Reliability** + - Use `restart: unless-stopped` in Docker Compose + - Set resource limits to prevent OOM kills + - Implement proper error handling in workers + - Use health checks + +3. **Observability** + - Use unique worker IDs for tracking + - Include detailed logs in worker output + - Monitor resource usage + - Set up alerting for failed tasks + +4. **Development Workflow** + - Test workers locally first + - Use verbose mode for debugging + - Start with basic examples, then customize + - Version your worker code + +## Next Steps + +- Review [worker documentation](../WORKER_STDIO.md) for detailed worker contract +- Check [Docker image documentation](../docker/README.md) for build and deployment +- Join [Conductor Slack](https://orkes.io/slack) for community support + +## Support + +- **Documentation**: https://orkes.io/content +- **GitHub Issues**: https://github.com/conductor-oss/conductor-cli/issues +- **Community**: Conductor Slack + +## License + +Apache 2.0 - See [LICENSE](../LICENSE) file diff --git a/docker-examples/docker-compose/docker-compose.yml b/docker-examples/docker-compose/docker-compose.yml new file mode 100644 index 0000000..dcc451f --- /dev/null +++ b/docker-examples/docker-compose/docker-compose.yml @@ -0,0 +1,19 @@ +version: '3.8' + +services: + worker: + image: orkes/cli-runner:python + command: > + worker stdio + --type greeting_task + python /app/worker.py + environment: + CONDUCTOR_SERVER_URL: ${CONDUCTOR_SERVER_URL:-https://developer.orkescloud.com/api} + CONDUCTOR_AUTH_TOKEN: ${CONDUCTOR_AUTH_TOKEN} + volumes: + - ../workers/python/simple_worker.py:/app/worker.py:ro + restart: unless-stopped + +# To run: +# 1. Set your auth token: export CONDUCTOR_AUTH_TOKEN=your-token +# 2. Run: docker-compose up diff --git a/docker-examples/workers/dotnet/SimpleWorker.cs b/docker-examples/workers/dotnet/SimpleWorker.cs new file mode 100644 index 0000000..2c8c584 --- /dev/null +++ b/docker-examples/workers/dotnet/SimpleWorker.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; + +/// +/// Simple stdio worker example for Orkes Conductor CLI. +/// +/// This worker reads a task from stdin, processes it, and returns a result to stdout. +/// +class SimpleWorker +{ + class Task + { + public string? TaskId { get; set; } + public string? WorkflowInstanceId { get; set; } + public string? TaskType { get; set; } + public Dictionary? InputData { get; set; } + } + + class Result + { + public string Status { get; set; } = "COMPLETED"; + public Dictionary? Output { get; set; } + public List? Logs { get; set; } + public string? Reason { get; set; } + } + + static void Main() + { + try + { + // Read task from stdin + string taskJson = Console.In.ReadToEnd(); + + // Parse task JSON + var task = JsonSerializer.Deserialize(taskJson); + + // Get input parameters + string name = "World"; + if (task?.InputData != null && task.InputData.ContainsKey("name")) + { + var nameValue = task.InputData["name"]; + if (nameValue != null) + { + name = nameValue.ToString() ?? "World"; + } + } + + // Get task metadata + string taskId = task?.TaskId ?? "unknown"; + string workflowId = task?.WorkflowInstanceId ?? "unknown"; + string taskType = task?.TaskType ?? "unknown"; + + // Process the task + string message = $"Hello, {name}!"; + + // Create result + var result = new Result + { + Status = "COMPLETED", + Output = new Dictionary + { + { "message", message }, + { "taskId", taskId }, + { "workflowId", workflowId } + }, + Logs = new List + { + $"Processing task {taskId} of type {taskType}", + $"Workflow: {workflowId}", + $"Generated greeting for {name}" + } + }; + + // Output result to stdout + string resultJson = JsonSerializer.Serialize(result); + Console.WriteLine(resultJson); + } + catch (Exception ex) + { + // Return failure result on error + var errorResult = new Result + { + Status = "FAILED", + Reason = ex.Message, + Logs = new List { $"Error processing task: {ex.Message}" } + }; + + string errorJson = JsonSerializer.Serialize(errorResult); + Console.WriteLine(errorJson); + } + } +} diff --git a/docker-examples/workers/go/go.mod b/docker-examples/workers/go/go.mod new file mode 100644 index 0000000..4143705 --- /dev/null +++ b/docker-examples/workers/go/go.mod @@ -0,0 +1,5 @@ +module conductor-stdio-worker + +go 1.23 + +// No external dependencies required for simple worker diff --git a/docker-examples/workers/go/simple_worker.go b/docker-examples/workers/go/simple_worker.go new file mode 100644 index 0000000..ddbc7cd --- /dev/null +++ b/docker-examples/workers/go/simple_worker.go @@ -0,0 +1,96 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "os" +) + +// Task represents the input task structure +type Task struct { + TaskId string `json:"taskId"` + WorkflowInstanceId string `json:"workflowInstanceId"` + TaskType string `json:"taskType"` + InputData map[string]interface{} `json:"inputData"` +} + +// Result represents the output result structure +type Result struct { + Status string `json:"status"` + Output map[string]interface{} `json:"output,omitempty"` + Logs []string `json:"logs,omitempty"` + Reason string `json:"reason,omitempty"` +} + +// Simple stdio worker example for Orkes Conductor CLI. +// +// This worker reads a task from stdin, processes it, and returns a result to stdout. +func main() { + // Read task from stdin + input, err := io.ReadAll(os.Stdin) + if err != nil { + outputError(fmt.Sprintf("Failed to read stdin: %v", err)) + return + } + + // Parse task JSON + var task Task + if err := json.Unmarshal(input, &task); err != nil { + outputError(fmt.Sprintf("Failed to parse task JSON: %v", err)) + return + } + + // Get input parameters + name := "World" + if n, ok := task.InputData["name"].(string); ok && n != "" { + name = n + } + + // Get task metadata + taskID := task.TaskId + if taskID == "" { + taskID = "unknown" + } + workflowID := task.WorkflowInstanceId + if workflowID == "" { + workflowID = "unknown" + } + taskType := task.TaskType + if taskType == "" { + taskType = "unknown" + } + + // Process the task + message := fmt.Sprintf("Hello, %s!", name) + + // Create result + result := Result{ + Status: "COMPLETED", + Output: map[string]interface{}{ + "message": message, + "taskId": taskID, + "workflowId": workflowID, + }, + Logs: []string{ + fmt.Sprintf("Processing task %s of type %s", taskID, taskType), + fmt.Sprintf("Workflow: %s", workflowID), + fmt.Sprintf("Generated greeting for %s", name), + }, + } + + // Output result to stdout + if err := json.NewEncoder(os.Stdout).Encode(result); err != nil { + outputError(fmt.Sprintf("Failed to encode result: %v", err)) + } +} + +// outputError outputs an error result +func outputError(message string) { + result := Result{ + Status: "FAILED", + Reason: message, + Logs: []string{fmt.Sprintf("Error processing task: %s", message)}, + } + json.NewEncoder(os.Stdout).Encode(result) +} diff --git a/docker-examples/workers/java/SimpleWorker.java b/docker-examples/workers/java/SimpleWorker.java new file mode 100644 index 0000000..df9fdc1 --- /dev/null +++ b/docker-examples/workers/java/SimpleWorker.java @@ -0,0 +1,86 @@ +import java.io.*; +import java.util.*; +import java.util.stream.*; + +/** + * Simple stdio worker example for Orkes Conductor CLI. + * + * This worker reads a task from stdin, processes it, and returns a result to stdout. + * Demonstrates basic JSON handling without external dependencies. + */ +public class SimpleWorker { + + public static void main(String[] args) { + try { + // Read task from stdin + BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); + String taskJson = reader.lines().collect(Collectors.joining("\n")); + + // Simple JSON parsing (in production, use a library like Gson or Jackson) + String name = extractValue(taskJson, "name", "World"); + + // Get task metadata from JSON + String taskId = extractValue(taskJson, "taskId", "unknown"); + String workflowId = extractValue(taskJson, "workflowInstanceId", "unknown"); + String taskType = extractValue(taskJson, "taskType", "unknown"); + + // Process the task + String message = "Hello, " + name + "!"; + + // Build result JSON + StringBuilder result = new StringBuilder(); + result.append("{"); + result.append("\"status\":\"COMPLETED\","); + result.append("\"output\":{"); + result.append("\"message\":\"").append(escapeJson(message)).append("\","); + result.append("\"taskId\":\"").append(taskId).append("\","); + result.append("\"workflowId\":\"").append(workflowId).append("\""); + result.append("},"); + result.append("\"logs\":["); + result.append("\"Processing task " + taskId + " of type " + taskType + "\","); + result.append("\"Workflow: " + workflowId + "\","); + result.append("\"Generated greeting for " + name + "\""); + result.append("]"); + result.append("}"); + + // Output result to stdout + System.out.println(result.toString()); + + } catch (Exception e) { + // Return failure result on error + String errorResult = String.format( + "{\"status\":\"FAILED\",\"reason\":\"%s\",\"logs\":[\"Error processing task: %s\"]}", + escapeJson(e.getMessage()), + escapeJson(e.getMessage()) + ); + System.out.println(errorResult); + } + } + + /** + * Simple JSON value extraction (for demonstration purposes). + * In production, use a proper JSON library like Gson or Jackson. + */ + private static String extractValue(String json, String key, String defaultValue) { + String pattern = "\"" + key + "\"\\s*:\\s*\"([^\"]+)\""; + java.util.regex.Pattern p = java.util.regex.Pattern.compile(pattern); + java.util.regex.Matcher m = p.matcher(json); + if (m.find()) { + return m.group(1); + } + return defaultValue; + } + + /** + * Escape JSON string values. + */ + private static String escapeJson(String value) { + if (value == null) return ""; + return value + .replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + } +} diff --git a/docker-examples/workers/node/package.json b/docker-examples/workers/node/package.json new file mode 100644 index 0000000..b168256 --- /dev/null +++ b/docker-examples/workers/node/package.json @@ -0,0 +1,17 @@ +{ + "name": "conductor-stdio-workers", + "version": "1.0.0", + "description": "Example stdio workers for Orkes Conductor CLI", + "main": "simple_worker.js", + "scripts": { + "simple": "node simple_worker.js" + }, + "keywords": [ + "conductor", + "orkes", + "worker", + "stdio" + ], + "author": "Orkes", + "license": "Apache-2.0" +} diff --git a/docker-examples/workers/node/simple_worker.js b/docker-examples/workers/node/simple_worker.js new file mode 100644 index 0000000..feca7f4 --- /dev/null +++ b/docker-examples/workers/node/simple_worker.js @@ -0,0 +1,57 @@ +#!/usr/bin/env node +/** + * Simple stdio worker example for Orkes Conductor CLI. + * + * This worker reads a task from stdin, processes it, and returns a result to stdout. + */ + +// Read task from stdin +let input = ''; +process.stdin.setEncoding('utf8'); + +process.stdin.on('data', chunk => { + input += chunk; +}); + +process.stdin.on('end', () => { + try { + // Parse task JSON + const task = JSON.parse(input); + const inputData = task.inputData || {}; + const name = inputData.name || 'World'; + + // Get task metadata + const taskId = task.taskId || 'unknown'; + const workflowId = task.workflowInstanceId || 'unknown'; + const taskType = task.taskType || 'unknown'; + + // Process the task + const message = `Hello, ${name}!`; + + // Return result to stdout + const result = { + status: 'COMPLETED', + output: { + message: message, + taskId: taskId, + workflowId: workflowId + }, + logs: [ + `Processing task ${taskId} of type ${taskType}`, + `Workflow: ${workflowId}`, + `Generated greeting for ${name}` + ] + }; + + console.log(JSON.stringify(result)); + + } catch (error) { + // Return failure result on error + const result = { + status: 'FAILED', + reason: error.message, + logs: [`Error processing task: ${error.message}`] + }; + console.log(JSON.stringify(result)); + } +}); diff --git a/docker-examples/workers/python/simple_worker.py b/docker-examples/workers/python/simple_worker.py new file mode 100644 index 0000000..1595398 --- /dev/null +++ b/docker-examples/workers/python/simple_worker.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +""" +Simple stdio worker example for Orkes Conductor CLI. + +This worker reads a task from stdin, processes it, and returns a result to stdout. +""" +import sys +import json + +def main(): + # Read task from stdin + task = json.load(sys.stdin) + + # Get input parameters + input_data = task.get('inputData', {}) + name = input_data.get('name', 'World') + + # Get task metadata + task_id = task.get('taskId', 'unknown') + workflow_id = task.get('workflowInstanceId', 'unknown') + task_type = task.get('taskType', 'unknown') + + # Process the task + message = f"Hello, {name}!" + + # Return result to stdout + result = { + "status": "COMPLETED", + "output": { + "message": message, + "taskId": task_id, + "workflowId": workflow_id + }, + "logs": [ + f"Processing task {task_id} of type {task_type}", + f"Workflow: {workflow_id}", + f"Generated greeting for {name}" + ] + } + + print(json.dumps(result)) + +if __name__ == '__main__': + try: + main() + except Exception as e: + # Return failure result on error + result = { + "status": "FAILED", + "reason": str(e), + "logs": [f"Error processing task: {e}"] + } + print(json.dumps(result)) diff --git a/docker/Dockerfile-base b/docker/Dockerfile-base new file mode 100644 index 0000000..2793bfe --- /dev/null +++ b/docker/Dockerfile-base @@ -0,0 +1,59 @@ +# Stage 1: Build the orkes CLI +FROM golang:1.23-alpine AS builder + +# Install build dependencies +RUN apk add --no-cache git ca-certificates + +WORKDIR /build + +# Copy go mod files (better caching) +COPY go.mod go.sum ./ +RUN go mod download + +# Copy source code +COPY . . + +# Build the binary +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o orkes . + +# Stage 2: Runtime +FROM alpine:3.19 + +# Install basic utilities +RUN apk add --no-cache \ + ca-certificates \ + curl \ + wget \ + git \ + bash \ + jq \ + && rm -rf /var/cache/apk/* + +# Create non-root user +RUN addgroup -g 1000 orkes && \ + adduser -D -u 1000 -G orkes orkes + +# Copy binary from builder +COPY --from=builder /build/orkes /usr/local/bin/orkes +RUN chmod +x /usr/local/bin/orkes + +# Create config directory +RUN mkdir -p /home/orkes/.conductor-cli /app && \ + chown -R orkes:orkes /home/orkes /app + +# Switch to non-root user +USER orkes +WORKDIR /app + +# Verify installation +RUN orkes --version + +# Labels +LABEL org.opencontainers.image.title="Orkes CLI Runner - Base" \ + org.opencontainers.image.description="Base image with Orkes CLI and basic utilities" \ + org.opencontainers.image.source="https://github.com/conductor-oss/conductor-cli" \ + org.opencontainers.image.vendor="Orkes" \ + maintainer="Orkes " + +ENTRYPOINT ["orkes"] +CMD ["--help"] diff --git a/docker/Dockerfile-dotnet b/docker/Dockerfile-dotnet new file mode 100644 index 0000000..809d639 --- /dev/null +++ b/docker/Dockerfile-dotnet @@ -0,0 +1,60 @@ +# Stage 1: Build the orkes CLI +FROM golang:1.23-alpine AS builder + +# Install build dependencies +RUN apk add --no-cache git ca-certificates + +WORKDIR /build + +# Copy go mod files (better caching) +COPY go.mod go.sum ./ +RUN go mod download + +# Copy source code +COPY . . + +# Build the binary +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o orkes . + +# Stage 2: Runtime with .NET +FROM mcr.microsoft.com/dotnet/runtime:8.0-alpine + +# Install utilities +RUN apk add --no-cache \ + ca-certificates \ + curl \ + wget \ + git \ + bash \ + jq \ + && rm -rf /var/cache/apk/* + +# Create non-root user +RUN addgroup -g 1000 orkes && \ + adduser -D -u 1000 -G orkes orkes + +# Copy orkes CLI binary +COPY --from=builder /build/orkes /usr/local/bin/orkes +RUN chmod +x /usr/local/bin/orkes + +# Create directories +RUN mkdir -p /home/orkes/.conductor-cli /app && \ + chown -R orkes:orkes /home/orkes /app + +# Switch to non-root user +USER orkes +WORKDIR /app + +# Verify installations +RUN orkes --version && dotnet --version + +# Labels +LABEL org.opencontainers.image.title="Orkes CLI Runner - .NET" \ + org.opencontainers.image.description="Orkes CLI with .NET 8 runtime" \ + org.opencontainers.image.source="https://github.com/conductor-oss/conductor-cli" \ + org.opencontainers.image.vendor="Orkes" \ + dotnet.version="8.0" \ + maintainer="Orkes " + +ENTRYPOINT ["orkes"] +CMD ["--help"] diff --git a/docker/Dockerfile-go b/docker/Dockerfile-go new file mode 100644 index 0000000..3e0cea9 --- /dev/null +++ b/docker/Dockerfile-go @@ -0,0 +1,60 @@ +# Stage 1: Build the orkes CLI +FROM golang:1.23-alpine AS builder + +# Install build dependencies +RUN apk add --no-cache git ca-certificates + +WORKDIR /build + +# Copy go mod files (better caching) +COPY go.mod go.sum ./ +RUN go mod download + +# Copy source code +COPY . . + +# Build the binary +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o orkes . + +# Stage 2: Runtime with Go +FROM golang:1.23-alpine3.19 + +# Install utilities +RUN apk add --no-cache \ + ca-certificates \ + curl \ + wget \ + git \ + bash \ + jq \ + && rm -rf /var/cache/apk/* + +# Create non-root user +RUN addgroup -g 1000 orkes && \ + adduser -D -u 1000 -G orkes orkes + +# Copy orkes CLI binary +COPY --from=builder /build/orkes /usr/local/bin/orkes +RUN chmod +x /usr/local/bin/orkes + +# Create directories +RUN mkdir -p /home/orkes/.conductor-cli /app && \ + chown -R orkes:orkes /home/orkes /app + +# Switch to non-root user +USER orkes +WORKDIR /app + +# Verify installations +RUN orkes --version && go version + +# Labels +LABEL org.opencontainers.image.title="Orkes CLI Runner - Go" \ + org.opencontainers.image.description="Orkes CLI with Go 1.23" \ + org.opencontainers.image.source="https://github.com/conductor-oss/conductor-cli" \ + org.opencontainers.image.vendor="Orkes" \ + go.version="1.23" \ + maintainer="Orkes " + +ENTRYPOINT ["orkes"] +CMD ["--help"] diff --git a/docker/Dockerfile-java b/docker/Dockerfile-java new file mode 100644 index 0000000..b890fa7 --- /dev/null +++ b/docker/Dockerfile-java @@ -0,0 +1,60 @@ +# Stage 1: Build the orkes CLI +FROM golang:1.23-alpine AS builder + +# Install build dependencies +RUN apk add --no-cache git ca-certificates + +WORKDIR /build + +# Copy go mod files (better caching) +COPY go.mod go.sum ./ +RUN go mod download + +# Copy source code +COPY . . + +# Build the binary +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o orkes . + +# Stage 2: Runtime with Java +FROM eclipse-temurin:21-jre-alpine + +# Install utilities +RUN apk add --no-cache \ + ca-certificates \ + curl \ + wget \ + git \ + bash \ + jq \ + && rm -rf /var/cache/apk/* + +# Create non-root user +RUN addgroup -g 1000 orkes && \ + adduser -D -u 1000 -G orkes orkes + +# Copy orkes CLI binary +COPY --from=builder /build/orkes /usr/local/bin/orkes +RUN chmod +x /usr/local/bin/orkes + +# Create directories +RUN mkdir -p /home/orkes/.conductor-cli /app && \ + chown -R orkes:orkes /home/orkes /app + +# Switch to non-root user +USER orkes +WORKDIR /app + +# Verify installations +RUN orkes --version && java -version + +# Labels +LABEL org.opencontainers.image.title="Orkes CLI Runner - Java" \ + org.opencontainers.image.description="Orkes CLI with OpenJDK 21 JRE" \ + org.opencontainers.image.source="https://github.com/conductor-oss/conductor-cli" \ + org.opencontainers.image.vendor="Orkes" \ + java.version="21" \ + maintainer="Orkes " + +ENTRYPOINT ["orkes"] +CMD ["--help"] diff --git a/docker/Dockerfile-node b/docker/Dockerfile-node new file mode 100644 index 0000000..c3cc10d --- /dev/null +++ b/docker/Dockerfile-node @@ -0,0 +1,60 @@ +# Stage 1: Build the orkes CLI +FROM golang:1.23-alpine AS builder + +# Install build dependencies +RUN apk add --no-cache git ca-certificates + +WORKDIR /build + +# Copy go mod files (better caching) +COPY go.mod go.sum ./ +RUN go mod download + +# Copy source code +COPY . . + +# Build the binary +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o orkes . + +# Stage 2: Runtime with Node.js +FROM node:20-alpine3.19 + +# Install utilities +RUN apk add --no-cache \ + ca-certificates \ + curl \ + wget \ + git \ + bash \ + jq \ + && rm -rf /var/cache/apk/* + +# Create non-root user +RUN addgroup -g 1000 orkes && \ + adduser -D -u 1000 -G orkes orkes + +# Copy orkes CLI binary +COPY --from=builder /build/orkes /usr/local/bin/orkes +RUN chmod +x /usr/local/bin/orkes + +# Create directories +RUN mkdir -p /home/orkes/.conductor-cli /app && \ + chown -R orkes:orkes /home/orkes /app + +# Switch to non-root user +USER orkes +WORKDIR /app + +# Verify installations +RUN orkes --version && node --version && npm --version + +# Labels +LABEL org.opencontainers.image.title="Orkes CLI Runner - Node.js" \ + org.opencontainers.image.description="Orkes CLI with Node.js 20 LTS runtime" \ + org.opencontainers.image.source="https://github.com/conductor-oss/conductor-cli" \ + org.opencontainers.image.vendor="Orkes" \ + node.version="20" \ + maintainer="Orkes " + +ENTRYPOINT ["orkes"] +CMD ["--help"] diff --git a/docker/Dockerfile-python b/docker/Dockerfile-python new file mode 100644 index 0000000..5ed3fb9 --- /dev/null +++ b/docker/Dockerfile-python @@ -0,0 +1,63 @@ +# Stage 1: Build the orkes CLI +FROM golang:1.23-alpine AS builder + +# Install build dependencies +RUN apk add --no-cache git ca-certificates + +WORKDIR /build + +# Copy go mod files (better caching) +COPY go.mod go.sum ./ +RUN go mod download + +# Copy source code +COPY . . + +# Build the binary +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o orkes . + +# Stage 2: Runtime with Python +FROM python:3.12-alpine3.19 + +# Install utilities and build tools (for Python packages with native extensions) +RUN apk add --no-cache \ + ca-certificates \ + curl \ + wget \ + git \ + bash \ + jq \ + gcc \ + musl-dev \ + libffi-dev \ + && rm -rf /var/cache/apk/* + +# Create non-root user +RUN addgroup -g 1000 orkes && \ + adduser -D -u 1000 -G orkes orkes + +# Copy orkes CLI binary +COPY --from=builder /build/orkes /usr/local/bin/orkes +RUN chmod +x /usr/local/bin/orkes + +# Create directories +RUN mkdir -p /home/orkes/.conductor-cli /app && \ + chown -R orkes:orkes /home/orkes /app + +# Switch to non-root user +USER orkes +WORKDIR /app + +# Verify installations +RUN orkes --version && python --version + +# Labels +LABEL org.opencontainers.image.title="Orkes CLI Runner - Python" \ + org.opencontainers.image.description="Orkes CLI with Python 3.12 runtime" \ + org.opencontainers.image.source="https://github.com/conductor-oss/conductor-cli" \ + org.opencontainers.image.vendor="Orkes" \ + python.version="3.12" \ + maintainer="Orkes " + +ENTRYPOINT ["orkes"] +CMD ["--help"] diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..05a8b16 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,287 @@ +# Orkes CLI Docker Images + +Pre-built Docker images for running stdio workers in any language. + +## Available Images + +| Image Tag | Runtime | Base Image | Size | Use Case | +|-----------|---------|------------|------|----------| +| `orkes/cli-runner:base` | CLI only | Alpine 3.19 | ~40MB | CLI operations + shell-based workers | +| `orkes/cli-runner:python` | Python 3.12 | Python Alpine | ~100MB | Python stdio workers | +| `orkes/cli-runner:node` | Node.js 20 LTS | Node Alpine | ~180MB | JavaScript/TypeScript workers | +| `orkes/cli-runner:java` | OpenJDK 21 JRE | Temurin Alpine | ~220MB | Java workers | +| `orkes/cli-runner:go` | Go 1.23 | Go Alpine | ~400MB | Go workers | +| `orkes/cli-runner:dotnet` | .NET 8 | .NET Alpine | ~220MB | C# workers | + +## Quick Start + +### Run a Python worker + +```bash +docker run --rm \ + -e CONDUCTOR_SERVER_URL=https://developer.orkescloud.com/api \ + -e CONDUCTOR_AUTH_TOKEN=your-token \ + -v $(pwd)/worker.py:/app/worker.py:ro \ + orkes/cli-runner:python \ + worker stdio --type greeting_task python /app/worker.py +``` + +### Run with Docker Compose + +```yaml +version: '3.8' +services: + worker: + image: orkes/cli-runner:python + command: worker stdio --type my_task python /app/worker.py + environment: + CONDUCTOR_SERVER_URL: https://developer.orkescloud.com/api + CONDUCTOR_AUTH_TOKEN: ${AUTH_TOKEN} + volumes: + - ./worker.py:/app/worker.py:ro +``` + +## Authentication + +Three methods supported (in order of precedence): + +### 1. Environment Variables (Recommended for containers) + +```bash +docker run \ + -e CONDUCTOR_SERVER_URL=https://your-server.com/api \ + -e CONDUCTOR_AUTH_TOKEN=your-token \ + orkes/cli-runner:python worker stdio ... +``` + +**Available environment variables:** +- `CONDUCTOR_SERVER_URL` - Conductor server URL +- `CONDUCTOR_AUTH_TOKEN` - Authentication token +- `CONDUCTOR_AUTH_KEY` - API key (alternative to token) +- `CONDUCTOR_AUTH_SECRET` - API secret (used with key) + +### 2. Mounted Config File + +```bash +docker run \ + -v ~/.conductor-cli/config.yaml:/home/orkes/.conductor-cli/config.yaml:ro \ + orkes/cli-runner:python worker stdio ... +``` + +### 3. Command-line Flags + +```bash +docker run orkes/cli-runner:python \ + --server https://your-server.com/api \ + --auth-token your-token \ + worker stdio ... +``` + +## Worker Examples + +### Python Worker (simple_worker.py) + +```python +#!/usr/bin/env python3 +import sys +import json + +task = json.load(sys.stdin) +name = task.get('inputData', {}).get('name', 'World') + +result = { + "status": "COMPLETED", + "output": {"message": f"Hello, {name}!"}, + "logs": [f"Processed task {task.get('taskId')}"] +} + +print(json.dumps(result)) +``` + +### Node.js Worker (simple_worker.js) + +```javascript +#!/usr/bin/env node +let input = ''; +process.stdin.on('data', chunk => input += chunk); +process.stdin.on('end', () => { + const task = JSON.parse(input); + const name = (task.inputData || {}).name || 'World'; + + const result = { + status: 'COMPLETED', + output: { message: `Hello, ${name}!` }, + logs: [`Processed task ${task.taskId}`] + }; + + console.log(JSON.stringify(result)); +}); +``` + +See [`../docker-examples/workers/`](../docker-examples/workers/) for complete examples in all languages. + +## Building Images + +### Build all images + +```bash +./docker/build-images.sh +``` + +### Build with version tag + +```bash +VERSION=v1.0.0 ./docker/build-images.sh +``` + +### Build and push to registry + +```bash +VERSION=v1.0.0 PUSH=true REGISTRY=myregistry ./docker/build-images.sh +``` + +### Build specific variant + +```bash +docker build -f docker/Dockerfile-python -t orkes/cli-runner:python . +``` + +### Multi-architecture build + +```bash +docker buildx build \ + --platform linux/amd64,linux/arm64 \ + -f docker/Dockerfile-python \ + -t orkes/cli-runner:python \ + --push . +``` + +## Testing Images + +```bash +./docker/test-images.sh +``` + +## Advanced Usage + +### Installing Additional Dependencies + +#### Python packages + +```dockerfile +FROM orkes/cli-runner:python +COPY requirements.txt /tmp/ +RUN pip install --no-cache-dir -r /tmp/requirements.txt +``` + +#### Node.js packages + +```dockerfile +FROM orkes/cli-runner:node +WORKDIR /app +COPY package*.json ./ +RUN npm ci --only=production +COPY worker.js . +``` + +### Running Java/Spring Boot Workers + +```dockerfile +FROM eclipse-temurin:21-jdk-alpine AS builder +WORKDIR /build +COPY . . +RUN ./gradlew build --no-daemon + +FROM orkes/cli-runner:java +WORKDIR /app +COPY --from=builder /build/build/libs/*.jar app.jar +CMD ["worker", "stdio", "--type", "my_task", "java", "-jar", "app.jar"] +``` + +Or run directly: + +```bash +docker run --rm \ + -e CONDUCTOR_SERVER_URL=https://developer.orkescloud.com/api \ + -e CONDUCTOR_AUTH_TOKEN=your-token \ + -v $(pwd):/workspace \ + -w /workspace \ + orkes/cli-runner:java \ + sh -c './gradlew bootRun' +``` + +### Using Config Profiles + +```bash +docker run \ + -e ORKES_PROFILE=production \ + -v ~/.conductor-cli/config-production.yaml:/home/orkes/.conductor-cli/config-production.yaml:ro \ + orkes/cli-runner:python worker stdio ... +``` + +### Batch Polling + +Process multiple tasks concurrently: + +```bash +docker run orkes/cli-runner:python \ + worker stdio --type my_task --count 10 python /app/worker.py +``` + +### Verbose Logging + +Enable detailed logging for debugging: + +```bash +docker run orkes/cli-runner:python \ + worker stdio --type my_task --verbose python /app/worker.py +``` + +## Troubleshooting + +### Worker not polling tasks + +**Check:** +1. Authentication credentials are correct +2. Server URL is accessible from container +3. Task type matches workflow definition +4. Network connectivity: `docker run orkes/cli-runner:python curl $CONDUCTOR_SERVER_URL` + +### Permission denied errors + +**Ensure:** +1. Worker files are readable by UID 1000 +2. Use read-only mounts (`:ro`) for worker code +3. Don't mount root-owned files without proper permissions + +### Container exits immediately + +**Check:** +1. Command syntax is correct +2. Worker script has correct shebang (e.g., `#!/usr/bin/env python3`) +3. Worker file has execute permissions +4. View logs: `docker logs ` + +### Runtime errors + +**Debug:** +1. Test worker locally first: `echo '{"inputData":{}}' | python worker.py` +2. Use `--verbose` flag to see task and result JSON +3. Check worker logs in task execution output + +## Examples + +Complete examples available in [`../docker-examples/`](../docker-examples/): + +- **workers/** - Worker scripts for all languages +- **docker-compose/** - Docker Compose configurations + +## Support + +- **Documentation**: https://orkes.io/content +- **GitHub Issues**: https://github.com/conductor-oss/conductor-cli/issues +- **Community**: Conductor Slack + +## License + +Apache 2.0 - See [LICENSE](../LICENSE) file diff --git a/docker/build-images.sh b/docker/build-images.sh new file mode 100755 index 0000000..dc6d6a3 --- /dev/null +++ b/docker/build-images.sh @@ -0,0 +1,95 @@ +#!/bin/bash +set -e + +VERSION=${VERSION:-"latest"} +REGISTRY=${REGISTRY:-"orkes"} +PUSH=${PUSH:-"false"} +PLATFORM=${PLATFORM:-"linux/amd64"} + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${GREEN}╔════════════════════════════════════════╗${NC}" +echo -e "${GREEN}║ Building Orkes CLI Docker Images ║${NC}" +echo -e "${GREEN}╚════════════════════════════════════════╝${NC}" +echo "" +echo -e "${BLUE}Configuration:${NC}" +echo -e " Version: ${YELLOW}${VERSION}${NC}" +echo -e " Registry: ${YELLOW}${REGISTRY}${NC}" +echo -e " Platform: ${YELLOW}${PLATFORM}${NC}" +echo -e " Push: ${YELLOW}${PUSH}${NC}" +echo "" + +# Build base image +echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "${YELLOW}Building base image...${NC}" +echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +docker build \ + -f docker/Dockerfile-base \ + -t ${REGISTRY}/cli-runner:${VERSION} \ + -t ${REGISTRY}/cli-runner:base-${VERSION} \ + --platform ${PLATFORM} \ + . +echo -e "${GREEN}✓ Base image built successfully${NC}\n" + +# Build runtime variants +for VARIANT in python node java go dotnet; do + echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${YELLOW}Building ${VARIANT} image...${NC}" + echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + docker build \ + -f docker/Dockerfile-${VARIANT} \ + -t ${REGISTRY}/cli-runner:${VARIANT}-${VERSION} \ + --platform ${PLATFORM} \ + . + echo -e "${GREEN}✓ ${VARIANT} image built successfully${NC}\n" +done + +# Tag latest +if [ "$VERSION" = "latest" ]; then + echo -e "${BLUE}Tagging latest versions...${NC}" + for VARIANT in base python node java go dotnet; do + docker tag ${REGISTRY}/cli-runner:${VARIANT}-${VERSION} ${REGISTRY}/cli-runner:${VARIANT} + echo -e "${GREEN}✓ Tagged ${REGISTRY}/cli-runner:${VARIANT}${NC}" + done + echo "" +fi + +# Push if requested +if [ "$PUSH" = "true" ]; then + echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${YELLOW}Pushing images to registry...${NC}" + echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + + docker push ${REGISTRY}/cli-runner:${VERSION} + docker push ${REGISTRY}/cli-runner:base-${VERSION} + + for VARIANT in python node java go dotnet; do + docker push ${REGISTRY}/cli-runner:${VARIANT}-${VERSION} + echo -e "${GREEN}✓ Pushed ${VARIANT}-${VERSION}${NC}" + done + + if [ "$VERSION" = "latest" ]; then + docker push ${REGISTRY}/cli-runner:base + for VARIANT in python node java go dotnet; do + docker push ${REGISTRY}/cli-runner:${VARIANT} + echo -e "${GREEN}✓ Pushed ${VARIANT}${NC}" + done + fi + echo "" +fi + +echo -e "${GREEN}╔════════════════════════════════════════╗${NC}" +echo -e "${GREEN}║ Build Complete! ✓ ║${NC}" +echo -e "${GREEN}╚════════════════════════════════════════╝${NC}" +echo "" +echo -e "${BLUE}Built images:${NC}" +echo -e " ${REGISTRY}/cli-runner:${VERSION}" +echo -e " ${REGISTRY}/cli-runner:base-${VERSION}" +for VARIANT in python node java go dotnet; do + echo -e " ${REGISTRY}/cli-runner:${VARIANT}-${VERSION}" +done +echo "" diff --git a/docker/test-images.sh b/docker/test-images.sh new file mode 100755 index 0000000..0d04054 --- /dev/null +++ b/docker/test-images.sh @@ -0,0 +1,142 @@ +#!/bin/bash +set -e + +REGISTRY=${REGISTRY:-"orkes"} +VERSION=${VERSION:-"latest"} + +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +TESTS_PASSED=0 +TESTS_FAILED=0 + +echo -e "${GREEN}╔════════════════════════════════════════╗${NC}" +echo -e "${GREEN}║ Testing Orkes CLI Docker Images ║${NC}" +echo -e "${GREEN}╚════════════════════════════════════════╝${NC}" +echo "" + +test_image() { + local variant=$1 + local image="${REGISTRY}/cli-runner:${variant}-${VERSION}" + + echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${YELLOW}Testing ${variant} image${NC}" + echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + + # Test 1: Image exists + if ! docker image inspect "$image" >/dev/null 2>&1; then + echo -e "${RED}✗ Image not found: $image${NC}" + ((TESTS_FAILED++)) + return 1 + fi + echo -e "${GREEN}✓ Image exists${NC}" + + # Test 2: CLI runs and shows version + if ! docker run --rm "$image" --version >/dev/null 2>&1; then + echo -e "${RED}✗ CLI failed to run${NC}" + ((TESTS_FAILED++)) + return 1 + fi + echo -e "${GREEN}✓ CLI runs successfully${NC}" + + # Test 3: Runtime available + case $variant in + base) + if ! docker run --rm "$image" bash --version >/dev/null 2>&1; then + echo -e "${RED}✗ Bash not available${NC}" + ((TESTS_FAILED++)) + return 1 + fi + echo -e "${GREEN}✓ Bash available${NC}" + ;; + python) + if ! docker run --rm "$image" python --version >/dev/null 2>&1; then + echo -e "${RED}✗ Python not available${NC}" + ((TESTS_FAILED++)) + return 1 + fi + echo -e "${GREEN}✓ Python available${NC}" + ;; + node) + if ! docker run --rm "$image" node --version >/dev/null 2>&1; then + echo -e "${RED}✗ Node.js not available${NC}" + ((TESTS_FAILED++)) + return 1 + fi + echo -e "${GREEN}✓ Node.js available${NC}" + ;; + java) + if ! docker run --rm "$image" java -version >/dev/null 2>&1; then + echo -e "${RED}✗ Java not available${NC}" + ((TESTS_FAILED++)) + return 1 + fi + echo -e "${GREEN}✓ Java available${NC}" + ;; + go) + if ! docker run --rm "$image" go version >/dev/null 2>&1; then + echo -e "${RED}✗ Go not available${NC}" + ((TESTS_FAILED++)) + return 1 + fi + echo -e "${GREEN}✓ Go available${NC}" + ;; + dotnet) + if ! docker run --rm "$image" dotnet --version >/dev/null 2>&1; then + echo -e "${RED}✗ .NET not available${NC}" + ((TESTS_FAILED++)) + return 1 + fi + echo -e "${GREEN}✓ .NET available${NC}" + ;; + esac + + # Test 4: Non-root user + USER_CHECK=$(docker run --rm "$image" whoami 2>/dev/null || echo "error") + if [ "$USER_CHECK" != "orkes" ]; then + echo -e "${RED}✗ Not running as orkes user (found: $USER_CHECK)${NC}" + ((TESTS_FAILED++)) + return 1 + fi + echo -e "${GREEN}✓ Running as non-root user (orkes)${NC}" + + # Test 5: Required utilities + for util in curl wget git jq; do + if ! docker run --rm "$image" which $util >/dev/null 2>&1; then + echo -e "${RED}✗ Utility $util not found${NC}" + ((TESTS_FAILED++)) + return 1 + fi + done + echo -e "${GREEN}✓ All required utilities present (curl, wget, git, jq)${NC}" + + ((TESTS_PASSED++)) + echo -e "${GREEN}✓ All tests passed for ${variant}${NC}\n" +} + +# Test all variants +for variant in base python node java go dotnet; do + test_image "$variant" +done + +echo -e "${GREEN}╔════════════════════════════════════════╗${NC}" +echo -e "${GREEN}║ Test Summary ║${NC}" +echo -e "${GREEN}╚════════════════════════════════════════╝${NC}" +echo "" +echo -e "${BLUE}Images tested:${NC} 6" +echo -e "${GREEN}Tests passed:${NC} ${TESTS_PASSED}" + +if [ $TESTS_FAILED -gt 0 ]; then + echo -e "${RED}Tests failed:${NC} ${TESTS_FAILED}" + echo "" + echo -e "${RED}Some tests failed!${NC}" + exit 1 +else + echo -e "${RED}Tests failed:${NC} 0" + echo "" + echo -e "${GREEN}All tests passed! ✓${NC}" + exit 0 +fi