Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions artifact/src/installinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,6 @@ use crate::ArtifactVersion;
/// own backwards compatibility code, which some temporary code in this module
/// supports:
///
/// - `name` is accepted as an alias for [`InstallinatorArtifact::file_name`].
/// We renamed this field in Tufaceous v2 for clarity.
/// - `ControlPlane` is an accepted variant for [`InstallinatorArtifactKind`].
/// This indicates to Installinator that an older version of Wicket was used
/// to read the repository, and that it will need to fetch the composite
Expand Down Expand Up @@ -82,8 +80,11 @@ pub struct InstallinatorArtifact {
pub hash: ArtifactHash,
/// A file name without directory separators; not necessarily the target
/// name.
// (temporary alias; see "v1 compatibility notes" above)
#[serde(alias = "name")]
///
/// This is named `name` in the serialized document in order to retain
/// compatibility with older versions of Installinator, allowing us to
/// downgrade clean-slated development environments with mupdate.
#[serde(rename = "name")]
pub file_name: String,
}

Expand Down
77 changes: 71 additions & 6 deletions lib/src/repo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use tough::schema::Target;
use tufaceous_artifact::Artifact;
use tufaceous_artifact::ArtifactHash;
use tufaceous_artifact::ArtifactSet;
use tufaceous_artifact::ArtifactVersion;
use tufaceous_artifact::KnownArtifactTags;
use tufaceous_artifact::Metadata;
use tufaceous_artifact::artifact_set::GetError;
Expand All @@ -52,6 +53,15 @@ pub struct Repository {
artifact_data: BTreeMap<Artifact, ArtifactData>,
metadata: BTreeMap<String, String>,

/// If this is a v1 repo, this contains the hash of the original
/// Installinator document (instead of the converted-to-v2 document).
installinator_v1_document: Option<ArtifactHash>,
/// If this is a v1 repo, this contains artifacts that allow reading
/// the original Installinator document (instead of the converted-to-v2
/// document), as well as the original control plane tarball referenced by
/// that document.
installinator_v1_artifacts: Vec<InstallinatorV1Artifact>,

// These are set directly by the ZIP archive convenience methods in the
// loader module.
// TODO after v2 merge: Move the loader module under this repo module so
Expand Down Expand Up @@ -126,6 +136,10 @@ impl Repository {
artifacts: partial.artifacts,
artifact_data: partial.artifact_data,
metadata: BTreeMap::new(),
installinator_v1_document: partial
.installinator_v1_document,
installinator_v1_artifacts: partial
.installinator_v1_artifacts,
archive_path: None,
archive_sha256: None,
});
Expand Down Expand Up @@ -156,6 +170,8 @@ impl Repository {
artifacts,
artifact_data,
metadata,
installinator_v1_document: None,
installinator_v1_artifacts: Vec::new(),
archive_path: None,
archive_sha256: None,
})
Expand Down Expand Up @@ -290,7 +306,11 @@ impl Repository {
tags: &KnownArtifactTags,
) -> Result<ArtifactHandle, GetError> {
let artifact = self.artifacts.get_only(tags)?.clone();
Ok(ArtifactHandle { artifact, repo: Arc::clone(self) })
Ok(ArtifactHandle {
artifact,
repo: Arc::clone(self),
installinator_v1_target_name: None,
})
}

