From 8adfd39bff0e04017634f753c46ab9f7fc52f83d Mon Sep 17 00:00:00 2001 From: "Zimin A.N." Date: Sun, 17 May 2026 10:39:07 +0300 Subject: [PATCH] =?UTF-8?q?feat(bundle):=20bundle=20hygiene=20v2=20?= =?UTF-8?q?=E2=80=94=20manifest=20spec=20+=20signing=20verifier=20+=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/integration/bundle-format-spec.md | 182 ++++++++++++++++++ docs/user-guide/bundle-authoring.md | 142 ++++++++++++++ ordinis-bundle-spec/pom.xml | 48 +++++ .../ordinis/bundle/spec/BundleManifest.java | 85 ++++++++ .../bundle/spec/BundleSignatureVerifier.java | 92 +++++++++ .../ordinis/bundle/spec/ManifestParser.java | 144 ++++++++++++++ .../spec/BundleSignatureVerifierTest.java | 86 +++++++++ .../bundle/spec/CuodManifestFixtureTest.java | 33 ++++ .../bundle/spec/ManifestParserTest.java | 127 ++++++++++++ .../test/resources/cuod-manifest-fixture.yaml | 181 +++++++++++++++++ ordinis-cuod-bundle/manifest.yaml | 181 +++++++++++++++++ pom.xml | 1 + 12 files changed, 1302 insertions(+) create mode 100644 docs/integration/bundle-format-spec.md create mode 100644 docs/user-guide/bundle-authoring.md create mode 100644 ordinis-bundle-spec/pom.xml create mode 100644 ordinis-bundle-spec/src/main/java/cloud/nstart/terravault/ordinis/bundle/spec/BundleManifest.java create mode 100644 ordinis-bundle-spec/src/main/java/cloud/nstart/terravault/ordinis/bundle/spec/BundleSignatureVerifier.java create mode 100644 ordinis-bundle-spec/src/main/java/cloud/nstart/terravault/ordinis/bundle/spec/ManifestParser.java create mode 100644 ordinis-bundle-spec/src/test/java/cloud/nstart/terravault/ordinis/bundle/spec/BundleSignatureVerifierTest.java create mode 100644 ordinis-bundle-spec/src/test/java/cloud/nstart/terravault/ordinis/bundle/spec/CuodManifestFixtureTest.java create mode 100644 ordinis-bundle-spec/src/test/java/cloud/nstart/terravault/ordinis/bundle/spec/ManifestParserTest.java create mode 100644 ordinis-bundle-spec/src/test/resources/cuod-manifest-fixture.yaml create mode 100644 ordinis-cuod-bundle/manifest.yaml diff --git a/docs/integration/bundle-format-spec.md b/docs/integration/bundle-format-spec.md new file mode 100644 index 0000000..cb9bc79 --- /dev/null +++ b/docs/integration/bundle-format-spec.md @@ -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// # 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: # REQUIRED, regex ^[a-z][a-z0-9.-]{1,127}$ + name: # REQUIRED, non-blank + version: # REQUIRED, regex X.Y.Z[-suffix] + description: | # OPTIONAL, multi-line + ... + domain: # OPTIONAL (e.g. "dzz", "finance") + scope: PUBLIC|INTERNAL|RESTRICTED # OPTIONAL — default scope для dicts + author: # OPTIONAL + license: # OPTIONAL + signature: ed25519: # ADDED BY PUBLISHER CLI (см. §4) + +dictionaries: # REQUIRED, non-empty list + - name: # REQUIRED, regex ^[a-z][a-z0-9_]{0,63}$, unique within bundle + schemaVersion: # REQUIRED + scope: PUBLIC|INTERNAL|RESTRICTED # REQUIRED + schemaRef: # REQUIRED, bundle-relative, no ".." + sampleDataRef: # OPTIONAL — seed records + displayName: # OPTIONAL — default humanized name + description: # OPTIONAL + checksums: # OPTIONAL — SHA-256 hex для tamper detection + schemaSha256: + sampleSha256: +``` + +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 ` +2. Sign archive bytes с ed25519 private key: + ``` + signature = ed25519: + ``` + Реально просто 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. diff --git a/docs/user-guide/bundle-authoring.md b/docs/user-guide/bundle-authoring.md new file mode 100644 index 0000000..2269502 --- /dev/null +++ b/docs/user-guide/bundle-authoring.md @@ -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 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: +// OR write sidecar bundle.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: "."` — 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). diff --git a/ordinis-bundle-spec/pom.xml b/ordinis-bundle-spec/pom.xml new file mode 100644 index 0000000..f58483c --- /dev/null +++ b/ordinis-bundle-spec/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + + cloud.nstart.terravault.ordinis + ordinis-parent + 0.1.0-SNAPSHOT + + + ordinis-bundle-spec + Ordinis :: Bundle Spec + + Canonical bundle manifest format + ed25519 signature verifier. + Дополнительно: ManifestParser (YAML), ManifestValidator. + + Этот module — публичный contract для bundle authoring/handoff: + Java team может ship'ить новые bundles следуя этой spec без + full ordinis codebase знания. Также используется future publisher + Maven plugin (отдельный artifact) + future consumer marketplace UI. + + NO Spring / NO JPA / NO Kafka deps — pure Java + Jackson YAML. + + + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + + + diff --git a/ordinis-bundle-spec/src/main/java/cloud/nstart/terravault/ordinis/bundle/spec/BundleManifest.java b/ordinis-bundle-spec/src/main/java/cloud/nstart/terravault/ordinis/bundle/spec/BundleManifest.java new file mode 100644 index 0000000..ea824bd --- /dev/null +++ b/ordinis-bundle-spec/src/main/java/cloud/nstart/terravault/ordinis/bundle/spec/BundleManifest.java @@ -0,0 +1,85 @@ +package cloud.nstart.terravault.ordinis.bundle.spec; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; + +import java.util.List; + +/** + * Canonical bundle manifest format (apiVersion: ordinis.io/v1, kind: Bundle). + * + *

