API de Proveedores¶
Interfaz¶
Un proveedor es una funcion asincrona que recibe parsed y detecta el comando (deploy/destroy) de los argumentos del proceso:
async function myProvider(parsed: z.infer<typeof Schema>): Promise<void> {
const cmd = process.argv.includes('deploy') ? 'deploy'
: process.argv.includes('destroy') ? 'destroy' : null;
if (!cmd) return;
const services = Object.entries(parsed.services)
.filter(([, s]) => s.provider.name === 'myprovider');
if (!services.length) return;
// deploy o destroy
}
Los proveedores:
- Detectan el comando de
process.argv - Filtran servicios por
provider.name - Si no hay servicios del proveedor, retornan sin hacer nada
- Ejecutan deploy o destroy
Archivo de estado¶
Cada proveedor lee y escribe el archivo pctl.{name}.json:
function stFile(stack: string) {
return resolvePath(process.cwd(), `pctl.${stack}.json`);
}
function stRead(stack: string): Record<string, any> {
try { return JSON.parse(readFileSync(stFile(stack), 'utf-8')); }
catch { return {}; }
}
function stWrite(stack: string, state: Record<string, any>) {
writeFileSync(stFile(stack), JSON.stringify(state, null, 2));
}
El estado es un mapa de {stack}-{service} a metadata del recurso. Cada proveedor agrega sus propios campos.
Formato AWS¶
{
"my-app-api": {
"provider": "aws",
"cluster": "prod",
"namespace": "app",
"registryUrl": "507738...ecr.../api",
"image": "507738...ecr.../api:1710234567890",
"labels": { "managed-by": "pctl", "pctl-stack": "my-app", "pctl-service": "my-app-api" },
"fingerprint": "sha256...",
"hasPorts": true,
"hasHpa": true,
"hasRbac": false,
"hasPvc": false,
"hasPv": false,
"hasPullSecret": false,
"pushedByPctl": true
}
}
Formato GCP¶
{
"my-app-api": {
"provider": "gcp",
"project": "my-project",
"zone": "us-central1-a",
"cluster": "prod",
"namespace": "app",
"registryUrl": "us-central1-docker.pkg.dev/.../api",
"image": "us-central1-docker.pkg.dev/.../api:1710234567890",
"labels": { "managed-by": "pctl", "pctl-stack": "my-app", "pctl-service": "my-app-api" },
"fingerprint": "sha256...",
"hasPorts": true,
"hasHpa": true,
"hasRbac": false,
"hasPvc": false,
"hasPv": false,
"hasPullSecret": false,
"pushedByPctl": true
}
}
Formato Docker¶
{
"my-app-api": {
"provider": "docker",
"host": "local",
"user": null,
"key": null,
"sudo": false,
"registryUrl": null,
"image": "pctl-local:1710234567890",
"labels": { "managed-by": "pctl", "pctl-stack": "my-app", "pctl-service": "my-app-api" },
"fingerprint": "sha256...",
"replica": 1,
"hasPorts": true,
"pushedByPctl": true
}
}
Labels¶
Todos los proveedores aplican labels consistentes:
const lbl = (stack: string, name: string) => ({
'managed-by': 'pctl',
'pctl-stack': stack,
'pctl-service': `${stack}-${name}`
});
| Label | Descripcion |
|---|---|
managed-by | Siempre pctl. Identifica recursos gestionados por pctl |
pctl-stack | Nombre del stack |
pctl-service | {stack}-{service}. Identificador unico del servicio |
En Kubernetes, los labels se aplican a Deployment, Service, HPA, PV, PVC, ServiceAccount, Role, RoleBinding y Secret.
En Docker, se aplican como --label al contenedor.
Fingerprint¶
El fingerprint es un hash SHA-256 del servicio (excluyendo provider) mas el contenido del Dockerfile (si aplica):
function fingerprint(service: Service, configDir: string): string {
const { provider, ...rest } = service;
// Ordena claves recursivamente para consistencia
let hash = JSON.stringify(sortKeys(rest));
if (service.image.startsWith('./')) {
const dockerfile = resolvePath(configDir, service.image);
if (existsSync(dockerfile)) hash += createHash('md5').update(readFileSync(dockerfile)).digest('hex');
}
return createHash('sha256').update(hash).digest('hex');
}
El fingerprint se compara con el almacenado en el estado previo. Si son iguales, el servicio no se redespliega.
Esto permite:
- Cambiar opciones del proveedor sin reconstruir la imagen
- Detectar cambios en variables de entorno, puertos, health checks, etc.
- Detectar cambios en el Dockerfile