/// Returns an iterator of [`ArtifactHandle`]s for every artifact
Expand All @@ -300,10 +320,38 @@ impl Repository {
/// together in a struct as a convenience. The repository must be wrapped in
/// [`Arc`] to call this method.
pub fn handles(self: &Arc<Self>) -> impl Iterator<Item = ArtifactHandle> {
self.artifacts
.iter()
.cloned()
.map(|artifact| ArtifactHandle { artifact, repo: Arc::clone(self) })
self.artifacts.iter().cloned().map(|artifact| ArtifactHandle {
artifact,
repo: Arc::clone(self),
installinator_v1_target_name: None,
})
}

/// If this was a v1 repository, returns the hash of the original
/// Installinator document (instead of the converted-to-v2 document).
pub fn installinator_v1_document(&self) -> Option<ArtifactHash> {
self.installinator_v1_document.as_ref().copied()
}

/// If this was a v1 repository, returns an iterator of [`ArtifactHandle`]
/// that can be used to read the original Installinator document or control
/// plane tarball.
///
/// Note that the `Artifact` returned by the handle has no tags as it is not
/// a valid artifact.
pub fn installinator_v1_handles(
self: &Arc<Self>,
) -> impl Iterator<Item = ArtifactHandle> {
self.installinator_v1_artifacts.iter().map(|artifact| ArtifactHandle {
artifact: Artifact {
version: artifact.version.clone(),
tags: BTreeMap::new(),
hash: artifact.hash,
length: artifact.length,
},
repo: Arc::clone(self),
installinator_v1_target_name: Some(artifact.target_name.clone()),
})
}

/// Reads all targets in the repository and verifies they have the correct
Expand Down Expand Up @@ -396,6 +444,14 @@ impl ArtifactData {
}
}

#[derive(Debug, Clone)]
struct InstallinatorV1Artifact {
version: ArtifactVersion,
hash: ArtifactHash,
length: u64,
target_name: String,
}

