diff --git a/README.md b/README.md index fd4a012..2cfabbb 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ A keyless node reading a local inbox, serving the bundled single-variant COVID allele-frequency sample. Nothing to author, no keys, no Docker. First the node: ```bash -mkdir -p ~/gdi-demo/inbox ~/gdi-demo/datasets +mkdir -p -m 0700 ~/gdi-demo/inbox ~/gdi-demo/datasets # keep the inbox owner-only, or the node warns cat > ~/gdi-demo/node.toml < int: return 1 # Value-constraint skips are reported, not fatal: unlike the mandatory predicates above, # an `sh:in`/`sh:pattern` site can legitimately target an optional property the corpus - # does not populate (`adms:status`, `dct:type` on a non-synthetic dataset). Making these - # fatal would force the corpus to carry every optional field, giving a fixture less like - # the real node the crawl is meant to represent. + # does not populate (`dct:type` on a non-synthetic dataset). Making these fatal would + # force the corpus to carry every optional field, giving a fixture less like the real + # node the crawl is meant to represent. if value_skipped: print( f"\nnote: {len(value_skipped)} value-constraint site(s) not exercised by this " @@ -876,9 +876,9 @@ def main(argv: list[str]) -> int: "is a corpus gap:" ) print( - " (a) optional property the corpus does not populate — `dct:type` (set only on " - "synthetic datasets) and `Distribution`'s `adms:status`. Closable by extending " - "the fixture corpus, at the cost of a corpus less like a real node." + " (a) optional property the corpus does not populate — `dct:type`, set only on " + "synthetic datasets. Closable by extending the fixture corpus, at the cost of a " + "corpus less like a real node." ) print( " (b) structurally unexercisable — every remaining `sh:uniqueLang` site. The " diff --git a/crates/fairdp/src/graph.rs b/crates/fairdp/src/graph.rs index 6df1d96..ceaee40 100644 --- a/crates/fairdp/src/graph.rs +++ b/crates/fairdp/src/graph.rs @@ -338,7 +338,7 @@ pub(crate) fn add_agent( /// `dcatap:applicableLegislation` and `dct:license` are inherited from the parent /// dataset. Its title and `dcat:accessURL` (where a user asks for access) come from /// `[fairdp.distribution]`; the inline `dcat:accessService` `dcat:DataService` is the -/// aggregated Beacon. +/// aggregated Beacon. Its `adms:status` is constant, as on the dataset. #[must_use] pub fn distribution_graph(entry: &DatasetEntry, ctx: &FdpContext) -> Graph { let mut b = GraphBuilder::new(); @@ -374,6 +374,12 @@ pub fn distribution_graph(entry: &DatasetEntry, ctx: &FdpContext) -> Graph { } b.add_iri(subj.clone(), vocab::DCT_LICENSE, &entry.metadata.license); + b.add_iri( + subj.clone(), + vocab::ADMS_STATUS, + vocab::DISTRIBUTION_STATUS_COMPLETED, + ); + // Inline DataService blank node under dcat:accessService. let service_subj = b.add_blank_node(&subj, vocab::DCAT_ACCESS_SERVICE, vocab::DCAT_DATA_SERVICE); diff --git a/crates/fairdp/src/vocab.rs b/crates/fairdp/src/vocab.rs index 9eb9ed2..e622267 100644 --- a/crates/fairdp/src/vocab.rs +++ b/crates/fairdp/src/vocab.rs @@ -215,6 +215,12 @@ pub(crate) const FDP_SPEC_V1_2: &str = "https://specs.fairdatapoint.org/v1.2/fdp pub(crate) const DATASET_STATUS_COMPLETED: &str = "http://publications.europa.eu/resource/authority/dataset-status/COMPLETED"; +/// `adms:status` constant on a distribution: the EU distribution-status `COMPLETED` +/// value. Distributions have their own status table, the only one gdi-metadata's +/// `DistributionShape` accepts. +pub(crate) const DISTRIBUTION_STATUS_COMPLETED: &str = + "http://publications.europa.eu/resource/authority/distribution-status/COMPLETED"; + // --- XSD datatype IRIs --- /// `xsd:string`. diff --git a/crates/fairdp/tests/snapshots/render__distribution_ntriples.snap b/crates/fairdp/tests/snapshots/render__distribution_ntriples.snap index 2487e0b..d8504f2 100644 --- a/crates/fairdp/tests/snapshots/render__distribution_ntriples.snap +++ b/crates/fairdp/tests/snapshots/render__distribution_ntriples.snap @@ -8,6 +8,7 @@ expression: nt "GDI User Portal" + _:c14n0 diff --git a/crates/gdi-dataset-tool/src/state.rs b/crates/gdi-dataset-tool/src/state.rs index 36a598e..09b555d 100644 --- a/crates/gdi-dataset-tool/src/state.rs +++ b/crates/gdi-dataset-tool/src/state.rs @@ -593,7 +593,7 @@ pub fn require_inbox(inbox: &Path) -> Result<(), ToolError> { "inbox {} does not exist. The node creates its inbox at startup, so an absent \ one means this is not that node's inbox path. Check the profile's `inbox` \ or `--inbox` against the node's `[service].inbox`. If you are staging a drop \ - before the node's first start, create it first: mkdir -p {}", + before the node's first start, create it first: mkdir -p -m 0700 {}", inbox.display(), inbox.display() ))), diff --git a/crates/gdi-node-standalone/src/main.rs b/crates/gdi-node-standalone/src/main.rs index 1ae7dea..57b07ae 100644 --- a/crates/gdi-node-standalone/src/main.rs +++ b/crates/gdi-node-standalone/src/main.rs @@ -1171,14 +1171,9 @@ async fn run(cli: Cli) -> Result<()> { // Inbox watcher + periodic rescan, if an inbox is configured. if let Some(inbox) = state.config.service.inbox.clone() { - // The inbox is an operator-provisioned ingress a provider drops into, so it keeps - // its configured permissions. Unlike the rest of the data path, it is not forced - // 0o700. - #[expect( - clippy::disallowed_methods, - reason = "the inbox is an operator-provisioned ingress that keeps its configured permissions" - )] - std::fs::create_dir_all(&inbox) + // A pre-existing inbox, such as a mounted volume or one the operator made, keeps its + // mode. One the node creates is owner-only, like the rest of the data path. + gdi_node_standalone_core::util::create_private_dir(&inbox) .with_context(|| format!("creating inbox {}", inbox.display()))?; // An inbox writable by more than its owner is a multi-tenant plaintext inbox, an // unsupported topology: a co-tenant can swap a just-validated staging file for a diff --git a/deploy/kubernetes/base/networkpolicy.yaml b/deploy/kubernetes/base/networkpolicy.yaml index d83c54f..de84283 100644 --- a/deploy/kubernetes/base/networkpolicy.yaml +++ b/deploy/kubernetes/base/networkpolicy.yaml @@ -36,12 +36,10 @@ spec: protocol: TCP # Management plane: the monitoring namespace's scraper only. # - # `namespaceSelector` and `podSelector` sit in the same `from` element: one - # element ANDs its selectors, whereas two elements OR them — which would widen this - # rule to every pod in the cluster carrying the label, the exact opposite of the - # intent. A bare `namespaceSelector` would admit every pod in `monitoring` (Grafana, - # Alertmanager, any sidecar, anything that later lands there) to the unauthenticated - # dataset-state oracle, so it is worth the extra selector. + # `namespaceSelector` and `podSelector` sit in the same `from` element, so both + # must match. Split into two elements, either would do, and every pod in + # `monitoring` (Grafana, Alertmanager, anything that later lands there) would + # reach the unauthenticated dataset-state oracle. # # Adjust the pod label to match your scraper — `app.kubernetes.io/name: prometheus` is # the prometheus-operator convention, not a universal one. Getting it wrong blocks the diff --git a/docs/gdi-dataset-tool.md b/docs/gdi-dataset-tool.md index 4e4aaad..fde3564 100644 --- a/docs/gdi-dataset-tool.md +++ b/docs/gdi-dataset-tool.md @@ -1830,8 +1830,8 @@ asynchronously: a successful `deploy` only drops the artifact into the inbox, so > wrong, and `deploy`, `publish`, `unpublish` and `delete` all refuse rather than create > it. A created-on-demand inbox would turn the one signal a mistyped path gives, `ENOENT`, > into a phantom inbox that accepts every drop while the node reads the real one and sees -> nothing. If you are staging a drop before the node's first start, `mkdir -p ` -> first; the error message names that command. +> nothing. If you are staging a drop before the node's first start, run +> `mkdir -p -m 0700 ` first; the error message names that command. ```bash gdi-dataset-tool deploy GDI-EE-EXAMPLE-20260409143052837.tar.c4gh --inbox /var/inbox