diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index e6a6a8dc..7a26a572 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -27,6 +27,7 @@ - [`man bootc-rollback`](man/bootc-rollback.md) - [`man bootc-usr-overlay`](man/bootc-usr-overlay.md) - [`man bootc-fetch-apply-updates.service`](man-md/bootc-fetch-apply-updates-service.md) +- [Controlling bootc via API](bootc-via-api.md) # Using `bootc install` diff --git a/docs/src/bootc-via-api.md b/docs/src/bootc-via-api.md new file mode 100644 index 00000000..b5fe2d40 --- /dev/null +++ b/docs/src/bootc-via-api.md @@ -0,0 +1,29 @@ +# Using bootc via API + +At the current time, bootc is primarily intended to be +driven via a fork/exec model. The core CLI verbs +are stable and will not change. + +## Using `bootc edit` and `bootc status --json --format-version=0` + +While bootc does not depend on Kubernetes, it does currently +also offere a Kubernetes *style* API, especially oriented +towards the [spec and status and other conventions](https://kubernetes.io/docs/reference/using-api/api-concepts/). + +In general, most use cases of driving bootc via API are probably +most easily done by forking off `bootc upgrade` when desired, +and viewing `bootc status --json --format-version=0`. + +## JSON Schema + +The current API is classified as `org.containers.bootc/v1alpha1` but +it will likely be officially stabilized mostly as is. However, +you should still request the current "v0" format via an explicit +`--format-version=0` as referenced above. + +There is a [JSON schema](https://json-schema.org/) generated from +the Rust source code available here: [host-v0.schema.json](host-v0.schema.json). + +A common way to use this is to run a code generator such as +[go-jsonschema](https://github.com/omissis/go-jsonschema) on the +input schema. diff --git a/docs/src/host-v0.schema.json b/docs/src/host-v0.schema.json new file mode 100644 index 00000000..ab4e70e9 --- /dev/null +++ b/docs/src/host-v0.schema.json @@ -0,0 +1,371 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Host", + "description": "The core host definition", + "type": "object", + "required": [ + "apiVersion", + "kind" + ], + "properties": { + "apiVersion": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/definitions/ObjectMeta" + } + ] + }, + "spec": { + "description": "The spec", + "default": { + "bootOrder": "default", + "image": null + }, + "allOf": [ + { + "$ref": "#/definitions/HostSpec" + } + ] + }, + "status": { + "description": "The status", + "default": { + "booted": null, + "rollback": null, + "rollbackQueued": false, + "staged": null, + "type": null + }, + "allOf": [ + { + "$ref": "#/definitions/HostStatus" + } + ] + } + }, + "definitions": { + "BootEntry": { + "description": "A bootable entry", + "type": "object", + "required": [ + "incompatible", + "pinned" + ], + "properties": { + "cachedUpdate": { + "description": "The last fetched cached update metadata", + "anyOf": [ + { + "$ref": "#/definitions/ImageStatus" + }, + { + "type": "null" + } + ] + }, + "image": { + "description": "The image reference", + "anyOf": [ + { + "$ref": "#/definitions/ImageStatus" + }, + { + "type": "null" + } + ] + }, + "incompatible": { + "description": "Whether this boot entry is not compatible (has origin changes bootc does not understand)", + "type": "boolean" + }, + "ostree": { + "description": "If this boot entry is ostree based, the corresponding state", + "anyOf": [ + { + "$ref": "#/definitions/BootEntryOstree" + }, + { + "type": "null" + } + ] + }, + "pinned": { + "description": "Whether this entry will be subject to garbage collection", + "type": "boolean" + } + } + }, + "BootEntryOstree": { + "description": "A bootable entry", + "type": "object", + "required": [ + "checksum", + "deploySerial" + ], + "properties": { + "checksum": { + "description": "The ostree commit checksum", + "type": "string" + }, + "deploySerial": { + "description": "The deployment serial", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + } + }, + "BootOrder": { + "description": "Configuration for system boot ordering.", + "oneOf": [ + { + "description": "The staged or booted deployment will be booted next", + "type": "string", + "enum": [ + "default" + ] + }, + { + "description": "The rollback deployment will be booted next", + "type": "string", + "enum": [ + "rollback" + ] + } + ] + }, + "HostSpec": { + "description": "The host specification", + "type": "object", + "properties": { + "bootOrder": { + "description": "If set, and there is a rollback deployment, it will be set for the next boot.", + "default": "default", + "allOf": [ + { + "$ref": "#/definitions/BootOrder" + } + ] + }, + "image": { + "description": "The host image", + "anyOf": [ + { + "$ref": "#/definitions/ImageReference" + }, + { + "type": "null" + } + ] + } + } + }, + "HostStatus": { + "description": "The status of the host system", + "type": "object", + "properties": { + "booted": { + "description": "The booted image; this will be unset if the host is not bootc compatible.", + "anyOf": [ + { + "$ref": "#/definitions/BootEntry" + }, + { + "type": "null" + } + ] + }, + "rollback": { + "description": "The previously booted image", + "anyOf": [ + { + "$ref": "#/definitions/BootEntry" + }, + { + "type": "null" + } + ] + }, + "rollbackQueued": { + "description": "Set to true if the rollback entry is queued for the next boot.", + "default": false, + "type": "boolean" + }, + "staged": { + "description": "The staged image for the next boot", + "anyOf": [ + { + "$ref": "#/definitions/BootEntry" + }, + { + "type": "null" + } + ] + }, + "type": { + "description": "The detected type of system", + "anyOf": [ + { + "$ref": "#/definitions/HostType" + }, + { + "type": "null" + } + ] + } + } + }, + "HostType": { + "description": "The detected type of running system. Note that this is not exhaustive and new variants may be added in the future.", + "oneOf": [ + { + "description": "The current system is deployed in a bootc compatible way.", + "type": "string", + "enum": [ + "bootcHost" + ] + } + ] + }, + "ImageReference": { + "description": "A container image reference with attached transport and signature verification", + "type": "object", + "required": [ + "image", + "transport" + ], + "properties": { + "image": { + "description": "The container image reference", + "type": "string" + }, + "signature": { + "description": "Signature verification type", + "anyOf": [ + { + "$ref": "#/definitions/ImageSignature" + }, + { + "type": "null" + } + ] + }, + "transport": { + "description": "The container image transport", + "type": "string" + } + } + }, + "ImageSignature": { + "description": "An image signature", + "oneOf": [ + { + "description": "Fetches will use the named ostree remote for signature verification of the ostree commit.", + "type": "object", + "required": [ + "ostreeRemote" + ], + "properties": { + "ostreeRemote": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "Fetches will defer to the `containers-policy.json`, but we make a best effort to reject `default: insecureAcceptAnything` policy.", + "type": "string", + "enum": [ + "containerPolicy" + ] + }, + { + "description": "No signature verification will be performed", + "type": "string", + "enum": [ + "insecure" + ] + } + ] + }, + "ImageStatus": { + "description": "The status of the booted image", + "type": "object", + "required": [ + "image", + "imageDigest" + ], + "properties": { + "image": { + "description": "The currently booted image", + "allOf": [ + { + "$ref": "#/definitions/ImageReference" + } + ] + }, + "imageDigest": { + "description": "The digest of the fetched image (e.g. sha256:a0...);", + "type": "string" + }, + "timestamp": { + "description": "The build timestamp, if any", + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "version": { + "description": "The version string, if any", + "type": [ + "string", + "null" + ] + } + } + }, + "ObjectMeta": { + "type": "object", + "properties": { + "annotations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "labels": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "namespace": { + "type": [ + "string", + "null" + ] + } + } + } + } +} \ No newline at end of file diff --git a/lib/src/cli.rs b/lib/src/cli.rs index ee4dcb21..780de95b 100644 --- a/lib/src/cli.rs +++ b/lib/src/cli.rs @@ -19,6 +19,7 @@ use ostree_container::store::PrepareResult; use ostree_ext::container as ostree_container; use ostree_ext::keyfileext::KeyFileExt; use ostree_ext::ostree; +use schemars::schema_for; use crate::deploy::RequiredHostSpec; use crate::lints; @@ -244,6 +245,8 @@ pub(crate) enum InternalsOpts { late_dir: Option, }, FixupEtcFstab, + /// Should only be used by `make update-generated` + PrintJsonSchema, } impl InternalsOpts { @@ -801,6 +804,12 @@ async fn run_from_opt(opt: Opt) -> Result<()> { crate::generator::generator(root, unit_dir) } InternalsOpts::FixupEtcFstab => crate::deploy::fixup_etc_fstab(&root), + InternalsOpts::PrintJsonSchema => { + let schema = schema_for!(crate::spec::Host); + let mut stdout = std::io::stdout().lock(); + serde_json::to_writer_pretty(&mut stdout, &schema)?; + Ok(()) + } }, #[cfg(feature = "docgen")] Opt::Man(manopts) => crate::docgen::generate_manpages(&manopts.directory), diff --git a/lib/src/k8sapitypes.rs b/lib/src/k8sapitypes.rs index c2b8a87a..8bc8a956 100644 --- a/lib/src/k8sapitypes.rs +++ b/lib/src/k8sapitypes.rs @@ -3,9 +3,10 @@ use std::collections::BTreeMap; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct Resource { pub api_version: String, @@ -14,7 +15,7 @@ pub struct Resource { pub metadata: ObjectMeta, } -#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct ObjectMeta { #[serde(skip_serializing_if = "Option::is_none")] diff --git a/lib/src/spec.rs b/lib/src/spec.rs index 5f6df932..f6414cf8 100644 --- a/lib/src/spec.rs +++ b/lib/src/spec.rs @@ -13,7 +13,7 @@ const KIND: &str = "BootcHost"; /// The default object name we use; there's only one. pub(crate) const OBJECT_NAME: &str = "host"; -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "camelCase")] /// The core host definition pub struct Host { @@ -30,7 +30,7 @@ pub struct Host { /// Configuration for system boot ordering. -#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "camelCase")] pub enum BootOrder { /// The staged or booted deployment will be booted next @@ -40,7 +40,7 @@ pub enum BootOrder { Rollback, } -#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "camelCase")] /// The host specification pub struct HostSpec { diff --git a/xtask/src/xtask.rs b/xtask/src/xtask.rs index 174d5825..f94ae5bd 100644 --- a/xtask/src/xtask.rs +++ b/xtask/src/xtask.rs @@ -139,6 +139,10 @@ fn update_generated(sh: &Shell) -> Result<()> { ) .run()?; } + let schema = cmd!(sh, "cargo run -q -- internals print-json-schema").read()?; + let target = "docs/src/host-v0.schema.json"; + std::fs::write(target, &schema)?; + println!("Updated {target}"); Ok(()) }