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
76 changes: 76 additions & 0 deletions docs/LLMO-5566/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
ticket: LLMO-5566
repo: adobe/spacecat-api-service
branch: feature/LLMO-5566-cloudfront-log-delivery-assume-role
generated: 2026-06-26
revised: 2026-06-30
---

# Implementation Plan: CloudFront CDN Log Delivery (LLMO-5566)

> **Revised 2026-06-30.** `main` PR #2682 shipped the CloudFront onboarding wizard
> (`LlmoCloudFrontController`, `cdn-onboard/cloudfront/*`) while this branch was open, making this
> branch's parallel `edge-optimize/*` wizard redundant. This branch was reset onto `main` and
> rebuilt around the only net-new slice: **CloudWatch CDN log delivery**. The wizard endpoints and
> the org-scoped-externalId model from the original plan were dropped (main's per-session UUID
> externalId model wins).

## Problem Statement

SpaceCat's LLM Optimizer needs CDN access logs from customers' CloudFront distributions. There was
no automated way to configure cross-account CloudWatch Logs delivery from the customer's AWS account
into Adobe's `cdn-logs` S3 bucket. main's CloudFront onboarding wizard wires routing but does not
set up log delivery.

## Solution Overview

Add two endpoints to the existing `LlmoCloudFrontController`, reusing its connector-role flow:

1. `POST /sites/:siteId/llmo/cdn-onboard/cloudfront/log-delivery` — enable access-log forwarding for
a single distribution (idempotent).
2. `POST /sites/:siteId/llmo/cdn-onboard/cloudfront/log-rescan` — idempotently enable forwarding for
all distributions in the account (bounded concurrency), for recovery/re-scan.

The assume-role `externalId` is main's client-supplied per-session UUID
(`validateCloudfrontCredentials`). The delivery destination + source names are **org-scoped**,
derived server-side from the site's IMS org id (independent of the externalId). Both endpoints are
gated by `isLLMOAdministrator()` and registered in `INTERNAL_ROUTES` (not on the external FACS
surface).

## Key Files Changed vs main

| File | Change |
|------|--------|
| `src/controllers/llmo/llmo-cloudfront.js` | +2 methods: `enableCdnLogDelivery`, `rescanCdnLogDelivery` |
| `src/support/cdn-log-delivery.js` | CloudWatch Logs delivery-source + delivery creation (paginated, idempotent) |
| `src/routes/index.js` | +2 routes (`log-delivery`, `log-rescan`) |
| `src/routes/facs-capabilities.js`, `src/routes/required-capabilities.js` | register the 2 routes in INTERNAL_ROUTES |
| `docs/openapi/api.yaml`, `docs/openapi/llmo-api.yaml` | OpenAPI for the 2 endpoints |
| `package.json` | + `@aws-sdk/client-cloudwatch-logs` |
| `test/controllers/llmo/llmo-cloudfront.test.js` | unit tests for both endpoints |
| `test/support/cdn-log-delivery.test.js` | unit tests for the support module |

## Design Notes

- **No local `edge-optimize.js`** — the rescan lists distributions via tokowaka's
`CloudFrontEdgeClient.listDistributions()`, already used by sibling endpoints.
- **Bounded concurrency** — rescan runs `createCdnLogDelivery` in batches of
`CDN_LOG_RESCAN_CONCURRENCY` (5) to avoid CloudWatch Logs per-account throttling.
- **Error handling** — server misconfig (missing `CDN_LOG_DELIVERY_DEST_ACCOUNT_ID`) → 500;
actionable AWS errors surface as 4xx via `mutationErrorResponse` (consistent with sibling
endpoints); per-distribution failures report the AWS error *category* only (no raw messages/ARNs).

## Runtime Dependency (follow-up)

For log delivery to succeed, the connector role created by main's bootstrap CloudFormation template
(`customer-bootstrap-role.yaml`, S3-hosted) must grant `logs:PutDeliverySource`,
`logs:CreateDelivery`, `logs:GetDeliverySource`, `logs:DescribeDeliveries`. If main's template
predates log delivery, that template (and its `Metadata.AdobeLLMOptimizerPermissions` block) must be
updated — tracked separately from this code change.