Этот record — Java mapping для {@code manifest.yaml} в корне любого + * bundle artifact (например {@code ordinis-cuod-bundle/manifest.yaml}). + * + *

Что это НЕ: не runtime loader для CUOD bundle. Существующий + * {@code CuodBundleImporter} продолжает работать с ad-hoc structure через + * {@code BundleManifest} в {@code ordinis-cuod-bundle} module. Этот spec — + * documentation artifact + future publisher CLI input + future consumer + * marketplace contract. + * + *

Зачем сейчас (N=1 customer): + *

    + *
  • Java team handoff — new engineers получают formal spec вместо + * reverse-engineering BundleImporter
  • + *
  • Sales demo — «here's how your team would ship custom bundle» + * без shipping консьюмерского UI
  • + *
  • Optionality — когда customer #2 пробуждается, spec уже defined
  • + *
+ * + *

Anti-feature commitment: NO consumer-side install logic в этом module. + * NO marketplace UI. NO bundle dep resolution. Это bundle hygiene, не + * marketplace. См. {@code docs-internal/design/dictionary-marketplace.md}. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record BundleManifest( + @JsonProperty(value = "apiVersion", required = true) String apiVersion, + @JsonProperty(required = true) String kind, + @JsonProperty(required = true) Metadata metadata, + @JsonProperty(required = true) List dictionaries) { + + public static final String CURRENT_API_VERSION = "ordinis.io/v1"; + public static final String KIND = "Bundle"; + + @JsonIgnoreProperties(ignoreUnknown = true) + public record Metadata( + /** Reverse-DNS bundle identifier (e.g. {@code ru.cuod.dzz-ground-segment}). */ + @JsonProperty(required = true) String id, + /** Human-readable display name. */ + @JsonProperty(required = true) String name, + /** Semver version (e.g. {@code 1.0.0}). Follows Maven artifact semver. */ + @JsonProperty(required = true) String version, + /** Multi-line description. Optional. */ + String description, + /** Domain tag (e.g. {@code dzz}, {@code finance}). Optional. */ + String domain, + /** Default scope для dicts которые не указали свой ({@code PUBLIC}/{@code INTERNAL}/{@code RESTRICTED}). */ + String scope, + /** Author / org name. Optional. */ + String author, + /** License identifier (SPDX-ish: {@code MIT}, {@code proprietary}, etc.). Optional. */ + String license, + /** + * Ed25519 signature над bundle archive в формате {@code ed25519:}. + * Добавляется publisher CLI на publish step. Verifier expects + * {@code ed25519:} prefix. + */ + String signature) {} + + @JsonIgnoreProperties(ignoreUnknown = true) + public record DictionaryEntry( + @JsonProperty(required = true) String name, + @JsonProperty(required = true) String schemaVersion, + /** PUBLIC / INTERNAL / RESTRICTED. */ + @JsonProperty(required = true) String scope, + /** Relative path к JSON Schema file (от bundle root). */ + @JsonProperty(value = "schemaRef", required = true) String schemaRef, + /** Optional relative path к sample data (record seed values). */ + String sampleDataRef, + /** Optional displayName override. Default — humanized {@code name}. */ + String displayName, + /** Optional short description. */ + String description, + /** Optional pre-computed checksums (SHA-256 hex) for tamper detection. */ + JsonNode checksums) {} +} diff --git a/ordinis-bundle-spec/src/main/java/cloud/nstart/terravault/ordinis/bundle/spec/BundleSignatureVerifier.java b/ordinis-bundle-spec/src/main/java/cloud/nstart/terravault/ordinis/bundle/spec/BundleSignatureVerifier.java new file mode 100644 index 0000000..3dd94fd --- /dev/null +++ b/ordinis-bundle-spec/src/main/java/cloud/nstart/terravault/ordinis/bundle/spec/BundleSignatureVerifier.java @@ -0,0 +1,92 @@ +package cloud.nstart.terravault.ordinis.bundle.spec; + +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.Signature; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; + +/** + * Ed25519 signature verification для bundle archives. + * + *

Bundle publisher CLI (отдельный artifact) подписывает {@code bundle.tar.gz} + * приватным ed25519 key → publish'ит signature в формате {@code ed25519:} + * либо в {@link BundleManifest.Metadata#signature()}, либо в отдельный файл + * {@code bundle.sig} рядом. + * + *

Этот verifier ДОКУМЕНТИРУЕТ verification contract — НЕ consumed на v2 + * ordinis runtime (no install flow). Built как handoff artifact для future + * consumer marketplace + Java team reference. + * + *

Why ed25519 (не RSA): deterministic, fast verify (~50µs), small keys + * (32 bytes pub), no parameter choices to mess up. Standard для signed + * package distribution (например Sigstore, Alpine apk). + * + *

Java native поддержка ed25519 — JDK 15+. Этот проект на JDK 21, OK. + */ +public final class BundleSignatureVerifier { + + public static final String SIG_PREFIX = "ed25519:"; + + private BundleSignatureVerifier() {} + + /** + * Verify archive bytes против ed25519 signature. + * + * @param archiveBytes the raw bundle archive (e.g. tar.gz contents) + * @param signature full signature string "ed25519:" + * @param publicKeyDer ed25519 public key в X.509 SubjectPublicKeyInfo DER format + * @return true если signature valid, false на любую ошибку (invalid sig, bad key, mismatch) + */ + public static boolean verify(byte[] archiveBytes, String signature, byte[] publicKeyDer) { + if (archiveBytes == null || signature == null || publicKeyDer == null) return false; + if (!signature.startsWith(SIG_PREFIX)) return false; + + String base64Sig = signature.substring(SIG_PREFIX.length()).trim(); + byte[] sigBytes; + try { + sigBytes = Base64.getDecoder().decode(base64Sig); + } catch (IllegalArgumentException e) { + return false; + } + + PublicKey publicKey; + try { + KeyFactory kf = KeyFactory.getInstance("Ed25519"); + publicKey = kf.generatePublic(new X509EncodedKeySpec(publicKeyDer)); + } catch (Exception e) { + return false; + } + + try { + Signature verifier = Signature.getInstance("Ed25519"); + verifier.initVerify(publicKey); + verifier.update(archiveBytes); + return verifier.verify(sigBytes); + } catch (Exception e) { + return false; + } + } + + /** + * Sign helper — для publisher CLI usage. Берёт private key DER (PKCS#8) + + * archive bytes → возвращает full signature string {@code ed25519:}. + * + *

Returns null при любой ошибке (invalid key, IO). Caller проверяет. + */ + public static String sign(byte[] archiveBytes, byte[] privateKeyDer) { + if (archiveBytes == null || privateKeyDer == null) return null; + try { + KeyFactory kf = KeyFactory.getInstance("Ed25519"); + var privateKey = kf.generatePrivate( + new java.security.spec.PKCS8EncodedKeySpec(privateKeyDer)); + Signature signer = Signature.getInstance("Ed25519"); + signer.initSign(privateKey); + signer.update(archiveBytes); + byte[] sig = signer.sign(); + return SIG_PREFIX + Base64.getEncoder().encodeToString(sig); + } catch (Exception e) { + return null; + } + } +} diff --git a/ordinis-bundle-spec/src/main/java/cloud/nstart/terravault/ordinis/bundle/spec/ManifestParser.java b/ordinis-bundle-spec/src/main/java/cloud/nstart/terravault/ordinis/bundle/spec/ManifestParser.java new file mode 100644 index 0000000..0378ac6 --- /dev/null +++ b/ordinis-bundle-spec/src/main/java/cloud/nstart/terravault/ordinis/bundle/spec/ManifestParser.java @@ -0,0 +1,144 @@ +package cloud.nstart.terravault.ordinis.bundle.spec; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +/** + * Parses bundle {@code manifest.yaml} → {@link BundleManifest} с structural + * validation (apiVersion + kind + required keys). + * + *

Не валидирует schema файлы (это задача downstream loader) — только + * manifest shape. SemVer version regex enforced. + */ +public final class ManifestParser { + + private static final ObjectMapper YAML = new ObjectMapper(new YAMLFactory()); + private static final Pattern SEMVER = Pattern.compile("^\\d+\\.\\d+\\.\\d+(?:-[A-Za-z0-9.-]+)?$"); + private static final Pattern BUNDLE_ID = Pattern.compile("^[a-z][a-z0-9.-]{1,127}$"); + private static final Pattern DICT_NAME = Pattern.compile("^[a-z][a-z0-9_]{0,63}$"); + + private ManifestParser() {} + + /** Parse YAML bytes → manifest record. */ + public static BundleManifest parse(byte[] yamlBytes) throws ManifestException { + try { + return YAML.readValue(yamlBytes, BundleManifest.class); + } catch (IOException e) { + throw new ManifestException("Failed to parse manifest YAML: " + e.getMessage(), e); + } + } + + /** Parse from stream — caller closes. */ + public static BundleManifest parse(InputStream is) throws ManifestException { + try { + return YAML.readValue(is, BundleManifest.class); + } catch (IOException e) { + throw new ManifestException("Failed to parse manifest stream: " + e.getMessage(), e); + } + } + + /** Parse + structural validate в одну операцию. Returns list of all issues. */ + public static List parseAndValidate(byte[] yamlBytes) { + BundleManifest m; + try { + m = parse(yamlBytes); + } catch (ManifestException e) { + return List.of(e.getMessage()); + } + return validate(m); + } + + /** Structural validate — без I/O или schema parse. */ + public static List validate(BundleManifest m) { + List errors = new ArrayList<>(); + if (m == null) { + errors.add("manifest is null"); + return errors; + } + if (!BundleManifest.CURRENT_API_VERSION.equals(m.apiVersion())) { + errors.add("apiVersion должен быть '" + BundleManifest.CURRENT_API_VERSION + + "', got: " + m.apiVersion()); + } + if (!BundleManifest.KIND.equals(m.kind())) { + errors.add("kind должен быть '" + BundleManifest.KIND + "', got: " + m.kind()); + } + if (m.metadata() == null) { + errors.add("metadata is required"); + } else { + validateMetadata(m.metadata(), errors); + } + if (m.dictionaries() == null || m.dictionaries().isEmpty()) { + errors.add("dictionaries: at least one entry required"); + } else { + var seenNames = new java.util.HashSet(); + for (int i = 0; i < m.dictionaries().size(); i++) { + validateDictionary(m.dictionaries().get(i), i, seenNames, errors); + } + } + return errors; + } + + private static void validateMetadata(BundleManifest.Metadata md, List errors) { + if (md.id() == null || !BUNDLE_ID.matcher(md.id()).matches()) { + errors.add("metadata.id: must match " + BUNDLE_ID + ", got: " + md.id()); + } + if (md.name() == null || md.name().isBlank()) { + errors.add("metadata.name: required"); + } + if (md.version() == null || !SEMVER.matcher(md.version()).matches()) { + errors.add("metadata.version: must be semver (X.Y.Z[-suffix]), got: " + md.version()); + } + if (md.scope() != null && !isValidScope(md.scope())) { + errors.add("metadata.scope: must be PUBLIC/INTERNAL/RESTRICTED, got: " + md.scope()); + } + if (md.signature() != null && !md.signature().isBlank() + && !md.signature().startsWith("ed25519:")) { + errors.add("metadata.signature: must be ed25519:, got prefix: " + + md.signature().split(":", 2)[0]); + } + } + + private static void validateDictionary( + BundleManifest.DictionaryEntry d, int idx, + java.util.Set seenNames, List errors) { + String ctx = "dictionaries[" + idx + "]"; + if (d.name() == null || !DICT_NAME.matcher(d.name()).matches()) { + errors.add(ctx + ".name: must match " + DICT_NAME + ", got: " + d.name()); + } else if (!seenNames.add(d.name())) { + errors.add(ctx + ".name: duplicate '" + d.name() + "'"); + } + if (d.schemaVersion() == null || !SEMVER.matcher(d.schemaVersion()).matches()) { + errors.add(ctx + ".schemaVersion: must be semver, got: " + d.schemaVersion()); + } + if (d.scope() == null || !isValidScope(d.scope())) { + errors.add(ctx + ".scope: must be PUBLIC/INTERNAL/RESTRICTED, got: " + d.scope()); + } + if (d.schemaRef() == null || d.schemaRef().isBlank()) { + errors.add(ctx + ".schemaRef: required"); + } else if (d.schemaRef().contains("..") || d.schemaRef().startsWith("/")) { + // Path traversal guard — schemaRef должен быть bundle-relative. + errors.add(ctx + ".schemaRef: must be bundle-relative path без '..', got: " + d.schemaRef()); + } + } + + private static boolean isValidScope(String s) { + return "PUBLIC".equals(s) || "INTERNAL".equals(s) || "RESTRICTED".equals(s); + } + + /** UTF-8 helper — for string-based tests + REPL usage. */ + public static BundleManifest parseString(String yaml) throws ManifestException { + return parse(yaml.getBytes(StandardCharsets.UTF_8)); + } + + public static class ManifestException extends Exception { + public ManifestException(String message) { super(message); } + public ManifestException(String message, Throwable cause) { super(message, cause); } + } +} diff --git a/ordinis-bundle-spec/src/test/java/cloud/nstart/terravault/ordinis/bundle/spec/BundleSignatureVerifierTest.java b/ordinis-bundle-spec/src/test/java/cloud/nstart/terravault/ordinis/bundle/spec/BundleSignatureVerifierTest.java new file mode 100644 index 0000000..fe4d82b --- /dev/null +++ b/ordinis-bundle-spec/src/test/java/cloud/nstart/terravault/ordinis/bundle/spec/BundleSignatureVerifierTest.java @@ -0,0 +1,86 @@ +package cloud.nstart.terravault.ordinis.bundle.spec; + +import org.junit.jupiter.api.Test; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.SecureRandom; + +import static org.assertj.core.api.Assertions.assertThat; + +class BundleSignatureVerifierTest { + + /** Generate ephemeral ed25519 keypair → sign → verify round-trip. */ + @Test + void roundtrip_sign_then_verify_ok() throws Exception { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("Ed25519"); + kpg.initialize(255, new SecureRandom()); + KeyPair kp = kpg.generateKeyPair(); + + byte[] archive = "fake bundle bytes for testing".getBytes(); + String signature = BundleSignatureVerifier.sign(archive, kp.getPrivate().getEncoded()); + + assertThat(signature).isNotNull(); + assertThat(signature).startsWith("ed25519:"); + + boolean valid = BundleSignatureVerifier.verify( + archive, signature, kp.getPublic().getEncoded()); + assertThat(valid).isTrue(); + } + + @Test + void verify_fails_on_tampered_archive() throws Exception { + KeyPair kp = KeyPairGenerator.getInstance("Ed25519").generateKeyPair(); + byte[] archive = "original".getBytes(); + String sig = BundleSignatureVerifier.sign(archive, kp.getPrivate().getEncoded()); + + boolean valid = BundleSignatureVerifier.verify( + "tampered".getBytes(), sig, kp.getPublic().getEncoded()); + assertThat(valid).isFalse(); + } + + @Test + void verify_fails_with_wrong_pubkey() throws Exception { + KeyPair signing = KeyPairGenerator.getInstance("Ed25519").generateKeyPair(); + KeyPair other = KeyPairGenerator.getInstance("Ed25519").generateKeyPair(); + byte[] archive = "test".getBytes(); + String sig = BundleSignatureVerifier.sign(archive, signing.getPrivate().getEncoded()); + + boolean valid = BundleSignatureVerifier.verify( + archive, sig, other.getPublic().getEncoded()); + assertThat(valid).isFalse(); + } + + @Test + void verify_rejects_missing_prefix() throws Exception { + KeyPair kp = KeyPairGenerator.getInstance("Ed25519").generateKeyPair(); + byte[] archive = "test".getBytes(); + // Sign а потом strip prefix чтобы simulate wrong format + String sig = BundleSignatureVerifier.sign(archive, kp.getPrivate().getEncoded()); + String malformed = sig.substring("ed25519:".length()); + + assertThat(BundleSignatureVerifier.verify(archive, malformed, kp.getPublic().getEncoded())) + .isFalse(); + } + + @Test + void verify_rejects_invalid_base64() throws Exception { + KeyPair kp = KeyPairGenerator.getInstance("Ed25519").generateKeyPair(); + assertThat(BundleSignatureVerifier.verify( + "test".getBytes(), "ed25519:!!! not base64 !!!", kp.getPublic().getEncoded())) + .isFalse(); + } + + @Test + void verify_rejects_null_inputs() { + assertThat(BundleSignatureVerifier.verify(null, "ed25519:abc", new byte[]{})).isFalse(); + assertThat(BundleSignatureVerifier.verify(new byte[]{}, null, new byte[]{})).isFalse(); + assertThat(BundleSignatureVerifier.verify(new byte[]{}, "ed25519:abc", null)).isFalse(); + } + + @Test + void sign_returns_null_on_invalid_key() { + assertThat(BundleSignatureVerifier.sign("test".getBytes(), "not a real key".getBytes())) + .isNull(); + } +} diff --git a/ordinis-bundle-spec/src/test/java/cloud/nstart/terravault/ordinis/bundle/spec/CuodManifestFixtureTest.java b/ordinis-bundle-spec/src/test/java/cloud/nstart/terravault/ordinis/bundle/spec/CuodManifestFixtureTest.java new file mode 100644 index 0000000..b757fa9 --- /dev/null +++ b/ordinis-bundle-spec/src/test/java/cloud/nstart/terravault/ordinis/bundle/spec/CuodManifestFixtureTest.java @@ -0,0 +1,33 @@ +package cloud.nstart.terravault.ordinis.bundle.spec; + +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Pin test: the actual CUOD bundle manifest must parse и validate clean. + * Fixture file копируется из {@code ordinis-cuod-bundle/manifest.yaml} (build + * step pre-commit, см. README). Если manifest добавляет invalid entry — этот + * test fail'нет на CI. + * + *

Подтверждает что spec самодостаточен для production CUOD bundle (40 dicts). + */ +class CuodManifestFixtureTest { + + @Test + void cuod_fixture_parses_and_validates_clean() throws Exception { + try (InputStream is = getClass().getClassLoader() + .getResourceAsStream("cuod-manifest-fixture.yaml")) { + assertThat(is).as("cuod-manifest-fixture.yaml must exist в test resources").isNotNull(); + BundleManifest m = ManifestParser.parse(is); + assertThat(m.apiVersion()).isEqualTo("ordinis.io/v1"); + assertThat(m.metadata().id()).isEqualTo("ru.cuod.dzz-ground-segment"); + assertThat(m.dictionaries()).hasSizeGreaterThanOrEqualTo(40); + List errors = ManifestParser.validate(m); + assertThat(errors).as("validation errors").isEmpty(); + } + } +} diff --git a/ordinis-bundle-spec/src/test/java/cloud/nstart/terravault/ordinis/bundle/spec/ManifestParserTest.java b/ordinis-bundle-spec/src/test/java/cloud/nstart/terravault/ordinis/bundle/spec/ManifestParserTest.java new file mode 100644 index 0000000..a6b4b7c --- /dev/null +++ b/ordinis-bundle-spec/src/test/java/cloud/nstart/terravault/ordinis/bundle/spec/ManifestParserTest.java @@ -0,0 +1,127 @@ +package cloud.nstart.terravault.ordinis.bundle.spec; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ManifestParserTest { + + private static final String VALID_MANIFEST = """ + apiVersion: ordinis.io/v1 + kind: Bundle + metadata: + id: ru.cuod.dzz-ground-segment + name: ЦУОД ДЗЗ — наземный сегмент + version: 1.0.0 + domain: dzz + scope: PUBLIC + author: ЦУОД team + license: proprietary + dictionaries: + - name: spacecraft + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: dictionaries/spacecraft/schema.json + - name: satellite_type + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: dictionaries/satellite_type/schema.json + """; + + @Test + void parses_canonical_manifest() throws Exception { + var m = ManifestParser.parseString(VALID_MANIFEST); + assertThat(m.apiVersion()).isEqualTo("ordinis.io/v1"); + assertThat(m.kind()).isEqualTo("Bundle"); + assertThat(m.metadata().id()).isEqualTo("ru.cuod.dzz-ground-segment"); + assertThat(m.metadata().version()).isEqualTo("1.0.0"); + assertThat(m.dictionaries()).hasSize(2); + assertThat(m.dictionaries().get(0).name()).isEqualTo("spacecraft"); + } + + @Test + void validate_clean_manifest_returns_no_errors() throws Exception { + var m = ManifestParser.parseString(VALID_MANIFEST); + assertThat(ManifestParser.validate(m)).isEmpty(); + } + + @Test + void rejects_wrong_apiVersion() throws Exception { + var bad = VALID_MANIFEST.replace("ordinis.io/v1", "ordinis.io/v0"); + var errors = ManifestParser.parseAndValidate(bad.getBytes()); + assertThat(errors).anyMatch(e -> e.contains("apiVersion")); + } + + @Test + void rejects_bad_semver() throws Exception { + var bad = VALID_MANIFEST.replace("version: 1.0.0", "version: v1.0"); + var errors = ManifestParser.parseAndValidate(bad.getBytes()); + assertThat(errors).anyMatch(e -> e.contains("metadata.version")); + } + + @Test + void rejects_path_traversal_in_schemaRef() throws Exception { + var bad = VALID_MANIFEST.replace( + "dictionaries/spacecraft/schema.json", + "../../../etc/passwd"); + var errors = ManifestParser.parseAndValidate(bad.getBytes()); + assertThat(errors).anyMatch(e -> e.contains("schemaRef")); + } + + @Test + void rejects_duplicate_dict_name() throws Exception { + var bad = VALID_MANIFEST.replace("name: satellite_type", "name: spacecraft"); + var errors = ManifestParser.parseAndValidate(bad.getBytes()); + assertThat(errors).anyMatch(e -> e.contains("duplicate")); + } + + @Test + void rejects_invalid_scope() throws Exception { + var bad = VALID_MANIFEST.replaceFirst("scope: PUBLIC", "scope: SUPER_SECRET"); + var errors = ManifestParser.parseAndValidate(bad.getBytes()); + assertThat(errors).anyMatch(e -> e.contains("scope")); + } + + @Test + void rejects_invalid_bundle_id() throws Exception { + var bad = VALID_MANIFEST.replace("ru.cuod.dzz-ground-segment", "RU.CUOD!"); + var errors = ManifestParser.parseAndValidate(bad.getBytes()); + assertThat(errors).anyMatch(e -> e.contains("metadata.id")); + } + + @Test + void rejects_signature_without_ed25519_prefix() throws Exception { + var withBadSig = VALID_MANIFEST + " signature: rsa:abc123\n"; + var errors = ManifestParser.parseAndValidate(withBadSig.getBytes()); + assertThat(errors).anyMatch(e -> e.contains("signature")); + } + + @Test + void unknown_fields_ignored() throws Exception { + var withExtra = VALID_MANIFEST + "extraField: someValue\n"; + var m = ManifestParser.parseString(withExtra); + assertThat(m.metadata().id()).isEqualTo("ru.cuod.dzz-ground-segment"); + } + + @Test + void empty_dictionaries_list_rejected() throws Exception { + var empty = """ + apiVersion: ordinis.io/v1 + kind: Bundle + metadata: + id: x.test + name: Test + version: 1.0.0 + dictionaries: [] + """; + var errors = ManifestParser.parseAndValidate(empty.getBytes()); + assertThat(errors).anyMatch(e -> e.contains("dictionaries")); + } + + @Test + void malformed_yaml_throws() { + assertThatThrownBy(() -> ManifestParser.parseString("not: valid: yaml: deeply: nested:bad:")) + .isInstanceOf(ManifestParser.ManifestException.class); + } +} diff --git a/ordinis-bundle-spec/src/test/resources/cuod-manifest-fixture.yaml b/ordinis-bundle-spec/src/test/resources/cuod-manifest-fixture.yaml new file mode 100644 index 0000000..faee751 --- /dev/null +++ b/ordinis-bundle-spec/src/test/resources/cuod-manifest-fixture.yaml @@ -0,0 +1,181 @@ +# Bundle manifest spec — apiVersion: ordinis.io/v1, kind: Bundle +# Canonical bundle authoring artifact. См. docs-internal/design/dictionary-marketplace.md +# Парсинг через cloud.nstart.terravault.ordinis.bundle.spec.ManifestParser. +apiVersion: ordinis.io/v1 +kind: Bundle + +metadata: + id: ru.cuod.dzz-ground-segment + name: ЦУОД ДЗЗ — наземный сегмент + version: 1.0.0 + description: | + 40 справочников для управления наземным сегментом ДЗЗ: + космические аппараты, типы КА, наземные станции, антенны, + частотные диапазоны, операторы, оборудование, миссии и т.д. + domain: dzz + scope: PUBLIC + author: ЦУОД team + license: proprietary + # signature заполняется ordinis-bundle-publisher CLI на publish step. + +dictionaries: + - name: acquisition_mode + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/acquisition_mode.schema.json + - name: antenna + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/antenna.schema.json + - name: atmospheric_correction + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/atmospheric_correction.schema.json + - name: climatic_zone + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/climatic_zone.schema.json + - name: compression_algorithm + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/compression_algorithm.schema.json + - name: consumer + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/consumer.schema.json + - name: consumer_type + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/consumer_type.schema.json + - name: contract_type + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/contract_type.schema.json + - name: coordinate_system + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/coordinate_system.schema.json + - name: country + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/country.schema.json + - name: coverage_zone + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/coverage_zone.schema.json + - name: data_format + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/data_format.schema.json + - name: deliverable_type + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/deliverable_type.schema.json + - name: frequency_band + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/frequency_band.schema.json + - name: geometric_correction + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/geometric_correction.schema.json + - name: ground_station + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/ground_station.schema.json + - name: imaging_task_type + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/imaging_task_type.schema.json + - name: inclination_class + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/inclination_class.schema.json + - name: industry_sector + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/industry_sector.schema.json + - name: instrument + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/instrument.schema.json + - name: land_cover_type + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/land_cover_type.schema.json + - name: launch_site + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/launch_site.schema.json + - name: launch_vehicle + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/launch_vehicle.schema.json + - name: license_type + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/license_type.schema.json + - name: map_projection + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/map_projection.schema.json + - name: mission + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/mission.schema.json + - name: modulation_scheme + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/modulation_scheme.schema.json + - name: operator + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/operator.schema.json + - name: orbit_type + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/orbit_type.schema.json + - name: orbital_regime + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/orbital_regime.schema.json + - name: polarization + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/polarization.schema.json + - name: priority_class + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/priority_class.schema.json + - name: processing_level + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/processing_level.schema.json + - name: qa_flag + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/qa_flag.schema.json + - name: resolution_class + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/resolution_class.schema.json + - name: satellite_type + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/satellite_type.schema.json + - name: sensor_type + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/sensor_type.schema.json + - name: spacecraft + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/spacecraft.schema.json + - name: spacecraft_status + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/spacecraft_status.schema.json + - name: spectral_band + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/spectral_band.schema.json diff --git a/ordinis-cuod-bundle/manifest.yaml b/ordinis-cuod-bundle/manifest.yaml new file mode 100644 index 0000000..faee751 --- /dev/null +++ b/ordinis-cuod-bundle/manifest.yaml @@ -0,0 +1,181 @@ +# Bundle manifest spec — apiVersion: ordinis.io/v1, kind: Bundle +# Canonical bundle authoring artifact. См. docs-internal/design/dictionary-marketplace.md +# Парсинг через cloud.nstart.terravault.ordinis.bundle.spec.ManifestParser. +apiVersion: ordinis.io/v1 +kind: Bundle + +metadata: + id: ru.cuod.dzz-ground-segment + name: ЦУОД ДЗЗ — наземный сегмент + version: 1.0.0 + description: | + 40 справочников для управления наземным сегментом ДЗЗ: + космические аппараты, типы КА, наземные станции, антенны, + частотные диапазоны, операторы, оборудование, миссии и т.д. + domain: dzz + scope: PUBLIC + author: ЦУОД team + license: proprietary + # signature заполняется ordinis-bundle-publisher CLI на publish step. + +dictionaries: + - name: acquisition_mode + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/acquisition_mode.schema.json + - name: antenna + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/antenna.schema.json + - name: atmospheric_correction + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/atmospheric_correction.schema.json + - name: climatic_zone + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/climatic_zone.schema.json + - name: compression_algorithm + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/compression_algorithm.schema.json + - name: consumer + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/consumer.schema.json + - name: consumer_type + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/consumer_type.schema.json + - name: contract_type + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/contract_type.schema.json + - name: coordinate_system + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/coordinate_system.schema.json + - name: country + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/country.schema.json + - name: coverage_zone + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/coverage_zone.schema.json + - name: data_format + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/data_format.schema.json + - name: deliverable_type + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/deliverable_type.schema.json + - name: frequency_band + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/frequency_band.schema.json + - name: geometric_correction + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/geometric_correction.schema.json + - name: ground_station + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/ground_station.schema.json + - name: imaging_task_type + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/imaging_task_type.schema.json + - name: inclination_class + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/inclination_class.schema.json + - name: industry_sector + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/industry_sector.schema.json + - name: instrument + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/instrument.schema.json + - name: land_cover_type + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/land_cover_type.schema.json + - name: launch_site + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/launch_site.schema.json + - name: launch_vehicle + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/launch_vehicle.schema.json + - name: license_type + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/license_type.schema.json + - name: map_projection + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/map_projection.schema.json + - name: mission + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/mission.schema.json + - name: modulation_scheme + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/modulation_scheme.schema.json + - name: operator + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/operator.schema.json + - name: orbit_type + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/orbit_type.schema.json + - name: orbital_regime + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/orbital_regime.schema.json + - name: polarization + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/polarization.schema.json + - name: priority_class + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/priority_class.schema.json + - name: processing_level + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/processing_level.schema.json + - name: qa_flag + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/qa_flag.schema.json + - name: resolution_class + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/resolution_class.schema.json + - name: satellite_type + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/satellite_type.schema.json + - name: sensor_type + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/sensor_type.schema.json + - name: spacecraft + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/spacecraft.schema.json + - name: spacecraft_status + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/spacecraft_status.schema.json + - name: spectral_band + schemaVersion: 1.0.0 + scope: PUBLIC + schemaRef: src/main/resources/bundles/cuod/spectral_band.schema.json diff --git a/pom.xml b/pom.xml index 7ed5c14..2208f6a 100644 --- a/pom.xml +++ b/pom.xml @@ -29,6 +29,7 @@ ordinis-app ordinis-read-api ordinis-projection-writer + ordinis-bundle-spec ordinis-cuod-bundle ordinis-migrations ordinis-notifications