Merge branch 'feat/bundle-hygiene-manifest' into 'main'
feat(bundle): bundle hygiene v2 — manifest spec + signing verifier + docs See merge request 2-6/2-6-4/terravault/ordinis!236
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).
|
||||
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>cloud.nstart.terravault.ordinis</groupId>
|
||||
<artifactId>ordinis-parent</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>ordinis-bundle-spec</artifactId>
|
||||
<name>Ordinis :: Bundle Spec</name>
|
||||
<description>
|
||||
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.
|
||||
</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
||||
<artifactId>jackson-dataformat-yaml</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
+85
@@ -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).
|
||||
*
|
||||
* <p>Этот record — Java mapping для {@code manifest.yaml} в корне любого
|
||||
* bundle artifact (например {@code ordinis-cuod-bundle/manifest.yaml}).
|
||||
*
|
||||
* <p><b>Что это НЕ:</b> не 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.
|
||||
*
|
||||
* <p><b>Зачем сейчас (N=1 customer):</b>
|
||||
* <ul>
|
||||
* <li>Java team handoff — new engineers получают formal spec вместо
|
||||
* reverse-engineering BundleImporter</li>
|
||||
* <li>Sales demo — «here's how your team would ship custom bundle»
|
||||
* без shipping консьюмерского UI</li>
|
||||
* <li>Optionality — когда customer #2 пробуждается, spec уже defined</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>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<DictionaryEntry> 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:<base64>}.
|
||||
* Добавляется 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) {}
|
||||
}
|
||||
+92
@@ -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.
|
||||
*
|
||||
* <p>Bundle publisher CLI (отдельный artifact) подписывает {@code bundle.tar.gz}
|
||||
* приватным ed25519 key → publish'ит signature в формате {@code ed25519:<base64>}
|
||||
* либо в {@link BundleManifest.Metadata#signature()}, либо в отдельный файл
|
||||
* {@code bundle.sig} рядом.
|
||||
*
|
||||
* <p>Этот verifier ДОКУМЕНТИРУЕТ verification contract — НЕ consumed на v2
|
||||
* ordinis runtime (no install flow). Built как handoff artifact для future
|
||||
* consumer marketplace + Java team reference.
|
||||
*
|
||||
* <p>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).
|
||||
*
|
||||
* <p>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:<base64>"
|
||||
* @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:<base64>}.
|
||||
*
|
||||
* <p>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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+144
@@ -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).
|
||||
*
|
||||
* <p>Не валидирует 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<String> 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<String> validate(BundleManifest m) {
|
||||
List<String> 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<String>();
|
||||
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<String> 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:<base64>, got prefix: "
|
||||
+ md.signature().split(":", 2)[0]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateDictionary(
|
||||
BundleManifest.DictionaryEntry d, int idx,
|
||||
java.util.Set<String> seenNames, List<String> 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); }
|
||||
}
|
||||
}
|
||||
+86
@@ -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();
|
||||
}
|
||||
}
|
||||
+33
@@ -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.
|
||||
*
|
||||
* <p>Подтверждает что 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<String> errors = ManifestParser.validate(m);
|
||||
assertThat(errors).as("validation errors").isEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
+127
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user