Skip to content

Commit 6ff9652

Browse files
authored
Merge pull request #746 from imageworks/spk-rm-corner-case
Fix remove_package not removing build spec
2 parents feadba9 + d34f98e commit 6ff9652

File tree

6 files changed

+192
-54
lines changed

6 files changed

+192
-54
lines changed

crates/spk-cli/group2/src/cmd_remove.rs

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -101,31 +101,45 @@ async fn remove_build(
101101
repo_name: &str,
102102
repo: &storage::RepositoryHandle,
103103
pkg: &BuildIdent,
104+
) -> Result<()> {
105+
remove_build_impl(repo_name, repo, pkg, 0).await
106+
}
107+
108+
async fn remove_build_impl(
109+
repo_name: &str,
110+
repo: &storage::RepositoryHandle,
111+
pkg: &BuildIdent,
112+
build_index: usize,
104113
) -> Result<()> {
105114
let repo_name = repo_name.bold();
106115
let pretty_pkg = pkg.format_ident();
107-
let (recipe, package) = tokio::join!(repo.remove_recipe(pkg.base()), repo.remove_package(pkg),);
116+
let (mut recipe, package) =
117+
tokio::join!(repo.remove_recipe(pkg.base()), repo.remove_package(pkg),);
108118
// First inform on the things that actually happened.
109119
if recipe.is_ok() {
110120
tracing::info!("removed recipe {pretty_pkg: >25} from {repo_name}")
111121
}
112122
if package.is_ok() {
113123
tracing::info!("removed build {pretty_pkg: >25} from {repo_name}")
114124
}
125+
126+
// When deleting multiple builds of the same package (by calling
127+
// `remove_build_impl` multiple times for the same package), the recipe
128+
// will get deleted on the first call and then it will be "not found" for
129+
// subsequent calls. To avoid warning about it not being found, transmute
130+
// PackageNotFoundError into a success.
131+
if build_index > 0 && matches!(&recipe, Err(err) if err.is_package_not_found()) {
132+
recipe = Ok(());
133+
}
134+
115135
// Treat "not found" problems as warnings unless both parts were not
116136
// found.
117137
let recipe_not_found = matches!(
118-
recipe,
119-
Err(spk_storage::Error::SpkValidatorsError(
120-
spk_schema::validators::Error::PackageNotFoundError(_)
121-
))
122-
);
138+
&recipe,
139+
Err(err) if err.is_package_not_found());
123140
let pkg_not_found = matches!(
124-
package,
125-
Err(spk_storage::Error::SpkValidatorsError(
126-
spk_schema::validators::Error::PackageNotFoundError(_)
127-
))
128-
);
141+
&package,
142+
Err(err) if err.is_package_not_found());
129143
if recipe_not_found && pkg_not_found {
130144
// Both parts were not found; this is a hard error so don't
131145
// emit warnings.
@@ -152,22 +166,34 @@ async fn remove_all(
152166
repo: &storage::RepositoryHandle,
153167
pkg: &VersionIdent,
154168
) -> Result<()> {
155-
let pretty_pkg = pkg.format_ident();
156-
for build in repo.list_package_builds(pkg).await? {
157-
if build.is_embedded() {
158-
// Don't attempt to remove an embedded package; the stub
159-
// will be removed when removing its provider.
160-
continue;
161-
}
162-
remove_build(repo_name, repo, &build).await?
169+
let mut deleted_something = false;
170+
171+
for (build_index, build) in repo
172+
.list_package_builds(pkg)
173+
.await?
174+
.iter()
175+
// Don't attempt to remove an embedded package; the stub
176+
// will be removed when removing its provider.
177+
.filter(|build| !build.is_embedded())
178+
.enumerate()
179+
{
180+
remove_build_impl(repo_name, repo, build, build_index).await?;
181+
deleted_something = true;
163182
}
183+
164184
let repo_name = repo_name.bold();
185+
let pretty_pkg = pkg.format_ident();
165186
match repo.remove_recipe(pkg).await {
166187
Ok(()) => tracing::info!("removed recipe {pretty_pkg: >25} from {repo_name}"),
167188
Err(spk_storage::Error::SpkValidatorsError(
168189
spk_schema::validators::Error::PackageNotFoundError(_),
169190
)) => {
170-
tracing::warn!("spec {pretty_pkg: >25} not found in {repo_name}")
191+
// If builds were found above, `remove_build` will have also
192+
// attempted to delete the recipe, so don't warn about not finding
193+
// it now.
194+
if !deleted_something {
195+
tracing::warn!("spec {pretty_pkg: >25} not found in {repo_name}")
196+
}
171197
}
172198
Err(err) => return Err(err.into()),
173199
}

crates/spk-schema/crates/foundation/src/ident_build/build.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ pub struct EmbeddedSourcePackage {
3737
pub components: BTreeSet<Component>,
3838
}
3939

40+
impl EmbeddedSourcePackage {
41+
pub const EMBEDDED_BY_PREFIX: &str = "embedded-by-";
42+
}
43+
4044
/// An embedded package's source (if known).
4145
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
4246
pub enum EmbeddedSource {
@@ -49,7 +53,8 @@ impl MetadataPath for EmbeddedSource {
4953
fn metadata_path(&self) -> RelativePathBuf {
5054
match self {
5155
package @ EmbeddedSource::Package { .. } => RelativePathBuf::from(format!(
52-
"embedded-by-{}",
56+
"{}{}",
57+
EmbeddedSourcePackage::EMBEDDED_BY_PREFIX,
5358
// Encode the parent ident into base32 to have a unique value
5459
// per unique parent that is a valid filename. The trailing
5560
// '=' are not allowed in tag names (use NOPAD).

crates/spk-schema/crates/validators/src/error.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,11 @@ pub enum Error {
4747
#[error("Existing file was {0:?}: {1:?}")]
4848
ExistingFileAltered(Box<DiffMode>, RelativePathBuf),
4949
}
50+
51+
impl Error {
52+
/// Return true if this is a `PackageNotFound` error.
53+
#[inline]
54+
pub fn is_package_not_found(&self) -> bool {
55+
matches!(self, Error::PackageNotFoundError(_))
56+
}
57+
}

crates/spk-storage/src/error.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@ pub enum Error {
4343
String(String),
4444
}
4545

46+
impl Error {
47+
/// Return true if this is a `PackageNotFound` error.
48+
#[inline]
49+
pub fn is_package_not_found(&self) -> bool {
50+
match self {
51+
Error::SpkValidatorsError(e) => e.is_package_not_found(),
52+
_ => false,
53+
}
54+
}
55+
}
56+
4657
impl From<String> for Error {
4758
fn from(err: String) -> Error {
4859
Error::String(err)

crates/spk-storage/src/storage/spfs.rs

Lines changed: 120 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use spk_schema::foundation::name::{PkgName, PkgNameBuf, RepositoryName, Reposito
2121
use spk_schema::foundation::version::{parse_version, Version};
2222
use spk_schema::ident::VersionIdent;
2323
use spk_schema::ident_build::parsing::embedded_source_package;
24-
use spk_schema::ident_build::EmbeddedSource;
24+
use spk_schema::ident_build::{EmbeddedSource, EmbeddedSourcePackage};
2525
use spk_schema::ident_ops::TagPath;
2626
use spk_schema::{AnyIdent, BuildIdent, FromYaml, Package, Recipe, Spec, SpecRecipe};
2727
use tokio::io::AsyncReadExt;
@@ -220,13 +220,33 @@ impl Storage for SPFSRepository {
220220
type Package = Spec;
221221

222222
async fn get_concrete_package_builds(&self, pkg: &VersionIdent) -> Result<HashSet<BuildIdent>> {
223-
let base = self.build_package_tag(pkg)?;
224-
let builds: HashSet<_> = self
225-
.ls_tags(&base)
226-
.await
223+
// It is possible for a `spk/spec/pkgname/1.0.0/BUILDKEY` tag to
224+
// exist without a corresponding `spk/spk/pkgname/1.0.0/BUILDKEY`
225+
// tag. In this scenario, "pkgname" will appear in the results of
226+
// `list_packages` and `list_package_versions`, because those look at
227+
// the `spk/spec/...` spfs tag tree, i.e., this package will appear
228+
// in the output of `spk ls`. In order to make it possible to locate
229+
// the build spec, e.g., for `spk rm pkgname/1.0.0` to work, this
230+
// method needs to return a union of all the build tags of both the
231+
// `spk/spec/` and `spk/pkg/` tag trees.
232+
let spec_base = self.build_spec_tag(pkg);
233+
let package_base = self.build_package_tag(pkg);
234+
235+
let spec_tags = self.ls_tags(&spec_base);
236+
let package_tags = self.ls_tags(&package_base);
237+
238+
let (spec_tags, package_tags) = tokio::join!(spec_tags, package_tags);
239+
240+
let builds: HashSet<_> = spec_tags
227241
.into_iter()
242+
.chain(package_tags.into_iter())
228243
.filter_map(|entry| match entry {
229-
Ok(EntryType::Tag(name)) => Some(name),
244+
Ok(EntryType::Tag(name))
245+
if !name.starts_with(EmbeddedSourcePackage::EMBEDDED_BY_PREFIX) =>
246+
{
247+
Some(name)
248+
}
249+
Ok(EntryType::Tag(_)) => None,
230250
Ok(EntryType::Folder(name)) => Some(name),
231251
Err(_) => None,
232252
})
@@ -261,7 +281,7 @@ impl Storage for SPFSRepository {
261281
Err(_) => None,
262282
})
263283
.filter_map(|b| {
264-
b.strip_prefix("embedded-by-")
284+
b.strip_prefix(EmbeddedSourcePackage::EMBEDDED_BY_PREFIX)
265285
.and_then(|encoded_ident| {
266286
data_encoding::BASE32_NOPAD
267287
.decode(encoded_ident.as_bytes())
@@ -317,7 +337,7 @@ impl Storage for SPFSRepository {
317337
package: &<Self::Recipe as spk_schema::Recipe>::Output,
318338
components: &HashMap<Component, spfs::encoding::Digest>,
319339
) -> Result<()> {
320-
let tag_path = self.build_package_tag(package.ident())?;
340+
let tag_path = self.build_package_tag(package.ident());
321341

322342
// We will also publish the 'run' component in the old style
323343
// for compatibility with older versions of the spk command.
@@ -456,28 +476,97 @@ impl Storage for SPFSRepository {
456476
}
457477

458478
async fn remove_package_from_storage(&self, pkg: &BuildIdent) -> Result<()> {
459-
for tag_spec in
460-
with_cache_policy!(self, CachePolicy::BypassCache, { self.lookup_package(pkg) })
461-
.await?
462-
.tags()
463-
{
464-
match self.inner.remove_tag_stream(tag_spec).await {
465-
Err(spfs::Error::UnknownReference(_)) => (),
466-
res => res?,
479+
// The three things this method is responsible for deleting are:
480+
//
481+
// 1. Component build tags like: `spk/pkg/example/4.2.1/GMTG3CXY/build`.
482+
// 2. Legacy build tags like : `spk/pkg/example/4.2.1/GMTG3CXY`.
483+
// 3. Build recipe tags like : `spk/spec/example/4.2.1/GMTG3CXY`.
484+
//
485+
// It should make an effort to delete all three types before returning
486+
// any failures.
487+
488+
let component_tags = async {
489+
let mut deleted_something = false;
490+
491+
for tag_spec in
492+
with_cache_policy!(self, CachePolicy::BypassCache, { self.lookup_package(pkg) })
493+
.await?
494+
.tags()
495+
{
496+
match self.inner.remove_tag_stream(tag_spec).await {
497+
Err(spfs::Error::UnknownReference(_)) => (),
498+
Ok(_) => deleted_something = true,
499+
res => res?,
500+
};
467501
}
468-
}
469-
// because we double-publish packages to be visible/compatible
470-
// with the old repo tag structure, we must also try to remove
471-
// the legacy version of the tag after removing the discovered
472-
// as it may still be there and cause the removal to be ineffective
473-
if let Ok(legacy_tag) = spfs::tracking::TagSpec::parse(self.build_package_tag(pkg)?) {
474-
match self.inner.remove_tag_stream(&legacy_tag).await {
475-
Err(spfs::Error::UnknownReference(_)) => (),
476-
res => res?,
502+
Ok::<_, Error>(deleted_something)
503+
};
504+
505+
let legacy_tags = async {
506+
// because we double-publish packages to be visible/compatible
507+
// with the old repo tag structure, we must also try to remove
508+
// the legacy version of the tag after removing the discovered
509+
// as it may still be there and cause the removal to be ineffective
510+
let mut deleted_something = false;
511+
512+
if let Ok(legacy_tag) = spfs::tracking::TagSpec::parse(self.build_package_tag(pkg)) {
513+
match self.inner.remove_tag_stream(&legacy_tag).await {
514+
Err(spfs::Error::UnknownReference(_)) => (),
515+
Ok(_) => deleted_something = true,
516+
res => res?,
517+
}
518+
};
519+
Ok::<_, Error>(deleted_something)
520+
};
521+
522+
let build_recipe_tags = async {
523+
let tag_path = self.build_spec_tag(pkg);
524+
let tag_spec = spfs::tracking::TagSpec::parse(&tag_path)?;
525+
match self.inner.remove_tag_stream(&tag_spec).await {
526+
Err(spfs::Error::UnknownReference(_)) => Err(Error::SpkValidatorsError(
527+
spk_schema::validators::Error::PackageNotFoundError(pkg.to_any()),
528+
)),
529+
Err(err) => Err(err.into()),
530+
Ok(_) => Ok(true),
477531
}
478-
}
532+
};
533+
534+
let (component_tags_result, legacy_tags_result, build_recipe_tags_result) =
535+
tokio::join!(component_tags, legacy_tags, build_recipe_tags);
536+
537+
// Still invalidate caches in case some of individual deletions were
538+
// successful.
479539
self.invalidate_caches();
480-
Ok(())
540+
541+
// If any of the three sub-tasks successfully deleted something *and*
542+
// the only failures otherwise was `PackageNotFoundError`, then return
543+
// success. Since something was deleted then the package was
544+
// technically "found."
545+
[
546+
component_tags_result,
547+
build_recipe_tags_result,
548+
// Check legacy tags last because errors deleting legacy tags are
549+
// less important.
550+
legacy_tags_result,
551+
]
552+
.into_iter()
553+
.fold(Ok::<_, Error>(false), |acc, x| match (acc, x) {
554+
// Preserve the first non-PackageNotFoundError encountered.
555+
(Err(err), _) if !err.is_package_not_found() => Err(err),
556+
// Incoming error is not PackageNotFoundError.
557+
(_, Err(err)) if !err.is_package_not_found() => Err(err),
558+
// Successes merge with successes and retain "deleted
559+
// something" if either did.
560+
(Ok(x), Ok(y)) => Ok(x || y),
561+
// Having successfully deleted something trumps
562+
// `PackageNotFound`.
563+
(Ok(true), Err(err)) if err.is_package_not_found() => Ok(true),
564+
(Err(err), Ok(true)) if err.is_package_not_found() => Ok(true),
565+
// Otherwise, keep the prevailing error.
566+
(Err(err), _) => Err(err),
567+
(_, Err(err)) => Err(err),
568+
})
569+
.map(|_| ())
481570
}
482571
}
483572

@@ -699,7 +788,7 @@ impl Repository for SPFSRepository {
699788
let components = stored.into_components();
700789
for (name, tag_spec) in components.into_iter() {
701790
let tag = self.inner.resolve_tag(&tag_spec).await?;
702-
let new_tag_path = self.build_package_tag(&build)?.join(name.to_string());
791+
let new_tag_path = self.build_package_tag(&build).join(name.to_string());
703792
let new_tag_spec = spfs::tracking::TagSpec::parse(&new_tag_path)?;
704793

705794
// NOTE(rbottriell): this copying process feels annoying
@@ -855,7 +944,7 @@ impl SPFSRepository {
855944
/// (with or without package components)
856945
async fn lookup_package(&self, pkg: &BuildIdent) -> Result<StoredPackage> {
857946
use spfs::tracking::TagSpec;
858-
let tag_path = self.build_package_tag(pkg)?;
947+
let tag_path = self.build_package_tag(pkg);
859948
let tag_specs: HashMap<Component, TagSpec> = self
860949
.ls_tags(&tag_path)
861950
.await
@@ -881,15 +970,15 @@ impl SPFSRepository {
881970
}
882971

883972
/// Construct an spfs tag string to represent a binary package layer.
884-
fn build_package_tag<T>(&self, pkg: &T) -> Result<RelativePathBuf>
973+
fn build_package_tag<T>(&self, pkg: &T) -> RelativePathBuf
885974
where
886975
T: TagPath,
887976
{
888977
let mut tag = RelativePathBuf::from("spk");
889978
tag.push("pkg");
890979
tag.push(pkg.tag_path());
891980

892-
Ok(tag)
981+
tag
893982
}
894983

895984
/// Construct an spfs tag string to represent a spec file blob.

crates/spk-storage/src/storage/spfs_test.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,7 @@ async fn test_upgrade_changes_tags(tmpdir: tempfile::TempDir) {
8181

8282
// publish an "old style" package spec and build
8383
let mut old_path =
84-
spfs::tracking::TagSpec::from_str(repo.build_package_tag(&ident).unwrap().as_str())
85-
.unwrap();
84+
spfs::tracking::TagSpec::from_str(repo.build_package_tag(&ident).as_str()).unwrap();
8685
spfs_repo
8786
.push_tag(&old_path, &spfs::encoding::EMPTY_DIGEST.into())
8887
.await

0 commit comments

Comments
 (0)