## Acceptance Criteria

- Both endpoints reachable at the documented paths and gated by `isLLMOAdministrator()`
- Log delivery is idempotent (`{ alreadyExisted: true }` on repeat)
- Rescan respects the concurrency cap and never aborts on a single distribution failure
- Unit tests pass (`npm test`); OpenAPI validates (`npm run docs:lint`); route snapshot updated
4 changes: 4 additions & 0 deletions docs/openapi/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,10 @@ paths:
$ref: './llmo-api.yaml#/site-llmo-cloudfront-plan'
/sites/{siteId}/llmo/cdn-onboard/cloudfront/permissions:
$ref: './llmo-api.yaml#/site-llmo-cloudfront-permissions'
/sites/{siteId}/llmo/cdn-onboard/cloudfront/log-delivery:
$ref: './llmo-api.yaml#/site-llmo-cloudfront-log-delivery'
/sites/{siteId}/llmo/cdn-onboard/cloudfront/log-rescan:
$ref: './llmo-api.yaml#/site-llmo-cloudfront-log-rescan'
/sites/{siteId}/llmo/edge-optimize-status:
$ref: './llmo-api.yaml#/llmo-edge-optimize-status'
/sites/{siteId}/llmo/probes/edge-optimize:
Expand Down
101 changes: 101 additions & 0 deletions docs/openapi/llmo-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3451,6 +3451,107 @@ site-llmo-cloudfront-permissions:
security:
- api_key: [ ]

site-llmo-cloudfront-log-delivery:
post:
tags:
- llmo
summary: Enable CDN access-log forwarding for one CloudFront distribution
description: |
Enables CloudWatch Logs delivery of the selected CloudFront distribution's access logs to
Adobe's cross-account cdn-logs destination, via the customer's connector role. Idempotent —
returns `alreadyExisted: true` and mutates nothing when forwarding is already set up. The
assume-role `externalId` is the per-session value from bootstrap; the delivery destination is
org-scoped, resolved server-side from the site's IMS organization.
operationId: enableLlmoCloudFrontLogDelivery
parameters:
- $ref: './parameters.yaml#/siteId'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/cloudfront-distribution-request'
responses:
'200':
description: The CDN log-delivery result for the distribution.
content:
application/json:
schema:
type: object
properties:
created:
type: boolean
alreadyExisted:
type: boolean
deliverySourceName:
type: string
deliveryId:
type: string
'400':
$ref: './responses.yaml#/400'
'401':
$ref: './responses.yaml#/401'
'403':
$ref: './responses.yaml#/403'
'404':
$ref: './responses.yaml#/404'
'500':
$ref: './responses.yaml#/500'
security:
- api_key: [ ]

site-llmo-cloudfront-log-rescan:
post:
tags:
- llmo
summary: Re-scan and enable CDN log forwarding for all distributions
description: |
Idempotently enables CloudWatch Logs delivery for every CloudFront distribution in the
customer account. Intended for recovery/re-scan: e.g. after a new distribution is added or
a missed setup. Each distribution is attempted independently — one failure never aborts the
rest — and the response summarizes per-distribution outcomes.
operationId: rescanLlmoCloudFrontLogDelivery
parameters:
- $ref: './parameters.yaml#/siteId'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/cloudfront-connector-request'
responses:
'200':
description: Summary of the log-delivery re-scan across all distributions.
content:
application/json:
schema:
type: object
properties:
scanned:
type: integer
created:
type: integer
alreadyExisted:
type: integer
failed:
type: integer
distributions:
type: array
items:
type: object
'400':
$ref: './responses.yaml#/400'
'401':
$ref: './responses.yaml#/401'
'403':
$ref: './responses.yaml#/403'
'404':
$ref: './responses.yaml#/404'
'500':
$ref: './responses.yaml#/500'
security:
- api_key: [ ]

cloudfront-connector-request:
type: object
required:
Expand Down
Loading
Loading