
# Aplicar un plan de configuración

`POST /v1/config-plans/{plan_id}:apply` · permiso `config:write`, los permisos de lectura del plan y el permiso de escritura de cada tipo que cambia

Aplica un [plan](create.md) almacenado al entorno. Cada cambio se escribe en una sola transacción: si uno falla, no se conserva ninguno. Un plan puede aplicarse una sola vez.

## Solicitud

| Parámetro | Descripción |
|---|---|
| `plan_id` | El id del plan, `cfgp_…`. |

El cuerpo es opcional.

| Campo | Descripción |
|---|---|
| `resolved_secrets` | Objeto de `secret_ref` nombre → valor para cada cambio con `requires_secret: true` (por ejemplo `KEENAUTH_CONNECTION_GITHUB_CLIENT_SECRET`). Los valores tienen como máximo 8 KiB. El plan nunca los almacena, registra ni devuelve. |
| `confirm_deletions` | Lista de `<kind>/<key>` eliminaciones que confirmas, por ejemplo `connection/legacy`. Las advertencias `deletion_requires_confirmation` del plan indican las eliminaciones que lo requieren. |
| `sp_host` | El host del proveedor de servicios del entorno, por ejemplo `login.example.com`. Solo es obligatorio cuando el plan crea el material OIDC de una conexión empresarial (la callback `https://<sp_host>/sso/<connection id>/callback` se fija a partir de él); una solicitud que no lo necesite no debe enviarlo. |

- Los permisos de escritura son `environment-settings:write`, `identity-settings:write`, `attack-protection:write`, `branding:write` (para `branding` y `application_branding`), `connections:write`, `apis:write`, `roles:write` y `applications:write`, solo para los tipos con un cambio distinto de `no_op`.
- Antes de escribir, el destino se vuelve a leer. Si cambió la versión de algún recurso planificado o el contenido de los tipos del plan después de crearlo, no se escribe nada, el plan pasa a `conflicted` y la solicitud falla con `409 config_conflict`. Cree un plan nuevo. Con `prune`, también se comparan los tipos que hacen referencia a los recursos planificados, y las referencias entre recursos se vuelven a comprobar; una referencia rota también hace que el plan pase a `conflicted`.
- Eliminar una conexión, o una aplicación que recibió un token en los últimos 30 días, debe repetirse en `confirm_deletions`. De lo contrario, la solicitud falla con `409 deletion_not_confirmed` y el plan permanece `planned`.
- El material OIDC de una conexión empresarial (`spec.oidc`) y su secreto de cliente se escriben en el servicio de federación después del registro de la conexión. Si esa segunda escritura se rechaza, el plan permanece `planned`, el registro de la conexión puede existir ya, y un plan nuevo escribe solo el material que falta. Un `spec.oidc` de `null` deja el material sin cambios. El secreto de cliente de una conexión social todavía no puede escribirse: un plan con un cambio `requires_secret` de ese tipo devuelve `501 not_implemented` después de comprobar los secretos. No se escribe nada.
- `Idempotency-Key` es compatible. Envíelo sin secretos en el cuerpo siempre que sea posible: el registro de la clave guarda un hash del cuerpo.

## Respuesta

`200 OK` con `Cache-Control: no-store`: el plan en el formato de [Obtener un plan de configuración](get.md) con `status: applied` y `applied_at`.

La configuración de los recursos afectados antes del cambio se conserva como una revisión (las 50 más recientes por entorno, como máximo 90 días). La aplicación escribe una entrada `config.applied` en el registro de gestión del entorno con los recuentos de recursos creados, actualizados, eliminados y sin cambios; cada recurso modificado también escribe su propia entrada.

## Errores

