quorate/docs/spec/mirrored-image-spec.md

85 lines
4.4 KiB
Markdown
Raw Normal View History

# MirroredImage Specification
## Boundary
| Concern | Requirement |
| --------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Controller | A dedicated controller Deployment and service account watch `MirroredImage`; the Project Delivery controller does not. |
| Isolation | Mirrored Image and Project Delivery controller pods remain separate because their permissions and owned resources differ. |
| Reuse | Generic Go libraries and the F# runner may be shared; controller state machines remain separate. |
| Intent | One `MirroredImage` owns one upstream repository. |
| Destination | The corresponding Quay repositories are `candidates/<metadata.name>` and `mirror/<metadata.name>`. |
| Credentials | Two Zitadel-backed Quay personas isolate OAuth authority by organization; four assigned tokens isolate it by privilege. |
| New work | The `MirroredImage` API, controller state machine, Argo Workflows, and tests are independent work. |
| Excluded input | Smeltery specifications, implementation, workflows, and tests are not inputs. |
| Workload policy | Workload image-reference policy is outside the controller; Kyverno governs workload declarations. |
## Declaration
```yaml
kind: MirroredImage
metadata:
name: ingress-nginx
spec:
upstream: registry.k8s.io/ingress-nginx/controller
policy:
rejectAtOrAbove: High
digests:
- digest: sha256:...
tag: v1.13.0
- digest: sha256:...
```
## Quay authorization
OAuth application scopes are privilege boundaries, not organization boundaries. Each token acts as its assigned persona, and each persona has Quay authority only within its corresponding organization.
| Persona | Quay resource boundary | Token | OAuth scope |
| ---------------- | ---------------------- | ------------------- | --------------------------- |
| `mirror-bot` | `mirror` only | `mirror-reader` | `repo:read` |
| `mirror-bot` | `mirror` only | `mirror-writer` | `repo:create`, `repo:write` |
| `candidates-bot` | `candidates` only | `candidates-reader` | `repo:read` |
| `candidates-bot` | `candidates` only | `candidates-writer` | `repo:create`, `repo:write` |
The reader and writer tokens are distinct credentials despite being assigned to the same persona. Neither persona has Quay authority in `quorate` or in the other mirror organization.
Quay treats `repo:write` as including `repo:read`; each writer token therefore has create, read, and write authority inside its persona's organization.
| Runtime | Quay credentials |
| ---------------- | ------------------------------------ |
| Controller | `candidates-reader`, `mirror-reader` |
| Prepare Workflow | `candidates-writer` |
| Promote Workflow | `candidates-reader`, `mirror-writer` |
| Candidate GC | `candidates-writer` |
| Mirror GC | `mirror-writer` |
## Models
- [MirroredImage.tla](mirrored-image/MirroredImage.tla)
- [RetentionTags.tla](mirrored-image/RetentionTags.tla)
- [TagExpiry.tla](mirrored-image/TagExpiry.tla)
## Workflow interface
| Argo Workflow | Stages |
| ------------- | ----------------------- |
| Prepare | Prepare |
| Promotion | Promote |
| Cleanup | Candidate GC, Mirror GC |
| Field | Type |
| -------------------------- | ----------------------- |
| `retryOf` | Workflow-name reference |
| `mirror.quorate.dev/retry` | Workflow-name reference |
| `status.lastHandledRetry` | Workflow-name reference |
## Scan inputs
| Image shape | `ClairInputs` |
| --------------- | ------------------ |
| Single manifest | Candidate manifest |
| OCI index | Child manifests |
```math
\mathrm{Safe} \iff \forall m \in \mathrm{ClairInputs}:\ \mathrm{ReportAvailable}(m) \land \forall v \in \mathrm{Findings}(m):\ \mathrm{severity}(v) < \mathrm{spec.policy.rejectAtOrAbove}
```