Custom Plugins Examples¶
Plugin 1: Log All Resolved Values¶
Prints every service's resolved environment variables and configuration after the resolve step.
plugins/audit-log.js¶
module.exports = async function auditLog(parsed) {
console.log(`[audit] Stack: ${parsed.name}`);
console.log(`[audit] Services: ${Object.keys(parsed.services).join(', ')}`);
for (const [name, service] of Object.entries(parsed.services)) {
console.log(`[audit] --- ${name} ---`);
console.log(`[audit] image: ${service.image}`);
console.log(`[audit] provider: ${service.provider.name}`);
console.log(`[audit] replica: ${JSON.stringify(service.scale.replica)}`);
if (service.env) {
for (const [k, v] of Object.entries(service.env)) {
// Mask sensitive values
const display = k.match(/SECRET|PASSWORD|TOKEN|KEY/i) ? '***' : v;
console.log(`[audit] env.${k}: ${display}`);
}
}
}
};
pctl.yaml¶
plugin:
- ./plugins/audit-log.js
name: my-app
services:
api:
image: ./Dockerfile
env:
NODE_ENV: production
DB_PASSWORD: "${ssm:/prod/db-pass}"
scale:
replica: 1
provider:
name: docker
Output during deploy:
[audit] Stack: my-app
[audit] Services: api
[audit] --- api ---
[audit] image: ./Dockerfile
[audit] provider: docker
[audit] replica: 1
[audit] env.NODE_ENV: production
[audit] env.DB_PASSWORD: ***
Plugin 2: Validate Environment Variables Exist¶
Ensures critical env vars are present after resolution. Throws if any are null or empty.
plugins/require-env.js¶
const REQUIRED = {
api: ['NODE_ENV', 'DB_URL', 'PORT'],
worker: ['QUEUE_URL'],
};
module.exports = async function requireEnv(parsed) {
for (const [name, service] of Object.entries(parsed.services)) {
const required = REQUIRED[name];
if (!required) continue;
for (const key of required) {
const value = service.env?.[key];
if (!value || value === 'null') {
throw new Error(`[require-env] Service "${name}" is missing required env var: ${key}`);
}
}
}
};
pctl.yaml¶
plugin:
- ./plugins/require-env.js
name: my-app
services:
api:
image: ./Dockerfile
env:
NODE_ENV: production
DB_URL: "${ssm:/prod/db-url}"
PORT: "3000"
scale:
replica: 1
provider:
name: docker
worker:
image: ./worker/Dockerfile
env:
QUEUE_URL: "${env:QUEUE_URL}"
scale:
replica: 1
provider:
name: docker
If QUEUE_URL is not set in the environment:
Plugin 3: Add Default Labels via Custom¶
Injects deployment metadata into the custom block for downstream use.
plugins/deploy-meta.js¶
module.exports = async function deployMeta(parsed) {
parsed.custom.deployed_at = new Date().toISOString();
parsed.custom.deployed_by = process.env.USER || 'ci';
parsed.custom.git_sha = (() => {
try {
return require('child_process')
.execSync('git rev-parse --short HEAD', { encoding: 'utf-8' })
.trim();
} catch {
return 'unknown';
}
})();
// Inject into every service's env
for (const service of Object.values(parsed.services)) {
service.env = service.env || {};
service.env.DEPLOY_SHA = parsed.custom.git_sha;
service.env.DEPLOY_TIME = parsed.custom.deployed_at;
}
};
pctl.yaml¶
plugin:
- ./plugins/deploy-meta.js
name: my-app
services:
api:
image: ./Dockerfile
env:
NODE_ENV: production
scale:
replica: 1
provider:
name: docker
After the plugin runs, the API container receives:
Plugin 4: Scale Based on Time of Day¶
Adjusts auto-scaling ranges during off-hours to save resources.
plugins/time-scale.js¶
module.exports = async function timeScale(parsed) {
const hour = new Date().getHours();
const isOffHours = hour < 8 || hour > 20;
if (!isOffHours) return;
for (const [name, service] of Object.entries(parsed.services)) {
if (!Array.isArray(service.scale.replica)) continue;
const [min, max] = service.scale.replica;
const newMin = Math.max(1, Math.floor(min / 2));
const newMax = Math.max(2, Math.floor(max / 2));
console.log(`[time-scale] "${name}": off-hours, scaling [${min},${max}] -> [${newMin},${newMax}]`);
service.scale.replica = [newMin, newMax];
}
};
pctl.yaml¶
plugin:
- ./plugins/time-scale.js
name: my-app
services:
api:
image: ./Dockerfile
scale:
replica: [4, 40]
cpu: 500m
memory: 1Gi
ports:
- 3000
provider:
name: aws
options:
cluster: prod
namespace: production
worker:
image: ./worker/Dockerfile
scale:
replica: 10
cpu: 1
memory: 2Gi
provider:
name: aws
options:
cluster: prod
namespace: production
During off-hours (before 8 AM or after 8 PM):
The worker service has a fixed replica count (not an array), so it is not modified.