| HTTP | código | Cuándo |
|---|---|---|
| 400 | `malformed_request`, `unknown_field` | El cuerpo no es JSON o tiene un campo distinto de `resolved_secrets`, `confirm_deletions` y `sp_host`. |
| 401 | `unauthenticated` | La credencial falta, no es válida o fue revocada. |
| 403 | `insufficient_permissions` | La credencial carece de un permiso necesario; `required_permissions` los enumera todos. |
| 404 | `not_found` | El plan no existe, pertenece a otro entorno o fue eliminado. |
| 409 | `config_conflict` | El destino cambió después de crear el plan; el plan ahora está `conflicted`. |
| 409 | `config_plan_not_applicable` | El plan ya se aplicó o está `conflicted`, o otra solicitud lo está aplicando ahora (`plan_status: applying`); `plan_status` indica su estado. |
| 409 | `deletion_not_confirmed` | Falta una eliminación protegida en `confirm_deletions`; `unconfirmed_deletions` las enumera. |
| 409 | `conflict` | Otro recurso ya usa un valor único de un cambio (`errors[].pointer` es `/changes/<i>/spec/<field>`, código `duplicate`); el plan permanece `planned`. |
| 409 | `connection_in_use` | La conexión que se va a eliminar todavía está en uso; el plan permanece `planned`. |
| 409 | `connection_test_required` | Un cambio crea una conexión `enterprise` `active` o activa una sin un inicio de sesión de prueba exitoso (`errors[].pointer` es `/changes/<i>/spec`, código `test_required`); el plan permanece `planned`. |
| 410 | `config_plan_expired` | Ha pasado la hora del plan; cree un plan nuevo. |
| 413 | `config_document_too_large` | Los cambios, o la revisión conservada para ellos (como máximo 10 MiB), son demasiado grandes para aplicarse en una sola solicitud; planifique menos tipos. |
| 422 | `validation_failed` | Vea más abajo. |
| 422 | `policy_conflict` | El cambio entra en conflicto con una política del entorno; el plan permanece `planned`. |
| 429 | `rate_limited` | Se superó el límite de frecuencia `config_plan` (compartido con la creación de planes), o se están calculando o aplicando demasiados planes para este entorno a la vez; el plan permanece `planned`; reintente después del periodo `Retry-After`. |
| 501 | `not_implemented` | El plan necesita el secreto de cliente de una conexión social, que todavía no puede escribirse. |
| 503 | `service_unavailable` | Una dependencia (el directorio o, para el material OIDC empresarial, el servicio de federación) no está disponible temporalmente, o la capacidad de planes está temporalmente llena; no se escribió nada o, en el caso del servicio de federación, solo el registro de la conexión. Reintente después del periodo `Retry-After`. |

| `errors[].pointer` | `errors[].code` | Significado |
|---|---|---|
| `/resolved_secrets/<NAME>` | `secret_missing` | El plan necesita este secreto. |
| `/resolved_secrets/<NAME>` | `secret_unused` | El plan no necesita este secreto. |
| `/resolved_secrets/<NAME>` | `secret_invalid` | El valor está vacío, supera 8 KiB o no es texto. |
| `/resolved_secrets/<NAME>` | `invalid` | El nombre no tiene la forma `KEENAUTH_…` (`A-Z`, `0-9`, `_`). |
| `/sp_host` | `sp_host_required`, `sp_host_unused`, `invalid` | El plan crea material OIDC y no se indicó ningún host, no se necesita host, o el host no es un nombre DNS en minúsculas. |
| `/changes/<i>/spec/oidc/...` | como en la API OIDC de conexiones | El servicio de federación rechazó el material en esta posición del `changes` del plan. |
| `/confirm_deletions/<i>` | `duplicate`, `not_a_deletion` | El elemento está repetido o no es una eliminación en el plan. |
| `/confirm_deletions/<i>` | `invalid` | El elemento no tiene la forma `<kind>/<key>`. |
| `/resolved_secrets`, `/confirm_deletions` | `too_many` | Más de 5.000 entradas. |
| `/changes/<i>/spec/...` | como en la API propia del recurso | El entorno rechazó el cambio en esta posición del `changes` del plan. |

## Ejemplo

```bash
curl -X POST "https://api.eu.keenauth.com/v1/config-plans/cfgp_01m4qpqtywfs7930tb6kqn5rc5:apply" \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"confirm_deletions": ["connection/legacy"]}'

keenauth config apply cfgp_01m4qpqtywfs7930tb6kqn5rc5 --confirm-deletion connection/legacy
```

`keenauth config apply --secrets-from-env` lee cada secreto que necesita el plan de la variable de entorno del mismo nombre y nunca lo imprime.
