Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
301 changes: 189 additions & 112 deletions Cargo.lock

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,43 @@ These packages are natively implemented in Rust — no Node.js required:
| **Database** | mysql2, pg, ioredis |
| **Security** | bcrypt, argon2, jsonwebtoken |
| **Utilities** | dotenv, uuid, nodemailer, zlib, node-cron |
| **Container** | perry/container (OCI container management) |

---

## Container Module

Perry includes a native container management module `perry/container` for creating, running, and managing OCI containers:

```typescript
import { run, list, composeUp } from 'perry/container';

// Run a container
const container = await run({
image: 'nginx:alpine',
name: 'my-nginx',
ports: ['8080:80'],
});

// List containers
const containers = await list();
console.log(containers);

// Multi-container orchestration
const compose = await composeUp({
services: {
web: { image: 'nginx:alpine' },
db: { image: 'postgres:15-alpine' },
},
});
```

**Platform support:**
- macOS/iOS: Podman (apple/container support coming soon)
- Linux: Podman (native)
- Windows: Podman Desktop (experimental)

See `example-code/container-demo/` for a complete example.

---

Expand Down
505 changes: 283 additions & 222 deletions crates/perry-codegen/src/lower_call.rs

Large diffs are not rendered by default.

45 changes: 45 additions & 0 deletions crates/perry-container-compose/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
[package]
name = "perry-container-compose"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
authors = ["Perry Contributors"]
description = "Port of container-compose/cli to Rust - Docker Compose-like experience for Apple Container / Podman"

[dependencies]
serde = { workspace = true }
serde_json = { workspace = true }
serde_yaml = "0.9"
tokio = { workspace = true }
clap = { workspace = true }
anyhow = { workspace = true }
thiserror = { workspace = true }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
async-trait = "0.1"
md-5 = "0.10"
hex = "0.4"
dotenvy = { workspace = true }
indexmap = { version = "2.2", features = ["serde"] }
dashmap = "5"
rand = "0.8"
regex = "1"
atty = "0.2"
dialoguer = "0.11"
console = "0.15"
once_cell = "1"
which = "6.0"

[dev-dependencies]
tokio = { workspace = true }
proptest = "1"

[features]
default = []
ffi = [] # Enable FFI exports for Perry TypeScript integration
integration-tests = [] # Tests that require a running container backend

[[bin]]
name = "perry-compose"
path = "src/main.rs"
23 changes: 23 additions & 0 deletions crates/perry-container-compose/examples/build/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { composeUp, composeDown } from 'perry/compose';

const stack = await composeUp({
version: '3.8',
services: {
app: {
build: {
context: '.',
dockerfile: 'Dockerfile',
args: {
BUILD_ENV: 'production',
},
},
ports: ['8080:8080'],
environment: {
NODE_ENV: 'production',
},
},
},
});

// Tear down when done
await composeDown(stack);
208 changes: 208 additions & 0 deletions crates/perry-container-compose/examples/forgejo/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/**
* perry-container-compose — Production Forgejo Stack Example
*
* This example demonstrates a production-ready Forgejo (self-hosted Git service)
* deployment using Perry's container-compose API.
*
* Architecture:
* - forgejo: Main Forgejo application (gitea/gitea)
* - postgres: PostgreSQL database for Forgejo data
*
* Features:
* - Named volumes for persistent data
* - Custom networks for service isolation
* - Health checks and restart policies
* - Environment variable interpolation
* - Proper port mapping with firewall considerations
*/

import { composeUp, getBackend } from 'perry/container-compose';

// ──────────────────────────────────────────────────────────────
// Verify Backend Support
// ──────────────────────────────────────────────────────────────

const backend = getBackend();
console.log(`🔧 Using container backend: ${backend}\n`);

// ──────────────────────────────────────────────────────────────
// Forgejo Production Stack Configuration
// ──────────────────────────────────────────────────────────────

const FORGEJO_VERSION = '1.23-stable';
const postgresVersion = '16-alpine';

// Stack name for tracking
const stack = await composeUp({
version: '3.8',
services: {
postgres: {
image: `postgres:${postgresVersion}`,
restart: 'always',
environment: {
POSTGRES_USER: '${FORGEJO_DB_USER:-forgejo}',
POSTGRES_PASSWORD: '${FORGEJO_DB_PASSWORD:-changeme}',
POSTGRES_DB: '${FORGEJO_DB_NAME:-forgejo}',
},
volumes: ['forgejo-pgdata:/var/lib/postgresql/data'],
ports: ['5432:5432'],
networks: ['forgejo-network'],
},
forgejo: {
image: `codeberg.org/forgejo/forgejo:${FORGEJO_VERSION}`,
restart: 'always',
dependsOn: ['postgres'],
environment: {
// Database configuration
FORGEJO__database__HOST: '${FORGEJO_DB_HOST:-postgres:5432}',
FORGEJO__database__name: '${FORGEJO_DB_NAME:-forgejo}',
FORGEJO__database__user: '${FORGEJO_DB_USER:-forgejo}',
FORGEJO__database__passwd: '${FORGEJO_DB_PASSWORD:-changeme}',
// URL configuration (adjust for your setup)
FORGEJO__server__PROTOCOL: '${FORGEJO_PROTOCOL:-http}',
FORGEJO__server__DOMAIN: '${FORGEJO_DOMAIN:-localhost}',
FORGEJO__server__ROOT_URL: '${FORGEJO_ROOT_URL:-http://localhost:3000}',
// Admin configuration
FORGEJO__security__INSTALL_LOCK: 'true',
FORGEJO__service__DISABLE_REGISTRATION: 'false',
FORGEJO__service__REQUIRE_SIGNIN: 'true',
},
volumes: [
'forgejo-data:/data',
'forgejo-config:/config',
'/etc/timezone:/etc/timezone:ro',
'/etc/localtime:/etc/localtime:ro',
],
ports: ['3000:3000', '2222:22'],
networks: ['forgejo-network'],
},
},
networks: {
'forgejo-network': {
driver: 'bridge',
},
},
volumes: {
'forgejo-pgdata': {
driver: 'local',
},
'forgejo-data': {
driver: 'local',
},
'forgejo-config': {
driver: 'local',
},
},
});

