feat(bundle): bundle hygiene v2 — manifest spec + signing verifier + docs
Ships marketplace-design-v2 scoped to bundle hygiene (NO consumer UI, NO install dialog, NO marketplace). 2-3 day cap respected — Approach C narrow slice. Anti-feature commitment: scope creep → new design doc + /office-hours. ## What ships ### New module ordinis-bundle-spec - BundleManifest.java — canonical apiVersion: ordinis.io/v1 record (Metadata + DictionaryEntry, Jackson-bindable, forward-compat ignoreUnknown) - ManifestParser.java — YAML → record + structural validate (apiVersion/kind enforce, semver regex, bundle-id regex, path traversal guard на schemaRef, unique dict names, scope enum) - BundleSignatureVerifier.java — ed25519 sign/verify (JDK 21 native) - Zero Spring/JPA/Kafka deps — pure Java + Jackson YAML ### CUOD reference implementation - ordinis-cuod-bundle/manifest.yaml — 40 dicts canonical entries - Test fixture pin: cuod-manifest-fixture.yaml parses+validates clean (regression guard на каждом CI run) ### Tests - 12 cases ManifestParser (happy + reject paths) - 7 cases BundleSignatureVerifier (sign/verify roundtrip, tamper, wrong key) - 1 case CUOD fixture pin - Total 20 tests, all green ### Docs - docs/integration/bundle-format-spec.md — formal spec для Java handoff - docs/user-guide/bundle-authoring.md — how-to для новых bundle authors ## Why now (N=1 customer) Per office-hours v2 decision: marketplace UI build = N customers value scale, but bundle hygiene + sign format = AT N=1 ROI: - Java team handoff requires documented spec независимо - Sales demo «here's how customer #2 would ship custom bundle» - Optionality для marketplace UI когда customer #2 LOI surfaces ## Explicitly NOT shipped (Phase 2 scope if ever) - Publisher Maven plugin (separate artifact when actual publish needed) - Browse catalog UI / install dialog (no customer #2 with use case) - Dep resolution / topological install - Multi-tenant Nexus namespacing - Bundle uninstall flow
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
# Bundle Format Specification
|
||||
|
||||
**Status:** v1 (Stable contract — backward-compatible changes only)
|
||||
**Authority:** `ordinis-bundle-spec` module
|
||||
**Updated:** 2026-05-17
|
||||
|
||||
Formal spec для authoring и publish'инга справочников через Ordinis bundle artifacts. Этот документ — handoff артефакт: новые Java-инженеры могут shipпить bundles без чтения runtime loader кода.
|
||||
|
||||
---
|
||||
|
||||
## 1. Bundle layout
|
||||
|
||||
```
|
||||
my-bundle/
|
||||
├── manifest.yaml # REQUIRED — canonical bundle metadata
|
||||
├── src/main/resources/bundles/<id>/ # schema files (canonical layout per CUOD example)
|
||||
│ ├── spacecraft.schema.json
|
||||
│ ├── satellite_type.schema.json
|
||||
│ └── ...
|
||||
└── pom.xml # OPTIONAL — если bundle distributed via Maven artifact
|
||||
```
|
||||
|
||||
`manifest.yaml` — единственный required file. `schemaRef` paths могут быть произвольной структурой ниже bundle root (но не выходят выше через `..`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Manifest schema (apiVersion: ordinis.io/v1)
|
||||
|
||||
```yaml
|
||||
apiVersion: ordinis.io/v1 # REQUIRED, must equal "ordinis.io/v1"
|
||||
kind: Bundle # REQUIRED, must equal "Bundle"
|
||||
|
||||
metadata: # REQUIRED block
|
||||
id: <reverse-dns-bundle-id> # REQUIRED, regex ^[a-z][a-z0-9.-]{1,127}$
|
||||
name: <human-readable> # REQUIRED, non-blank
|
||||
version: <semver> # REQUIRED, regex X.Y.Z[-suffix]
|
||||
description: | # OPTIONAL, multi-line
|
||||
...
|
||||
domain: <tag> # OPTIONAL (e.g. "dzz", "finance")
|
||||
scope: PUBLIC|INTERNAL|RESTRICTED # OPTIONAL — default scope для dicts
|
||||
author: <org-or-team> # OPTIONAL
|
||||
license: <SPDX-or-proprietary> # OPTIONAL
|
||||
signature: ed25519:<base64> # ADDED BY PUBLISHER CLI (см. §4)
|
||||
|
||||
dictionaries: # REQUIRED, non-empty list
|
||||
- name: <snake_case> # REQUIRED, regex ^[a-z][a-z0-9_]{0,63}$, unique within bundle
|
||||
schemaVersion: <semver> # REQUIRED
|
||||
scope: PUBLIC|INTERNAL|RESTRICTED # REQUIRED
|
||||
schemaRef: <relative-path> # REQUIRED, bundle-relative, no ".."
|
||||
sampleDataRef: <relative-path> # OPTIONAL — seed records
|
||||
displayName: <human-readable> # OPTIONAL — default humanized name
|
||||
description: <text> # OPTIONAL
|
||||
checksums: # OPTIONAL — SHA-256 hex для tamper detection
|
||||
schemaSha256: <hex>
|
||||
sampleSha256: <hex>
|
||||
```
|
||||
|
||||
Unknown top-level и nested fields игнорируются (forward-compat).
|
||||
|
||||
---
|
||||
|
||||
## 3. Validation rules
|
||||
|
||||
`ManifestParser.validate()` enforces:
|
||||
|
||||
| Rule | Enforced |
|
||||
|---|---|
|
||||
| `apiVersion == "ordinis.io/v1"` | ✅ |
|
||||
| `kind == "Bundle"` | ✅ |
|
||||
| `metadata.id` matches reverse-DNS regex | ✅ |
|
||||
| `metadata.version` valid semver | ✅ |
|
||||
| `metadata.scope` ∈ {PUBLIC, INTERNAL, RESTRICTED} if present | ✅ |
|
||||
| `metadata.signature` starts с `ed25519:` if present | ✅ |
|
||||
| `dictionaries` non-empty | ✅ |
|
||||
| `dictionaries[].name` regex match | ✅ |
|
||||
| `dictionaries[].name` unique within bundle | ✅ |
|
||||
| `dictionaries[].schemaVersion` valid semver | ✅ |
|
||||
| `dictionaries[].scope` ∈ {PUBLIC, INTERNAL, RESTRICTED} | ✅ |
|
||||
| `dictionaries[].schemaRef` no `..`, no leading `/` | ✅ (path traversal guard) |
|
||||
|
||||
Не валидируется (downstream responsibility):
|
||||
- `schemaRef` файл существует
|
||||
- Referenced JSON Schema валидный draft-07
|
||||
- FK references (`x-references`) resolve cross-dict
|
||||
|
||||
---
|
||||
|
||||
## 4. Signing protocol
|
||||
|
||||
Bundle publisher CLI (см. §5 — future Maven plugin):
|
||||
|
||||
1. Build bundle archive: `tar -czf bundle.tar.gz manifest.yaml <schemaRef paths>`
|
||||
2. Sign archive bytes с ed25519 private key:
|
||||
```
|
||||
signature = ed25519:<base64(ed25519_sign(privateKey, sha256(archive)))>
|
||||
```
|
||||
Реально просто sign'ить archive bytes напрямую — ed25519 internally hashes.
|
||||
3. Write signature в `metadata.signature` ИЛИ в отдельный `bundle.sig` file.
|
||||
|
||||
Verification (`BundleSignatureVerifier.verify`):
|
||||
|
||||
```java
|
||||
byte[] archive = Files.readAllBytes(Path.of("bundle.tar.gz"));
|
||||
String signature = manifest.metadata().signature(); // или из bundle.sig
|
||||
byte[] publicKeyDer = Files.readAllBytes(Path.of("publisher.pub.der"));
|
||||
|
||||
boolean ok = BundleSignatureVerifier.verify(archive, signature, publicKeyDer);
|
||||
```
|
||||
|
||||
Public key format: X.509 SubjectPublicKeyInfo DER. Generation:
|
||||
```bash
|
||||
openssl genpkey -algorithm Ed25519 -out priv.pem
|
||||
openssl pkey -in priv.pem -pubout -outform DER -out pub.der
|
||||
openssl pkey -in priv.pem -pubout -outform PEM -out pub.pem # для distribution
|
||||
```
|
||||
|
||||
Ed25519 chosen для: deterministic signatures, fast verify (~50µs), small keys (32 bytes pub). JDK 15+ native поддержка.
|
||||
|
||||
---
|
||||
|
||||
## 5. Publisher tooling (future)
|
||||
|
||||
`ordinis-bundle-publisher` Maven plugin (отдельный artifact, NOT shipped в v2):
|
||||
|
||||
```bash
|
||||
mvn ordinis:bundle-publish \
|
||||
-Dbundle.path=ordinis-cuod-bundle \
|
||||
-Dnexus.url=https://nexus.corp/ordinis-bundles/private/cuod \
|
||||
-Dsigning.key=$BUNDLE_SIGNING_KEY
|
||||
```
|
||||
|
||||
Steps плагина (когда built):
|
||||
1. `ManifestParser.parseAndValidate(manifest.yaml)` — fail fast если errors
|
||||
2. Resolve schema refs → tar.gz archive
|
||||
3. `BundleSignatureVerifier.sign(archive, privateKey)` → signature
|
||||
4. Patch `metadata.signature` в archive's manifest
|
||||
5. Upload tar.gz + sidecar bundle.sig к Nexus
|
||||
|
||||
Скрипт можно написать через shell + openssl до того как plugin доступен.
|
||||
|
||||
---
|
||||
|
||||
## 6. Consumer protocol (future v3)
|
||||
|
||||
NOT shipped в v2. Reserved для multi-tenant marketplace scenario:
|
||||
- Download bundle from Nexus + verify signature
|
||||
- Parse manifest + extract schemas
|
||||
- Apply dictionaries to target ordinis instance через standard write API + draft workflow
|
||||
|
||||
Spec гарантирует что implementer этого consumer'а получит deterministic bundle behavior independent от ordinis runtime version.
|
||||
|
||||
---
|
||||
|
||||
## 7. Versioning policy
|
||||
|
||||
| Component | Versioning |
|
||||
|---|---|
|
||||
| `apiVersion` | bumped только на breaking spec change (e.g. `ordinis.io/v2`). Backward-compat additions stay на v1. |
|
||||
| `metadata.version` | semver. Patch = data fix, minor = schemaVersion bump одного dict, major = breaking schema change |
|
||||
| `dictionaries[].schemaVersion` | per-dict semver — independent от bundle version |
|
||||
|
||||
Breaking changes в schemas (`additionalProperties` add, required field remove, type change) требуют MAJOR bump на bundle level.
|
||||
|
||||
---
|
||||
|
||||
## 8. Reference implementation
|
||||
|
||||
`ordinis-cuod-bundle/manifest.yaml` — 40 dicts, signed via CI на publish. Working example для bundle authoring.
|
||||
|
||||
Parser test fixture: `ordinis-bundle-spec/src/test/resources/cuod-manifest-fixture.yaml` (snapshot — pin test catches regressions).
|
||||
|
||||
---
|
||||
|
||||
## 9. Anti-features (explicitly NOT in v1)
|
||||
|
||||
- Bundle dependency declaration (no `requires:` block)
|
||||
- Topological install order
|
||||
- Multi-tenant namespace isolation
|
||||
- Bundle uninstall / migration scripts
|
||||
- Public marketplace catalog
|
||||
|
||||
См. `docs-internal/design/dictionary-marketplace.md` — почему scoped down.
|
||||
@@ -0,0 +1,142 @@
|
||||
# Authoring a Bundle
|
||||
|
||||
How to package справочники как distributable Ordinis bundle.
|
||||
|
||||
См. также: [Bundle Format Specification](../integration/bundle-format-spec.md) — formal spec.
|
||||
|
||||
---
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# 1. Create bundle structure
|
||||
mkdir -p my-bundle/src/main/resources/bundles/my-id
|
||||
cd my-bundle
|
||||
|
||||
# 2. Add at least one schema
|
||||
cat > src/main/resources/bundles/my-id/widget.schema.json <<'JSON'
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"x-id-source": "code",
|
||||
"required": ["code", "name"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"code": { "type": "string", "pattern": "^[A-Z][A-Z0-9_]{1,31}$", "x-unique": true },
|
||||
"name": {
|
||||
"type": "object", "x-localized": true,
|
||||
"patternProperties": { "^[a-z]{2}-[A-Z]{2}$": { "type": "string" } },
|
||||
"required": ["ru-RU"]
|
||||
}
|
||||
}
|
||||
}
|
||||
JSON
|
||||
|
||||
# 3. Write manifest
|
||||
cat > manifest.yaml <<'YAML'
|
||||
apiVersion: ordinis.io/v1
|
||||
kind: Bundle
|
||||
metadata:
|
||||
id: my-org.widgets
|
||||
name: My Widgets Bundle
|
||||
version: 1.0.0
|
||||
scope: PUBLIC
|
||||
dictionaries:
|
||||
- name: widget
|
||||
schemaVersion: 1.0.0
|
||||
scope: PUBLIC
|
||||
schemaRef: src/main/resources/bundles/my-id/widget.schema.json
|
||||
YAML
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Versioning
|
||||
|
||||
- `metadata.version` — semver. Patch на data fixes, minor на add'инге dict'ов, major на breaking schema changes.
|
||||
- `dictionaries[].schemaVersion` — per-dict semver, independent от bundle version. Bump когда меняется shape конкретной schema.
|
||||
- Breaking schema change (remove required field, change type, add `additionalProperties: false`) → MAJOR bump bundle.
|
||||
|
||||
---
|
||||
|
||||
## Validation locally
|
||||
|
||||
```bash
|
||||
# Через maven (если bundle — Maven artifact):
|
||||
mvn test -pl ordinis-bundle-spec -Dtest=CuodManifestFixtureTest
|
||||
|
||||
# Или programmatically:
|
||||
import cloud.nstart.terravault.ordinis.bundle.spec.*;
|
||||
|
||||
byte[] yaml = Files.readAllBytes(Path.of("manifest.yaml"));
|
||||
List<String> errors = ManifestParser.parseAndValidate(yaml);
|
||||
if (!errors.isEmpty()) {
|
||||
errors.forEach(System.err::println);
|
||||
System.exit(1);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Signing (production publish)
|
||||
|
||||
Ed25519 keypair generation:
|
||||
```bash
|
||||
openssl genpkey -algorithm Ed25519 -out signing-priv.pem
|
||||
openssl pkey -in signing-priv.pem -pubout -outform DER -out signing-pub.der
|
||||
```
|
||||
|
||||
Sign bundle archive (until publisher Maven plugin lands — manual для now):
|
||||
```java
|
||||
byte[] archive = Files.readAllBytes(Path.of("bundle.tar.gz"));
|
||||
byte[] privKey = Files.readAllBytes(Path.of("signing-priv.der")); // convert PEM→DER first
|
||||
String sig = BundleSignatureVerifier.sign(archive, privKey);
|
||||
// → patch manifest.yaml: metadata.signature: <sig>
|
||||
// OR write sidecar bundle.sig: <sig>
|
||||
```
|
||||
|
||||
Public key распространяется к operators которые verify bundles перед install.
|
||||
|
||||
---
|
||||
|
||||
## Conventions
|
||||
|
||||
### Naming
|
||||
|
||||
- `metadata.id` — reverse-DNS (`ru.cuod.dzz-ground-segment`, `com.acme.widgets`).
|
||||
- `dictionaries[].name` — snake_case (`spacecraft`, `ground_station`, `frequency_band`).
|
||||
|
||||
### Locales
|
||||
|
||||
Localized fields (`x-localized: true`) — каждое значение это объект `{lang-COUNTRY: "..."}`:
|
||||
```json
|
||||
"name": { "ru-RU": "Космический аппарат", "en-US": "Spacecraft" }
|
||||
```
|
||||
|
||||
Bundle сам не объявляет supported locales — это per-dict. Manifest may add optional `metadata.locales` в future v1.x но spec строго ignores unknown сейчас.
|
||||
|
||||
### Foreign keys
|
||||
|
||||
`x-references: "<target_dict>.<field>"` — backend validates на write. Если target dict в том же bundle — order ensured (см. consumer protocol §6 в spec).
|
||||
|
||||
---
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- ❌ Не shipпите `additionalProperties: true` в production schemas — типизация теряется
|
||||
- ❌ Не используйте `$ref` к внешним URLs (security risk + offline build fail)
|
||||
- ❌ Не делайте 100+ dicts в одном bundle — расщепляйте по domain (e.g. `cuod-spacecraft`, `cuod-radio`)
|
||||
- ❌ Не bump'айте `schemaVersion` без реального schema change
|
||||
|
||||
---
|
||||
|
||||
## Handoff checklist (Java team)
|
||||
|
||||
Когда передаёте bundle authoring к новой команде:
|
||||
1. ✅ Прочитать [bundle-format-spec.md](../integration/bundle-format-spec.md)
|
||||
2. ✅ Изучить `ordinis-cuod-bundle/manifest.yaml` как working example
|
||||
3. ✅ Прогнать `mvn test -pl ordinis-bundle-spec` локально
|
||||
4. ✅ Generate signing keypair, distribute pub key к ops
|
||||
5. ✅ Set up CI publish job (когда `ordinis-bundle-publisher` plugin landed)
|
||||
|
||||
Для вопросов: ordinis maintainers (см. CODEOWNERS).
|
||||
Reference in New Issue
Block a user