/// An [`Artifact`] and the [`Repository`] it belongs to, for convenience to
/// code that works at the artifact level but needs to read the artifact data
/// from the repository.
Expand All @@ -405,6 +461,11 @@ impl ArtifactData {
pub struct ArtifactHandle {
artifact: Artifact,
repo: Arc<Repository>,

/// If `Some`, this is not a "real" artifact but instead was generated
/// by [`Repository::installinator_v1_artifacts`], and `artifact.tags` is
/// empty.
installinator_v1_target_name: Option<String>,
}

impl ArtifactHandle {
Expand All @@ -421,7 +482,11 @@ impl ArtifactHandle {

/// Read this artifact from its repository.
pub async fn stream(&self) -> Result<TargetStream, Error> {
self.repo.read_artifact(&self.artifact).await
if let Some(target_name) = &self.installinator_v1_target_name {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needing to do this makes me slightly nervous - this means any call site that uses an ArtifactHandle has to know to check installinator_v1_target_name. Although since it's private, maybe this is the only call site and this isn't a huge deal?

Would it make sense to, instead of adding a separate installinator_v1_target_name field, change ArtifactHandle::artifact to be an enum containing either Artifact or the v1 target name String?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had initially considered the enum, but we still need to create an Artifact to support the fn artifact API, and that can't be a newly-generated artifact because we return a reference.

This module is the only possible call site for this method, ArtifactHandle is intentionally opaque.

self.repo.read_target(target_name).await
} else {
self.repo.read_artifact(&self.artifact).await
}
}
}

Expand Down
22 changes: 22 additions & 0 deletions lib/src/repo/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ use crate::error::Error;
use crate::error::ErrorKind;
use crate::error::try_path;
use crate::repo::ArtifactData;
use crate::repo::InstallinatorV1Artifact;
use crate::repo::read_target;
use crate::repo::read_target_json;
use crate::repo::read_target_vec;
Expand All @@ -66,6 +67,8 @@ pub(super) struct PartialRepository {
pub(super) system_version: Version,
pub(super) artifacts: ArtifactSet,
pub(super) artifact_data: BTreeMap<Artifact, ArtifactData>,
pub(super) installinator_v1_document: Option<ArtifactHash>,
pub(super) installinator_v1_artifacts: Vec<InstallinatorV1Artifact>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a very small structural nit, but: would this be slightly cleaner as one field? installinator_v1: Option<InstallinatorV1Details> or somesuch.

My initial driver for this was "would it make sense to have a document of None with a nonempty set of v1 artifacts", which I think the answer is no? Unless we had a repo with v1 artifacts but no installinator document, which I don't think we should have and wouldn't be usable if we did?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had thought about this but I wasn't really happy with how the code was written. This implementation felt clearer to review.

}

impl PartialRepository {
Expand Down Expand Up @@ -98,6 +101,8 @@ pub(crate) async fn from_loaded(
system_version,
artifacts: ArtifactSet::default(),
artifact_data: BTreeMap::new(),
installinator_v1_document: None,
installinator_v1_artifacts: Vec::new(),
};
let mut installinator_document = None;
for V1Artifact { version, kind, target } in v1_artifacts {
Expand Down Expand Up @@ -214,6 +219,14 @@ pub(crate) async fn from_loaded(
}

V1KnownArtifactKind::ControlPlane => {
partial.installinator_v1_artifacts.push(
InstallinatorV1Artifact {
version,
hash,
length,
target_name: target.clone(),
},
);
CompositeArtifact::unpack(tuf_repo, target)
.await?
.read_control_plane(&mut partial)
Expand All @@ -226,6 +239,15 @@ pub(crate) async fn from_loaded(
// for the v2 artifacts once all of the potential artifacts are
// extracted.
V1KnownArtifactKind::InstallinatorDocument => {
partial.installinator_v1_document = Some(hash);
partial.installinator_v1_artifacts.push(
InstallinatorV1Artifact {
version: version.clone(),
hash,
length,
target_name: target.clone(),
},
);
installinator_document = Some((version, target));
continue;
}
Expand Down
67 changes: 61 additions & 6 deletions lib/tests/v1_compatibility.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@
// 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::BTreeSet;
use std::sync::Arc;

use camino::Utf8Path;
use futures_util::TryStreamExt;
use tufaceous::ExpirationEnforcement;
use tufaceous::RepositoryLoader;
use tufaceous::error::Error;
use tufaceous_artifact::ArtifactSet;
use tufaceous_artifact::InstallinatorDocument;
use tufaceous_artifact::KnownArtifactTags;
use tufaceous_artifact::OsBoard;
use tufaceous_artifact::OsPhase1Tags;
Expand All @@ -25,12 +30,14 @@ async fn v1_fake() -> Result<(), Error> {
let path = Utf8Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/data/v1-fake.zip");

let repo = RepositoryLoader::new()
.expiration_enforcement(ExpirationEnforcement::Unsafe)
.unsafe_blindly_trust_repo()
.v1_compatibility(true)
.load_zip_path(path, &log)
.await?;
let repo = Arc::new(
RepositoryLoader::new()
.expiration_enforcement(ExpirationEnforcement::Unsafe)
.unsafe_blindly_trust_repo()
.v1_compatibility(true)
.load_zip_path(path, &log)
.await?,
);
let parallelism = std::thread::available_parallelism().unwrap().get();
repo.verify_targets(parallelism).await?;

Expand Down Expand Up @@ -77,6 +84,54 @@ async fn v1_fake() -> Result<(), Error> {
// And there should be no unexpected artifacts:
assert_eq!(&seen, repo.artifacts());

// The Installinator document should be regenerated to only reference
// v2 artifacts.
let mut hashes = repo
.artifacts()
.iter()
.map(|artifact| artifact.hash)
.collect::<BTreeSet<_>>();
let artifact = repo
.artifacts()
.get_only(&KnownArtifactTags::InstallinatorDocument)
.unwrap();
let doc_json = repo
.read_artifact(artifact)
.await?
.map_ok(Vec::from)
.try_concat()
.await?;
let doc: InstallinatorDocument = serde_json::from_slice(&doc_json).unwrap();
for artifact in doc.artifacts {
assert!(
hashes.contains(&artifact.hash),
"converted Installinator document references non-existent hash {}",
artifact.hash
);
}

// The original Installinator document should be accessible, and may
// reference either v2 or v1-only artifacts.
hashes.extend(
repo.installinator_v1_handles().map(|handle| handle.artifact().hash),
);
let handle = repo
.installinator_v1_handles()
.find(|handle| {
handle.artifact().hash == repo.installinator_v1_document().unwrap()
})
.unwrap();
let doc_json =
handle.stream().await?.map_ok(Vec::from).try_concat().await?;
let doc: InstallinatorDocument = serde_json::from_slice(&doc_json).unwrap();
for artifact in doc.artifacts {
assert!(
hashes.contains(&artifact.hash),
"original Installinator document references non-existent hash {}",
artifact.hash
);
}

Ok(())
}

Expand Down
Loading