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
4 changes: 2 additions & 2 deletions catalogue/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,11 @@ and `publisher` (so existing installs keep their pin and keep running), set
`metadata_url` (the tombstone is not installable), and delete the old
`apps/<old-id>/` detail dir. The full new entry lives under the new id, and the
catalogue is re-signed. A `pilotctl` that knows the fields (this repo's, from
the release after v1.13.10) then:
v1.14.1) then:

- omits the old id from `catalogue` (text and `--json`);
- on `install <old>`, warns and installs `renamed_to` instead. With
`--version` (a pin, as the managed-fleet reconcile passes) it refuses, naming
`--version` (a pin) it refuses, naming
the new id: a pin names a release of one app, and the old id has none;
- reports an installed old id in `outdated` as `renamed` (AVAILABLE is the new
id). `upgrade --all` skips it, since the hourly updater runs it and the new id
Expand Down
15 changes: 13 additions & 2 deletions cmd/pilotctl/appstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -779,7 +779,7 @@ func cmdAppStoreUninstall(args []string) {
// crafted id (e.g. "../../something") can't RemoveAll outside the
// install tree. Same guard the install/verify paths use.
root := appStoreRoot()
dir, err := resolveUnder(root, appID)
dir, err := resolveAppDir(root, appID)
if err != nil {
fatalHint("invalid_argument",
"app ids are single directory names; try `pilotctl appstore list`",
Expand Down Expand Up @@ -1132,6 +1132,17 @@ func resolveUnder(base, rel string) (string, error) {
return joined, nil
}

// resolveAppDir confines a user-supplied app id to one directory directly
// under root. resolveUnder alone accepts "." (root itself) and nested paths,
// so `appstore uninstall .` would resolve to the install root and remove
// every installed app.
func resolveAppDir(root, appID string) (string, error) {
if appID == "." || appID == ".." || strings.ContainsAny(appID, `/\`) {
return "", fmt.Errorf("app id must be a single directory name")
}
return resolveUnder(root, appID)
}

func cmdAppStoreInstall(args []string) {
if len(args) < 1 {
fatalHint("invalid_argument",
Expand Down Expand Up @@ -1914,7 +1925,7 @@ func cmdAppStoreCaps(args []string) {
// appID is user input; confine it to a single entry under the app
// store root so a crafted id (e.g. "../../etc") can't read or open
// files outside the install tree.
appDir, err := resolveUnder(appStoreRoot(), appID)
appDir, err := resolveAppDir(appStoreRoot(), appID)
if err != nil {
fatalHint("invalid_argument",
"app ids are single directory names; try `pilotctl appstore list`",
Expand Down
4 changes: 2 additions & 2 deletions cmd/pilotctl/appstore_catalogue.go
Original file line number Diff line number Diff line change
Expand Up @@ -418,8 +418,8 @@ func resolveInstallTargetVersion(target, wantVersion string) (string, installSou
// A pin names a release of one app. The old id has no releases,
// and following the rename would install a different app id (with
// its own publisher key) than the one the caller checks for
// afterwards — the managed-fleet reconcile would reinstall it
// every cycle. Fail closed and name the new id instead.
// afterwards, so a caller that re-checks would reinstall it every
// time. Fail closed and name the new id instead.
return "", installSourceCatalogue, fmt.Errorf("%w: %s was renamed to %s and has no releases of its own; pin %s instead",
ErrCatalogueVersionUnavailable, target, e.RenamedTo, e.RenamedTo)
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/pilotctl/appstore_nextsteps.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ func loadNextStepsGraph(appID string) *nextStepsGraph {
// `pilotctl appstore call ../../etc/x ...` would resolve outside the install
// root and we would happily read (and print) whatever we found. resolveUnder
// is the same containment guard install and the supervisor use.
dir, err := resolveUnder(appStoreRoot(), appID)
dir, err := resolveAppDir(appStoreRoot(), appID)
if err != nil {
return nil
}
Expand Down Expand Up @@ -451,7 +451,7 @@ func ensureNextStepsFresh(appID string) {
if nextStepsDisabled() {
return
}
dir, err := resolveUnder(appStoreRoot(), appID)
dir, err := resolveAppDir(appStoreRoot(), appID)
if err != nil {
return
}
Expand Down
32 changes: 32 additions & 0 deletions cmd/pilotctl/zz_subprocess_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,38 @@ func TestCLIAppStoreUninstallRequiresYes(t *testing.T) {
}
}

// `uninstall .` used to resolve to the install root itself, so --yes removed
// every installed app. An app id is a single directory name.
func TestCLIAppStoreUninstallRejectsRootAndNestedIDs(t *testing.T) {
t.Parallel()
for _, id := range []string{".", "..", "victim/..", "a/b"} {
id := id
t.Run(id, func(t *testing.T) {
t.Parallel()
root := filepath.Join(t.TempDir(), "apps-root")
for _, app := range []string{"victim", "bystander"} {
if err := os.MkdirAll(filepath.Join(root, app), 0o755); err != nil {
t.Fatal(err)
}
}
_, stderr, code := runCLI(t, []string{"appstore", "uninstall", id, "--yes"}, map[string]string{
"PILOT_APPSTORE_ROOT": root,
})
if code == 0 {
t.Errorf("uninstall %q --yes succeeded", id)
}
if !strings.Contains(stderr, "invalid app id") {
t.Errorf("expected invalid app id error: %s", stderr)
}
for _, app := range []string{"victim", "bystander"} {
if _, err := os.Stat(filepath.Join(root, app)); err != nil {
t.Errorf("%s should still exist: %v", app, err)
}
}
})
}
}

func TestCLIAppStoreVerifyMissingBundle(t *testing.T) {
t.Parallel()
_, stderr, code := runCLI(t, []string{"appstore", "verify", "/no/such/bundle"}, nil)
Expand Down
Loading