From f70ea4a72f3cb4920553daad9a4c1c05953c26e9 Mon Sep 17 00:00:00 2001 From: Andrii Melashchenko Date: Sun, 26 Jul 2026 22:15:48 +0200 Subject: [PATCH] =?UTF-8?q?feat(compose):=20implement=20SUP-01=20Compose?= =?UTF-8?q?=20OCI=20=E2=80=94=20The=20Margo=20Way?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement Specification Update Proposal SUP-01 which defines the normative Compose Archive packaging model for Margo, following the same OCI registry pattern established for Helm components. Schema changes (application-description.linkml.yaml): - Make repository and revision required for all component types - Add SemVer 2.0 pattern constraint on revision field - Remove packageLocation and keyLocation fields (never released) Schema changes (desired-state.linkml.yaml): - Add optional revision attribute to Component class with SemVer pattern Documentation (application-registry.md): - Add Compose-specific OCI media types to Margo-Specific Media Types table (application/vnd.org.margo.component.compose+json and application/vnd.org.margo.component.compose.tar+gzip) - Add normative Compose Archive Structure section (directory layout, security constraints, integrity verification) - Add publishing workflow guidance (oras push recommended) - Add wait semantics for Compose components Documentation (index.md.jinja2): - Update ComponentProperties compose table to document repository/revision - Remove Investigation Needed block for compose properties Examples: - Update all compose examples to use repository/revision instead of packageLocation/keyLocation Resolves: margo/specification#168, margo/specification#166, margo/specification#179 SUP: specification-enhancements/proposals/compose-oci/sup-01-compose-oci.md Breaking-Change: yes (pre-draft, no backward compat required) Signed-off-by: Andrii Melashchenko --- .../application-description.linkml.yaml | 26 +++--- .../invalid/ApplicationDescription-002.yaml | 4 +- .../valid/ApplicationDescription-002.yaml | 4 +- .../applications/resources/index.md.jinja2 | 10 +-- .../desired-state.linkml.yaml | 13 ++- .../examples/valid/DesiredState-002.yaml | 4 +- .../examples/valid/gateway-autonomous.yaml | 4 +- .../examples/valid/gateway-directed.yaml | 4 +- .../applications/application-registry.md | 79 ++++++++++++++++++- 9 files changed, 118 insertions(+), 30 deletions(-) diff --git a/src/specification/applications/application-description.linkml.yaml b/src/specification/applications/application-description.linkml.yaml index 4fe7a0c8..4ca7c264 100644 --- a/src/specification/applications/application-description.linkml.yaml +++ b/src/specification/applications/application-description.linkml.yaml @@ -394,13 +394,25 @@ classes: rank: 80 attributes: repository: - description: Repository location for the component. + description: >- + OCI repository URI for the component (e.g., oci://registry.example.com/org/component-name). + MUST be used for Helm, Compose, and Quadlet components. rank: 10 range: string + required: true revision: - description: Revision version for the component. - rank: 20 + description: >- + OCI tag identifying the component version within the repository + (e.g., "1.0.0", "2.3.1", "1.0.0-rc1", "1.0.0_build.123"). + MUST be a valid SemVer 2.0 string without a leading `v`. + SemVer build metadata separator `+` MUST be stored as `_` + because `+` is not a valid OCI tag character. + Implementations comparing this value against a SemVer string + MUST convert `_` back to `+` before comparison. range: string + required: true + rank: 20 + pattern: "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:_([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" wait: description: If True, indicates the device waits for the component installation to complete. rank: 30 @@ -409,14 +421,6 @@ classes: description: Time to wait for component installation to complete, formatted as "##m##s". rank: 40 range: string - packageLocation: - description: The URL indicating the Compose package's location. It should be a direct path to the compose.yaml or compose.yaml file archived in tar.gz - rank: 50 - range: string - keyLocation: - description: URL for the public key used to validate a digitally signed package. - rank: 60 - range: string Parameter: description: >- diff --git a/src/specification/applications/resources/examples/invalid/ApplicationDescription-002.yaml b/src/specification/applications/resources/examples/invalid/ApplicationDescription-002.yaml index 8dbbf496..747d9c54 100644 --- a/src/specification/applications/resources/examples/invalid/ApplicationDescription-002.yaml +++ b/src/specification/applications/resources/examples/invalid/ApplicationDescription-002.yaml @@ -59,8 +59,8 @@ deploymentProfiles: components: - name: digitron-orchestrator-docker properties: - packageLocation: https://northsitarida.com/digitron/docker/digitron-orchestrator.tar.gz - keyLocation: https://northsitarida.com/digitron/docker/public-key.asc + repository: oci://northstarida.azurecr.io/compose/digitron-orchestrator + revision: 1.0.9 parameters: idpName: targets: diff --git a/src/specification/applications/resources/examples/valid/ApplicationDescription-002.yaml b/src/specification/applications/resources/examples/valid/ApplicationDescription-002.yaml index c80a52ac..88f537f1 100644 --- a/src/specification/applications/resources/examples/valid/ApplicationDescription-002.yaml +++ b/src/specification/applications/resources/examples/valid/ApplicationDescription-002.yaml @@ -56,8 +56,8 @@ deploymentProfiles: components: - name: digitron-orchestrator-docker properties: - packageLocation: https://northsitarida.com/digitron/docker/digitron-orchestrator.tar.gz - keyLocation: https://northsitarida.com/digitron/docker/public-key.asc + repository: oci://northstarida.azurecr.io/compose/digitron-orchestrator + revision: 1.0.9 parameters: idpName: value: "test" diff --git a/src/specification/applications/resources/index.md.jinja2 b/src/specification/applications/resources/index.md.jinja2 index 7eb100a3..67892c4e 100644 --- a/src/specification/applications/resources/index.md.jinja2 +++ b/src/specification/applications/resources/index.md.jinja2 @@ -111,14 +111,12 @@ The expected properties for the suppported deployment types are indicated below. - Properties for `compose` components -> **Investigation Needed**: We need to have more discussion about how Compose should be handled and what is required here. - | Attribute | Type | Required? | Description | | --- | --- | --- | --- | -| packageLocation | string | Y | The URL indicating the Compose package's location. It should be a direct path to the compose.yaml or compose file archived in tar.gz | -| keyLocation | string | N | The public key used to validated the digitally signed package. It is highly recommend to digitally sign the package. When signing the package PGP MUST be used.| -| wait | bool | N | If `True`, indicates the device MUST wait until the Compose file has finished starting up before starting the next Compose file. The default is `True`. The Workload Fleet Management Client MUST support `True` and MAY support `False`. Only applies if multiple `compose` components are provided.| -| timeout | string | N | The time to wait for the component's installation to complete. If the installation does not completed before the timeout occurs the installation process fails. The format is "##m##s" indicating the total number of minutes and seconds to wait.| +| repository | string | Y | OCI repository URI for the Compose Archive (e.g., `oci://registry.example.com/org/component-name`). MUST be used for Compose components. See [Compose Component Registry](application-registry.md#compose-component-registry) for details.| +| revision | string | Y | OCI tag identifying the component version. MUST be a valid SemVer 2.0 string without a leading `v`. Build metadata separator `+` MUST be stored as `_` because `+` is not a valid OCI tag character.| +| wait | bool | N | If `True`, indicates the device MUST wait until all containers in the Compose project reach running state before reporting success. The default is `True`. The Workload Fleet Management Client MUST support `True` and MAY support `False`. Only applies if multiple `compose` components are provided.| +| timeout | string | N | The time to wait for the component's installation to complete. If the installation does not complete before the timeout occurs the installation process fails. The format is "##m##s" indicating the total number of minutes and seconds to wait.| ## Defining configurable application parameters diff --git a/src/specification/margo-management-interface/desired-state.linkml.yaml b/src/specification/margo-management-interface/desired-state.linkml.yaml index dd9ed4cb..c75309af 100644 --- a/src/specification/margo-management-interface/desired-state.linkml.yaml +++ b/src/specification/margo-management-interface/desired-state.linkml.yaml @@ -142,13 +142,22 @@ classes: Component: description: A class representing a component of a deployment profile. rank: 50 - # abstract: true attributes: name: description: The name of the component. - # identifier: true required: true rank: 10 + revision: + description: >- + OCI tag identifying the component version within the repository + (e.g., "1.0.0", "2.3.1", "1.0.0-rc1", "1.0.0_build.123"). + MUST be a valid SemVer 2.0 string without a leading `v`. + SemVer build metadata separator `+` MUST be stored as `_` + because `+` is not a valid OCI tag character. + range: string + required: false + rank: 15 + pattern: "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:_([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" properties: description: Properties associated with the component. range: Property diff --git a/src/specification/margo-management-interface/resources/examples/valid/DesiredState-002.yaml b/src/specification/margo-management-interface/resources/examples/valid/DesiredState-002.yaml index 5e555e0d..7cacc942 100644 --- a/src/specification/margo-management-interface/resources/examples/valid/DesiredState-002.yaml +++ b/src/specification/margo-management-interface/resources/examples/valid/DesiredState-002.yaml @@ -12,8 +12,8 @@ spec: components: - name: digitron-orchestrator-docker properties: - keyLocation: https://northsitarida.com/digitron/docker/public-key.asc - packageLocation: https://northsitarida.com/digitron/docker/digitron-orchestrator.tar.gz + repository: oci://northstarida.azurecr.io/compose/digitron-orchestrator + revision: 1.0.9 parameters: adminName: value: Some One diff --git a/src/specification/margo-management-interface/resources/examples/valid/gateway-autonomous.yaml b/src/specification/margo-management-interface/resources/examples/valid/gateway-autonomous.yaml index d15d4fd8..ced3f046 100644 --- a/src/specification/margo-management-interface/resources/examples/valid/gateway-autonomous.yaml +++ b/src/specification/margo-management-interface/resources/examples/valid/gateway-autonomous.yaml @@ -12,8 +12,8 @@ spec: components: - name: digitron-orchestrator-docker properties: - keyLocation: https://northsitarida.com/digitron/docker/public-key.asc - packageLocation: https://northsitarida.com/digitron/docker/digitron-orchestrator.tar.gz + repository: oci://northstarida.azurecr.io/compose/digitron-orchestrator + revision: 1.0.9 parameters: adminName: value: Some One diff --git a/src/specification/margo-management-interface/resources/examples/valid/gateway-directed.yaml b/src/specification/margo-management-interface/resources/examples/valid/gateway-directed.yaml index e15c54cd..445027b8 100644 --- a/src/specification/margo-management-interface/resources/examples/valid/gateway-directed.yaml +++ b/src/specification/margo-management-interface/resources/examples/valid/gateway-directed.yaml @@ -12,8 +12,8 @@ spec: components: - name: digitron-orchestrator-docker properties: - keyLocation: https://northsitarida.com/digitron/docker/public-key.asc - packageLocation: https://northsitarida.com/digitron/docker/digitron-orchestrator.tar.gz + repository: oci://northstarida.azurecr.io/compose/digitron-orchestrator + revision: 1.0.9 parameters: adminName: value: Some One diff --git a/system-design/specification/applications/application-registry.md b/system-design/specification/applications/application-registry.md index 3c2c0e7c..5763dd55 100644 --- a/system-design/specification/applications/application-registry.md +++ b/system-design/specification/applications/application-registry.md @@ -229,6 +229,8 @@ The following response example is a Margo-specific OCI image manifest following |``application/vnd.margo.app.descriptionFile.v1+{file format}``| MUST be used to mark a layer in the OCI image manifest as pointing to description file of a Margo Application Package | |``application/vnd.margo.app.licenseFile.v1+{file format}``| MUST be used to mark a layer in the OCI image manifest as pointing to the license file of a Margo Application Package| |``application/vnd.margo.app.releaseNotes.v1+{file format}``| MUST be used to mark a layer in the OCI image manifest as pointing to the release notes file of a Margo Application Package| +|``application/vnd.org.margo.component.compose+json``| MUST be used as the **artifactType** in the OCI image manifest for a Margo Compose Archive | +|``application/vnd.org.margo.component.compose.tar+gzip``| MUST be used as the layer blob **mediaType** for a Margo Compose Archive | #### Margo-Specific Annotation Keys @@ -243,4 +245,79 @@ The following response example is a Margo-specific OCI image manifest following Retrieving the different files that compose an Application Package MUST be implemented according to the ["Pulling blobs" section of the OCI_spec](https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-blobs). -Also for this purpose available tools and libraries can be used for an implementation with low complexity. \ No newline at end of file +Also for this purpose available tools and libraries can be used for an implementation with low complexity. + +## Compose Component Registry + +Compose components MUST be stored in an OCI-compliant Component Registry and referenced via `repository` (an `oci://` URI) and `revision` (an OCI tag matching SemVer 2.0) in the ApplicationDescription and Desired State manifests. + +The OCI image manifest for a Compose component MUST use `artifactType` = `application/vnd.org.margo.component.compose+json`. The single layer blob MUST use `mediaType` = `application/vnd.org.margo.component.compose.tar+gzip`. + +### Compose Archive Structure + +A Compose Archive is a gzip-compressed tar archive (`.tar.gz` or `.tgz`) that packages a Compose application for deployment on edge devices. The archive MUST conform to the following structural requirements. + +#### Directory Layout + +The archive MUST contain exactly one top-level directory. + +The directory name SHOULD match the component `name` as specified in the ApplicationDescription for human readability, but implementations MUST NOT depend on the directory name for discovery. + +Discovery algorithm: enter the single top-level directory; locate the file named `compose.yaml`. + +The top-level directory MAY contain any number of subdirectories (e.g., `configs/`, `certs/`, `scripts/`). All referenced files MUST resolve within the top-level directory. + +The top-level directory MUST contain a file named `compose.yaml`. The Compose file MUST conform to the Compose Specification as currently published. + +> **Note:** The Compose file MUST be named `compose.yaml`. The alternative names `compose.yml`, `docker-compose.yaml`, and `docker-compose.yml` are NOT valid within a Margo Compose Archive. + +Files referenced by `compose.yaml` via `env_file` entries and `configs` (file source) MUST be included within the archive and MUST be referenced using relative paths that resolve within the top-level directory. + +Bind-mount volume paths declared in `volumes` are runtime paths and MUST NOT be included in the archive. + +Files for `secrets` (file source) MUST NOT be included in the archive. Secret provisioning is out of scope and is the responsibility of the device or WFM implementation at deployment time. + +#### Security Constraints + +- Symlinks MUST NOT target paths outside the top-level directory. +- Hard links MUST NOT reference paths outside the top-level directory. +- Absolute paths MUST NOT appear in the archive entries. +- File names MUST NOT contain path traversal sequences (`../`). +- Implementations SHOULD normalize file permissions during archive extraction. Implementations MUST NOT preserve setuid, setgid, or sticky bits from archive entries. +- WFM and device implementations MUST validate these constraints before extracting or deploying the archive. + +#### Integrity Verification + +When stored in an OCI-compliant Component Registry, the Compose Archive tarball is the content of a single layer blob. Integrity verification at the transport layer is provided by the OCI content-addressable digest as mandated by the [OCI Distribution Specification v1.1.0](https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md). Implementations MUST verify the OCI digest after pulling the blob and before extracting the archive. + +### Publishing Workflow + +To publish a Compose Archive to an OCI-compliant Component Registry, use `oras push` ([ORAS — OCI Registry as Storage](https://oras.land/)). This is the RECOMMENDED publishing tool for Margo Compose Archives. + +> **Warning**: `docker compose publish` (Docker Compose 2.34.0+) MUST NOT be used to publish Margo Compose components. It produces a structurally incompatible OCI artifact: `artifactType: application/vnd.docker.compose.project`, multiple layers (one per file), and SHA256-hashed file paths. This format cannot be consumed by a Margo-compliant WFM or device implementation. + +Example: + +```bash +oras push registry.example.com/org/myapp:1.0.0 \ + --artifact-type application/vnd.org.margo.component.compose+json \ + myapp-1.0.0-compose.tar.gz:application/vnd.org.margo.component.compose.tar+gzip +``` + +Reference the artifact in the ApplicationDescription: + +```yaml +components: + - name: myapp + properties: + repository: oci://registry.example.com/org/myapp + revision: "1.0.0" +``` + +### WFM Reconciliation and `wait` Semantics for Compose + +If `wait` is set to `true` for a Compose component, the device MUST wait until all containers in the Compose project reach **running** state before reporting the deployment as successful. This is equivalent to `docker compose up` or `podman-compose up` completing synchronously without `--detach`. + +If any container exits with a non-zero exit code during startup, the deployment MUST be reported as failed immediately. + +If health checks are defined in `compose.yaml`, implementations SHOULD additionally wait for all containers to reach **healthy** state before reporting success. \ No newline at end of file