From 17788e91ee5e81869c094c07932e644846fb8434 Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 14 Aug 2025 00:36:20 +0000 Subject: [PATCH 1/2] [spr] initial version Created using spr 1.3.6-beta.1 --- bin/src/dispatch.rs | 244 ++++--------------- bin/tests/integration-tests/command_tests.rs | 164 +------------ lib/src/artifact.rs | 32 +-- lib/src/assemble/build.rs | 16 +- lib/src/repository.rs | 168 +------------ 5 files changed, 57 insertions(+), 567 deletions(-) diff --git a/bin/src/dispatch.rs b/bin/src/dispatch.rs index a0ba460..a9b6274 100644 --- a/bin/src/dispatch.rs +++ b/bin/src/dispatch.rs @@ -5,13 +5,10 @@ use anyhow::{Context, Result, bail}; use camino::Utf8PathBuf; use chrono::{DateTime, Utc}; -use clap::{CommandFactory, Parser}; -use semver::Version; -use tufaceous_artifact::{ - ArtifactKind, ArtifactVersion, ArtifactsDocument, KnownArtifactKind, -}; +use clap::Parser; +use tufaceous_artifact::{ArtifactsDocument, KnownArtifactKind}; use tufaceous_lib::assemble::{ArtifactManifest, OmicronRepoAssembler}; -use tufaceous_lib::{AddArtifact, ArchiveExtractor, Key, OmicronRepo}; +use tufaceous_lib::{ArchiveExtractor, Key, OmicronRepo}; #[derive(Debug, Parser)] pub struct Args { @@ -38,117 +35,44 @@ pub struct Args { impl Args { /// Executes these arguments. pub async fn exec(self, log: &slog::Logger) -> Result<()> { - let repo_path = match self.repo { - Some(repo) => repo, - None => std::env::current_dir()?.try_into()?, - }; - match self.command { - // TODO-cleanup: we no longer use the init and add commands in - // production. We should get rid of these options and direct users - // towards assemble. (If necessary, we should build tooling for - // making it easy to build up a manifest that can then be - // assembled.) - Command::Init { system_version, no_generate_key } => { - let keys = maybe_generate_keys(self.keys, no_generate_key)?; - let root = - tufaceous_lib::root::new_root(keys.clone(), self.expiry) - .await?; - - let repo = OmicronRepo::initialize( - log, - &repo_path, - system_version, - keys, - root, - self.expiry, - true, - ) - .await?; - slog::info!( - log, - "Initialized TUF repository in {}", - repo.repo_path() - ); - Ok(()) - } - Command::Add { - kind, - allow_unknown_kinds, - path, - name, - version, + Command::Assemble { + manifest_path, + output_path, + build_dir, + no_generate_key, + skip_all_present, allow_non_semver, + no_installinator_document, } => { - if !allow_unknown_kinds { - // Try converting kind to a known kind. - if kind.to_known().is_none() { - // Simulate a failure to parse (though ideally there would - // be a way to also specify the underlying error -- there - // doesn't appear to be a public API to do so in clap 4). - let mut error = clap::Error::new( - clap::error::ErrorKind::ValueValidation, - ) - .with_cmd(&Args::command()); - error.insert( - clap::error::ContextKind::InvalidArg, - clap::error::ContextValue::String( - "".to_owned(), - ), - ); - error.insert( - clap::error::ContextKind::InvalidValue, - clap::error::ContextValue::String(kind.to_string()), - ); - error.exit(); - } + // The filename must end with "zip". + if output_path.extension() != Some("zip") { + bail!("output path `{output_path}` must end with .zip"); } + let manifest = ArtifactManifest::from_path(&manifest_path) + .context("error reading manifest")?; if !allow_non_semver { - if let Err(error) = version.as_str().parse::() { - let error = Args::command().error( - clap::error::ErrorKind::ValueValidation, - format!( - "version `{version}` is not valid semver \ - (pass in --allow-non-semver to override): {error}" - ), - ); - error.exit(); - } + manifest.verify_all_semver()?; + } + if !skip_all_present { + manifest.verify_all_present()?; } - let repo = OmicronRepo::load_untrusted_ignore_expiration( - log, &repo_path, - ) - .await?; - let mut editor = repo.into_editor().await?; - - let new_artifact = - AddArtifact::from_path(kind, name, version, path)?; - - editor - .add_artifact(&new_artifact) - .context("error adding artifact")?; - editor.sign_and_finish(self.keys, self.expiry, true).await?; - println!( - "added {} {}, version {}", - new_artifact.kind(), - new_artifact.name(), - new_artifact.version() + let keys = maybe_generate_keys(self.keys, no_generate_key)?; + let mut assembler = OmicronRepoAssembler::new( + log, + manifest, + keys, + self.expiry, + !no_installinator_document, + output_path, ); - Ok(()) - } - Command::Archive { output_path } => { - // The filename must end with "zip". - if output_path.extension() != Some("zip") { - bail!("output path `{output_path}` must end with .zip"); + if let Some(dir) = build_dir { + assembler.set_build_dir(dir); } - let repo = OmicronRepo::load_untrusted_ignore_expiration( - log, &repo_path, - ) - .await?; - repo.archive(&output_path)?; + assembler.build().await?; Ok(()) } @@ -166,7 +90,7 @@ impl Args { .with_context(|| { format!( "error loading extracted repository at `{dest}` \ - (extracted files are still available)" + (extracted files are still available)" ) })?; let artifacts = @@ -207,46 +131,6 @@ impl Args { })?; } - Ok(()) - } - Command::Assemble { - manifest_path, - output_path, - build_dir, - no_generate_key, - skip_all_present, - allow_non_semver, - no_installinator_document, - } => { - // The filename must end with "zip". - if output_path.extension() != Some("zip") { - bail!("output path `{output_path}` must end with .zip"); - } - - let manifest = ArtifactManifest::from_path(&manifest_path) - .context("error reading manifest")?; - if !allow_non_semver { - manifest.verify_all_semver()?; - } - if !skip_all_present { - manifest.verify_all_present()?; - } - - let keys = maybe_generate_keys(self.keys, no_generate_key)?; - let mut assembler = OmicronRepoAssembler::new( - log, - manifest, - keys, - self.expiry, - !no_installinator_document, - output_path, - ); - if let Some(dir) = build_dir { - assembler.set_build_dir(dir); - } - - assembler.build().await?; - Ok(()) } } @@ -255,60 +139,6 @@ impl Args { #[derive(Debug, Parser)] enum Command { - /// Create a new rack update TUF repository - Init { - /// The system version. - system_version: Version, - - /// Disable random key generation and exit if no keys are provided - #[clap(long)] - no_generate_key: bool, - }, - Add { - /// The kind of artifact this is. - kind: ArtifactKind, - - /// Allow artifact kinds that aren't known to tufaceous - #[clap(long)] - allow_unknown_kinds: bool, - - /// Path to the artifact. - path: Utf8PathBuf, - - /// Override the name for this artifact (default: filename with extension stripped) - #[clap(long)] - name: Option, - - /// Artifact version. - /// - /// This is required to be semver by default, but can be overridden with - /// --allow-non-semver. - version: ArtifactVersion, - - /// Allow versions to be non-semver. - /// - /// Transitional option for v13 -> v14. After v14, versions will be - /// allowed to be non-semver by default. - #[clap(long)] - allow_non_semver: bool, - }, - /// Archives this repository to a zip file. - Archive { - /// The path to write the archive to (must end with .zip). - output_path: Utf8PathBuf, - }, - /// Validates and extracts a repository created by the `archive` command. - Extract { - /// The file to extract. - archive_file: Utf8PathBuf, - - /// The destination to extract the file to. - dest: Utf8PathBuf, - - /// Indicate that the file does not contain an installinator document. - #[clap(long)] - no_installinator_document: bool, - }, /// Assembles a repository from a provided manifest. Assemble { /// Path to artifact manifest. @@ -342,6 +172,18 @@ enum Command { #[clap(long)] no_installinator_document: bool, }, + /// Validates and extracts a repository created by the `assemble` command. + Extract { + /// The file to extract. + archive_file: Utf8PathBuf, + + /// The destination to extract the file to. + dest: Utf8PathBuf, + + /// Indicate that the file does not contain an installinator document. + #[clap(long)] + no_installinator_document: bool, + }, } fn maybe_generate_keys( diff --git a/bin/tests/integration-tests/command_tests.rs b/bin/tests/integration-tests/command_tests.rs index de63ecf..5fac6e3 100644 --- a/bin/tests/integration-tests/command_tests.rs +++ b/bin/tests/integration-tests/command_tests.rs @@ -2,166 +2,12 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -use std::path::Path; - use anyhow::Result; use assert_cmd::Command; -use camino::Utf8PathBuf; use dropshot::test_util::LogContext; use dropshot::{ConfigLogging, ConfigLoggingIfExists, ConfigLoggingLevel}; use predicates::prelude::*; -use tufaceous_artifact::{ArtifactKind, KnownArtifactKind}; -use tufaceous_lib::{Key, OmicronRepo}; - -#[tokio::test] -async fn test_init_and_add() -> Result<()> { - let log_config = ConfigLogging::File { - level: ConfigLoggingLevel::Trace, - path: "UNUSED".into(), - if_exists: ConfigLoggingIfExists::Fail, - }; - let logctx = LogContext::new("test_init_and_add", &log_config); - let tempdir = tempfile::tempdir().unwrap(); - let key = Key::generate_ed25519()?; - - let mut cmd = make_cmd_with_repo(tempdir.path(), &key); - cmd.args(["init", "0.0.0"]); - cmd.assert().success(); - - // Create a couple of stub files on disk. - let nexus_path = tempdir.path().join("nexus.tar.gz"); - fs_err::write(&nexus_path, "test")?; - let unknown_path = tempdir.path().join("my-unknown-kind.tar.gz"); - fs_err::write(&unknown_path, "unknown test")?; - let switch_sp = tempdir.path().join("switch-sp.tar.gz"); - fs_err::write(&switch_sp, "switch_sp test")?; - - let mut cmd = make_cmd_with_repo(tempdir.path(), &key); - cmd.args(["add", "gimlet_sp"]); - cmd.arg(&nexus_path); - cmd.arg("42.0.0"); - cmd.assert().success(); - - // Try adding an unknown kind without --allow-unknown-kinds. - let mut cmd = make_cmd_with_repo(tempdir.path(), &key); - cmd.args(["add", "my_unknown_kind"]); - cmd.arg(&nexus_path); - cmd.arg("0.0.0"); - cmd.assert().failure().stderr(predicate::str::contains( - "invalid value 'my_unknown_kind' for ''", - )); - - // Try adding one with --allow-unknown-kinds. - let mut cmd = make_cmd_with_repo(tempdir.path(), &key); - cmd.args(["add", "my_unknown_kind", "--allow-unknown-kinds"]); - cmd.arg(&unknown_path); - cmd.arg("0.1.0"); - cmd.assert().success(); - - // Try adding an artifact with a version that doesn't parse as valid semver. - let mut cmd = make_cmd_with_repo(tempdir.path(), &key); - cmd.args(["add", "switch_sp"]); - cmd.arg(&switch_sp); - cmd.arg("non-semver"); - cmd.assert().failure().stderr(predicate::str::contains( - "version `non-semver` is not valid semver (pass in --allow-non-semver to override)", - )); - - // Try adding one with --allow-non-semver. - let mut cmd = make_cmd_with_repo(tempdir.path(), &key); - cmd.args(["add", "switch_sp", "--allow-non-semver"]); - cmd.arg(&switch_sp); - cmd.arg("non-semver"); - cmd.assert().success(); - - // Now read the repository and ensure the list of expected artifacts. - let repo_path: Utf8PathBuf = tempdir.path().join("repo").try_into()?; - let repo = OmicronRepo::load_untrusted(&logctx.log, &repo_path).await?; - - let artifacts = repo.read_artifacts().await?; - assert_eq!( - artifacts.artifacts.len(), - // 3 artifacts added above + installinator_document.json. - 4, - "repo should contain exactly 4 artifacts: {artifacts:?}" - ); - - let mut artifacts_iter = artifacts.artifacts.into_iter(); - let artifact = artifacts_iter.next().unwrap(); - assert_eq!(artifact.name, "installinator_document", "artifact name"); - assert_eq!(artifact.version, "0.0.0".parse().unwrap(), "artifact version"); - assert_eq!( - artifact.kind, - ArtifactKind::from_known(KnownArtifactKind::InstallinatorDocument), - "artifact kind" - ); - assert_eq!( - artifact.target, "installinator_document-0.0.0.json", - "artifact target" - ); - - let artifact = artifacts_iter.next().unwrap(); - assert_eq!(artifact.name, "nexus", "artifact name"); - assert_eq!(artifact.version, "42.0.0".parse().unwrap(), "artifact version"); - assert_eq!( - artifact.kind, - ArtifactKind::from_known(KnownArtifactKind::GimletSp), - "artifact kind" - ); - assert_eq!( - artifact.target, "gimlet_sp-nexus-42.0.0.tar.gz", - "artifact target" - ); - - let artifact = artifacts_iter.next().unwrap(); - assert_eq!(artifact.name, "my-unknown-kind", "artifact name"); - assert_eq!(artifact.version, "0.1.0".parse().unwrap(), "artifact version"); - assert_eq!( - artifact.kind, - ArtifactKind::new("my_unknown_kind".to_owned()), - "artifact kind" - ); - assert_eq!( - artifact.target, "my_unknown_kind-my-unknown-kind-0.1.0.tar.gz", - "artifact target" - ); - - let artifact = artifacts_iter.next().unwrap(); - assert_eq!(artifact.name, "switch-sp", "artifact name"); - assert_eq!( - artifact.version, - "non-semver".parse().unwrap(), - "artifact version" - ); - assert_eq!( - artifact.kind, - ArtifactKind::from_known(KnownArtifactKind::SwitchSp), - "artifact kind" - ); - assert_eq!( - artifact.target, "switch_sp-switch-sp-non-semver.tar.gz", - "artifact target" - ); - - // Create an archive from the given path. - let archive_path = tempdir.path().join("archive.zip"); - let mut cmd = make_cmd_with_repo(tempdir.path(), &key); - cmd.arg("archive"); - cmd.arg(&archive_path); - cmd.assert().success(); - - // Extract the archive to a new directory. - let dest_path = tempdir.path().join("dest"); - let mut cmd = make_cmd_with_repo(tempdir.path(), &key); - cmd.arg("extract"); - cmd.arg(&archive_path); - cmd.arg(&dest_path); - - cmd.assert().success(); - - logctx.cleanup_successful(); - Ok(()) -} +use tufaceous_lib::Key; #[test] fn test_assemble_fake() -> Result<()> { @@ -351,11 +197,3 @@ fn make_cmd(key: &Key) -> Command { cmd } - -fn make_cmd_with_repo(tempdir: &Path, key: &Key) -> Command { - let mut cmd = make_cmd(key); - cmd.arg("--repo"); - cmd.arg(tempdir.join("repo")); - - cmd -} diff --git a/lib/src/artifact.rs b/lib/src/artifact.rs index 25bbe50..baf7bfb 100644 --- a/lib/src/artifact.rs +++ b/lib/src/artifact.rs @@ -48,7 +48,7 @@ pub struct AddArtifact { impl AddArtifact { /// Creates an [`AddArtifact`] from the provided source. - pub fn new( + pub(crate) fn new( kind: ArtifactKind, name: String, version: ArtifactVersion, @@ -58,36 +58,6 @@ impl AddArtifact { Self { kind, name, version, source, deployment_units } } - /// Creates an [`AddArtifact`] from the path, name and version. - /// - /// If the name is `None`, it is derived from the filename of the path - /// without matching extensions. - pub fn from_path( - kind: ArtifactKind, - name: Option, - version: ArtifactVersion, - path: Utf8PathBuf, - ) -> Result { - let name = match name { - Some(name) => name, - None => path - .file_name() - .context("artifact path is a directory")? - .split('.') - .next() - .expect("str::split has at least 1 element") - .to_owned(), - }; - - Ok(Self { - kind, - name, - version, - source: ArtifactSource::File(path), - deployment_units: ArtifactDeploymentUnits::Unknown, - }) - } - /// Returns the kind of artifact this is. pub fn kind(&self) -> &ArtifactKind { &self.kind diff --git a/lib/src/assemble/build.rs b/lib/src/assemble/build.rs index 6270c01..947c497 100644 --- a/lib/src/assemble/build.rs +++ b/lib/src/assemble/build.rs @@ -8,7 +8,9 @@ use chrono::{DateTime, Utc}; use tough::editor::signed::SignedRole; use tough::schema::Root; -use crate::{AddArtifact, Key, OmicronRepo, utils::merge_anyhow_list}; +use crate::{ + AddArtifact, Key, OmicronRepo, OmicronRepoEditor, utils::merge_anyhow_list, +}; use super::ArtifactManifest; @@ -110,17 +112,11 @@ impl OmicronRepoAssembler { crate::root::new_root(self.keys.clone(), self.expiry).await? } }; - let mut repository = OmicronRepo::initialize( - &self.log, - build_dir, - self.manifest.system_version.clone(), - self.keys.clone(), + let mut repository = OmicronRepoEditor::initialize( + build_dir.to_owned(), root, - self.expiry, - self.include_installinator_doc, + self.manifest.system_version.clone(), ) - .await? - .into_editor() .await?; // Add all the artifacts. diff --git a/lib/src/repository.rs b/lib/src/repository.rs index 14f66cc..34e698e 100644 --- a/lib/src/repository.rs +++ b/lib/src/repository.rs @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::BTreeSet; use std::num::NonZeroU64; use anyhow::{Context, Result, anyhow}; @@ -41,38 +41,9 @@ pub struct OmicronRepo { } impl OmicronRepo { - /// Initializes a new repository at the given path, writing it to disk. - pub async fn initialize( - log: &slog::Logger, - repo_path: &Utf8Path, - system_version: Version, - keys: Vec, - root: SignedRole, - expiry: DateTime, - // TODO-cleanup: This is a transitional option for v15 -> v16, meant to be used - // for testing. After v16 we can assume that all valid TUF repositories have - // installinator documents. - include_installinator_doc: bool, - ) -> Result { - let editor = OmicronRepoEditor::initialize( - repo_path.to_owned(), - root, - system_version, - ) - .await?; - - editor - .sign_and_finish(keys, expiry, include_installinator_doc) - .await - .context("error signing new repository")?; - - // In theory we "trust" the key we just used to sign this repository, - // but the code path is equivalent to `load_untrusted`. - Self::load_untrusted(log, repo_path).await - } - /// Loads a repository from the given path. /// + #[cfg(test)] /// This method enforces expirations. To load without expiration enforcement, use /// [`Self::load_ignore_expiration`]. pub async fn load( @@ -292,12 +263,6 @@ impl OmicronRepo { Ok(()) } - /// Converts `self` into an `OmicronRepoEditor`, which can be used to perform - /// modifications to the repository. - pub async fn into_editor(self) -> Result { - OmicronRepoEditor::new(self).await - } - /// Prepends the target digest to the name if using consistent snapshots. Returns both the /// digest and the filename. /// @@ -331,121 +296,7 @@ pub struct OmicronRepoEditor { } impl OmicronRepoEditor { - async fn new(repo: OmicronRepo) -> Result { - let artifacts = repo.read_artifacts().await?; - - // There should be a reference to an installinator document within - // artifacts_document. - let installinator_document = - match artifacts.artifacts.iter().find(|artifact| { - artifact.kind.to_known() - == Some(KnownArtifactKind::InstallinatorDocument) - }) { - Some(artifact) => { - repo.read_installinator_document(&artifact.target).await? - } - None => { - // With empty repos and those without a preexisting - // installinator document, we generate an empty one. - // - // This isn't quite correct for incrementally updated TUF - // repos created via `tufaceous init` and `tufaceous add`, - // but our production users don't use that functionality and - // those should be removed in the future. - InstallinatorDocument::empty( - artifacts.system_version.clone(), - ) - } - }; - - let artifacts_by_target_name = artifacts - .artifacts - .iter() - .map(|artifact| (artifact.target.as_str(), artifact)) - .collect::>(); - - let mut errors = Vec::new(); - - // TODO: In the future, it would be nice to extract deployment units - // from composite artifacts. But that would require parsing each file, - // and the code for that lives in Omicron under update-common. - // - // For now we settle for treating all artifacts as single-unit ones. - let mut data_builder = - DeploymentUnitMapBuilder::new(DeploymentUnitScope::Repository); - - let existing_target_names = repo - .repo - .targets() - .signed - .targets_iter() - .filter_map(|(name, target)| { - let target_name = name.resolved().to_string(); - if target_name == ArtifactsDocument::FILE_NAME { - // The artifacts document does not refer to itself. - return None; - } - - let hash_bytes = <[u8; 32]>::try_from( - target.hashes.sha256.clone().into_vec(), - ) - .expect("SHA-256 hash should be exactly 32 bytes"); - let hash = ArtifactHash(hash_bytes); - - let Some(artifact) = - artifacts_by_target_name.get(target_name.as_str()) - else { - errors.push(anyhow!( - "artifact `{}` not found in {}", - target_name, - ArtifactsDocument::FILE_NAME - )); - return None; - }; - - let Ok(()) = data_builder.insert(DeploymentUnitData { - name: artifact.name.to_owned(), - version: artifact.version.clone(), - kind: artifact.kind.clone(), - hash, - }) else { - errors.push(anyhow!( - "failed to add deployment unit for artifact `{}`", - target_name - )); - return None; - }; - - Some(target_name) - }) - .collect::>(); - - // If any errors were found, return them. - if !errors.is_empty() { - return Err(merge_anyhow_list(errors)); - } - - let editor = RepositoryEditor::from_repo( - repo.repo_path - .join("metadata") - .join(format!("{}.root.json", repo.repo.root().signed.version)), - repo.repo, - ) - .await?; - - Ok(Self { - editor, - repo_path: repo.repo_path, - artifacts, - installinator_document, - existing_target_names, - existing_deployment_units: DeploymentUnitMapBuilder::new( - DeploymentUnitScope::Repository, - ), - }) - } - - async fn initialize( + pub(crate) async fn initialize( repo_path: Utf8PathBuf, root: SignedRole, system_version: Version, @@ -773,19 +624,12 @@ mod tests { let keys = vec![Key::generate_ed25519().unwrap()]; let expiry = Utc::now() + Days::new(1); let root = crate::root::new_root(keys.clone(), expiry).await.unwrap(); - let mut repo = OmicronRepo::initialize( - &logctx.log, - tempdir.path(), - "0.0.0".parse().unwrap(), - keys, + let mut repo = OmicronRepoEditor::initialize( + tempdir.path().to_owned(), root, - expiry, - true, + "0.0.0".parse().unwrap(), ) .await - .unwrap() - .into_editor() - .await .unwrap(); // Targets are uniquely identified by their kind/name/version triple; From 06a883b9b5bad39a0dede8203e5ebdaed2e542ba Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 14 Aug 2025 00:45:20 +0000 Subject: [PATCH 2/2] clippy Created using spr 1.3.6-beta.1 --- lib/src/repository.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/src/repository.rs b/lib/src/repository.rs index 34e698e..f9c99fd 100644 --- a/lib/src/repository.rs +++ b/lib/src/repository.rs @@ -220,7 +220,7 @@ impl OmicronRepo { /// Regardless of this roadblock, we don't want to foreclose that option /// forever, so this code uses zip rather than having to deal with a /// migration in the future. - pub fn archive(&self, output_path: &Utf8Path) -> Result<()> { + pub(crate) fn archive(&self, output_path: &Utf8Path) -> Result<()> { let mut builder = ArchiveBuilder::new(output_path.to_owned())?; let metadata_dir = self.repo_path.join("metadata"); @@ -277,10 +277,8 @@ impl OmicronRepo { } } -/// An [`OmicronRepo`] than can be edited. -/// -/// Created by [`OmicronRepo::into_editor`]. -pub struct OmicronRepoEditor { +/// An editable TUF repository, used to construct new ones. +pub(crate) struct OmicronRepoEditor { editor: RepositoryEditor, repo_path: Utf8PathBuf, artifacts: ArtifactsDocument, @@ -327,7 +325,7 @@ impl OmicronRepoEditor { } /// Adds an artifact to the repository. - pub fn add_artifact( + pub(crate) fn add_artifact( &mut self, new_artifact: &AddArtifact, ) -> Result { @@ -420,8 +418,9 @@ impl OmicronRepoEditor { new_artifact.finalize(&mut self.editor) } - /// Consumes self, signing the repository and writing out this repository to disk. - pub async fn sign_and_finish( + /// Consumes self, signing the repository and writing out this repository to + /// disk. + pub(crate) async fn sign_and_finish( mut self, keys: Vec, expiry: DateTime,