// ──────────────────────────────────────────────────────────────
// Verify Stack Status
// ──────────────────────────────────────────────────────────────

console.log('\n🔍 Checking Forgejo stack status...\n');

const statuses = await stack.ps();
console.table(statuses);

// Verify both services are running
const allRunning = statuses.every((s) => s.status === 'running' || s.status.includes('Up'));
if (!allRunning) {
console.error('❌ Not all services are running!');
console.log('Logs from forgejo service:');
const logs = await stack.logs({ service: 'forgejo', tail: 50 });
console.log(logs.stdout);
await stack.down({ volumes: true });
process.exit(1);
}

console.log('✅ Stack is up and running!');

// ──────────────────────────────────────────────────────────────
// Health Check: Verify PostgreSQL is ready
// ──────────────────────────────────────────────────────────────

console.log('\n🏥 Performing health checks...\n');

const postgresHealth = await stack.exec('postgres', [
'pg_isready',
'-U',
'forgejo',
'-d',
'forgejo',
]);

if (postgresHealth.stdout.includes('accepting connections')) {
console.log('✅ PostgreSQL: ready');
} else {
console.error('❌ PostgreSQL: not ready');
console.error('stderr:', postgresHealth.stderr);
await stack.down({ volumes: true });
process.exit(1);
}

// ──────────────────────────────────────────────────────────────
// First Run Setup: Get Initial Admin Credentials
// ──────────────────────────────────────────────────────────────

console.log('\n📋 First run: Fetching initial admin setup info...\n');

const initScript = await stack.exec(
'forgejo',
['bash', '-c', 'type setup 2>/dev/null || echo "Setup not required"']
);

console.log('Initial setup status:', initScript.stdout.trim() || 'complete');

// ──────────────────────────────────────────────────────────────
// Usage Instructions
// ──────────────────────────────────────────────────────────────

console.log(`
─────────────────────────────────────────────────────────────
🎉 Forgejo Stack is Ready!
─────────────────────────────────────────────────────────────

Access URLs:
- Web UI: http://localhost:3000
- SSH: ssh://localhost:2222

Default admin account (first-run):
- Username: root
- Password: (set via web UI on first login)

Environment variables used:
FORGEJO_DB_USER=forgejo
FORGEJO_DB_PASSWORD=changeme (change in production!)
FORGEJO_DB_NAME=forgejo
FORGEJO_DOMAIN=localhost
FORGEJO_ROOT_URL=http://localhost:3000

Useful commands:
# View logs
await stack.logs({ service: 'forgejo', tail: 100 });

# Execute command in forgejo container
await stack.exec('forgejo', ['ls', '/data/gitea/conf']);

# Stop stack (preserves data)
await stack.down();

# Stop stack and remove volumes (destroys all data)
await stack.down({ volumes: true });

─────────────────────────────────────────────────────────────
`);

// ──────────────────────────────────────────────────────────────
// Cleanup on SIGINT/SIGTERM
// ──────────────────────────────────────────────────────────────

const cleanup = async () => {
console.log('\n🧹 Cleaning up stack...');
await stack.down({ volumes: true });
console.log('✅ Cleanup complete');
process.exit(0);
};

process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
36 changes: 36 additions & 0 deletions crates/perry-container-compose/examples/multi-service/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { composeUp, composeDown, composeLogs } from 'perry/compose';

const stack = await composeUp({
version: '3.8',
services: {
db: {
image: 'postgres:16-alpine',
environment: {
// ${VAR:-default} interpolation is supported in string values
POSTGRES_USER: '${DB_USER:-myuser}',
POSTGRES_PASSWORD: '${DB_PASSWORD:-secret}',
POSTGRES_DB: 'mydb',
},
volumes: ['db-data:/var/lib/postgresql/data'],
ports: ['5432:5432'],
},
web: {
image: 'myapp:latest',
dependsOn: ['db'],
ports: ['3000:3000'],
environment: {
DATABASE_URL: 'postgres://${DB_USER:-myuser}:${DB_PASSWORD:-secret}@db:5432/mydb',
},
},
},
volumes: {
'db-data': {},
},
});

// Stream logs from both services
const logs = await composeLogs(stack, { services: ['web', 'db'], follow: false });
console.log(logs);

// Tear down, removing named volumes
await composeDown(stack, { volumes: true });
21 changes: 21 additions & 0 deletions crates/perry-container-compose/examples/simple/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { composeUp, composeDown, composePs } from 'perry/compose';

const stack = await composeUp({
version: '3.8',
services: {
web: {
image: 'nginx:alpine',
containerName: 'simple-nginx',
ports: ['8080:80'],
labels: {
app: 'simple-nginx',
},
},
},
});

const statuses = await composePs(stack);
console.table(statuses);

// Tear down when done
await composeDown(stack);
Loading