Skip to content
63 changes: 63 additions & 0 deletions doc/toolbox-upgrade.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@

% toolbox-upgrade(1)

## NAME
toolbox-upgrade - Upgrade packages in Toolbx containers

## SYNOPSIS
**toolbox upgrade** [*--all* | *-a*] [*--container* | *-c* *CONTAINER*] [*CONTAINER*]
Copy link
Member

Choose a reason for hiding this comment

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

Don't you want to support specifying multiple containers by name:

$ toolbox upgrade CONTAINER1 CONTAINER2

The rm and rmi commands support that.


## DESCRIPTION
Upgrades packages inside one or more Toolbx containers. The container must have been created using `toolbox create` and must include a label specifying how to upgrade its packages.

This command will:
1. Read the container's metadata label `com.github.containers.toolbox.package-manager.update`
2. Run the specified upgrade command inside the container
3. Report any errors that occur during the process

The `--container` flag is optional when a positional container name is given.

## LABEL REQUIREMENT
Each container **must** have the following OCI label set in its metadata:

`com.github.containers.toolbox.package-manager.update="COMMAND"`

This label defines the exact package upgrade command to run inside the container. For example:

`com.github.containers.toolbox.package-manager.update="sudo dnf --assumeyes update"`

This label is typically the responsibility of the **image publisher** and should be present at container creation.

## OPTIONS

**--all, -a**
Upgrade all Toolbx containers. Cannot be used with *--container* or a positional argument.

**--container, -c** *CONTAINER*
Upgrade a specific Toolbx container. Optional when a positional container name is provided.

## EXAMPLES

**Upgrade a specific container (positional):**

$ toolbox upgrade fedora-toolbox-38


**Upgrade a specific container (flag):**

$ toolbox upgrade --container fedora-toolbox-38


**Upgrade all containers:**

$ toolbox upgrade --all


## NOTES

- This command doesn't perform package manager detection itself.
- It relies entirely on the container image to define the correct update mechanism.
- The `package-manager.update` label **must be set**; otherwise, the upgrade will fail.

## SEE ALSO
`toolbox(1)`, `toolbox-create(1)`, `toolbox-list(1)`
4 changes: 4 additions & 0 deletions doc/toolbox.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ Remove one or more Toolbx images.

Run a command in an existing Toolbx container.

**toolbox-upgrade(1)**

Upgrade one or more existing Toolbx containers via their Package Manager.

## FILES ##

**toolbox.conf(5)**
Expand Down
1 change: 1 addition & 0 deletions images/arch/Containerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
FROM docker.io/library/archlinux:base-devel

LABEL com.github.containers.toolbox="true" \
com.github.containers.toolbox.package-manager.update="sudo pacman -Syu --noconfirm" \
Copy link
Collaborator

Choose a reason for hiding this comment

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

The issue is that this is not going to work that reliably. pacman is going to prompt for some resolutions and that will lock things up. You could run something along the lines of sudo sh -c 'yes | pacman -Syu --noconfirm' but you might end up in a situation where the keyring is sufficiently outdated for this to work.

sudo sh -c 'sudo pacman -Sy archlinux-keyring; yes | pacman -Su --noconfirm'

or similar could work but there is no guarantees as Arch Linux is not built around the assumptions that unattended upgrades are supported.

Copy link
Member

Choose a reason for hiding this comment

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

I see, interesting. Thanks for that insight, @Foxboron

In that case, my vote is to not add the label to the arch-toolbox image, and leave it as explicitly unsupported. It's better to not advertise things to users that are not meant to work.

name="arch-toolbox" \
version="base-devel" \
usage="This image is meant to be used with the toolbox command" \
Expand Down
1 change: 1 addition & 0 deletions images/ubuntu/24.04/Containerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
FROM docker.io/library/ubuntu:24.04

LABEL com.github.containers.toolbox="true" \
com.github.containers.toolbox.package-manager.update="sudo DEBIAN_FRONTEND=noninteractive apt-get update -y && sudo apt-get upgrade -y" \
name="ubuntu-toolbox" \
version="24.04" \
usage="This image is meant to be used with the toolbox command" \
Expand Down
1 change: 1 addition & 0 deletions images/ubuntu/24.10/Containerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
FROM docker.io/library/ubuntu:24.10

LABEL com.github.containers.toolbox="true" \
com.github.containers.toolbox.package-manager.update="sudo DEBIAN_FRONTEND=noninteractive apt-get update -y && sudo apt-get upgrade -y" \
name="ubuntu-toolbox" \
version="24.10" \
usage="This image is meant to be used with the toolbox command" \
Expand Down
1 change: 1 addition & 0 deletions images/ubuntu/25.04/Containerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
FROM docker.io/library/ubuntu:25.04

LABEL com.github.containers.toolbox="true" \
com.github.containers.toolbox.package-manager.update="sudo DEBIAN_FRONTEND=noninteractive apt-get update -y && sudo apt-get upgrade -y" \
name="ubuntu-toolbox" \
version="25.04" \
usage="This image is meant to be used with the toolbox command" \
Expand Down
139 changes: 139 additions & 0 deletions src/cmd/upgrade.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright © 2025 Hadi Chokr <[email protected]>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package cmd

import (
"errors"
"fmt"
"os"

"github.com/containers/toolbox/pkg/podman"
"github.com/containers/toolbox/pkg/utils"
"github.com/spf13/cobra"
)

var (
upgradeAll bool
upgradeContainer string
)

var upgradeCmd = &cobra.Command{
Use: "upgrade [container]",
Short: "Detect package manager and upgrade packages in toolbx containers",
Args: cobra.MaximumNArgs(1),
RunE: runUpgrade,
ValidArgsFunction: completionContainerNamesFiltered,
}

func init() {
upgradeCmd.Flags().BoolVar(&upgradeAll, "all", false, "Upgrade all Toolbx containers")
upgradeCmd.Flags().StringVar(&upgradeContainer, "container", "", "Name of the toolbox container to upgrade")

// Register container flag completion
if err := upgradeCmd.RegisterFlagCompletionFunc("container", completionContainerNames); err != nil {
fmt.Fprintf(os.Stderr, "failed to register flag completion function: %v\n", err)
os.Exit(1)
}
upgradeCmd.SetHelpFunc(upgradeHelp)
rootCmd.AddCommand(upgradeCmd)
}

func runUpgrade(cmd *cobra.Command, args []string) error {
// Use positional argument as container name if --container not set
if upgradeContainer == "" && len(args) == 1 {
upgradeContainer = args[0]
}

if !upgradeAll && upgradeContainer == "" {
return errors.New("must specify either --all or a container name")
Copy link
Member

Choose a reason for hiding this comment

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

We should show:

Error: missing argument for "upgrade"
Run 'toolbox --help' for usage.

... as we do for the other commands. eg., see src/cmd/rm.go

}

if upgradeAll && upgradeContainer != "" {
return errors.New("cannot specify both --all and a container name")
}

if upgradeAll {
containers, err := getContainers()
if err != nil {
return err
}

if len(containers) == 0 {
return errors.New("no Toolbx containers found")
}

for _, container := range containers {
fmt.Printf("Upgrading container: %s\n", container.Name())
if err := execUpgradeInContainer(container.Name()); err != nil {
fmt.Fprintf(os.Stderr, "Error upgrading container %s: %v\n", container.Name(), err)
}
}
return nil
}

return execUpgradeInContainer(upgradeContainer)
}

// execUpgradeInContainer runs detection and upgrade inside the specified container
func execUpgradeInContainer(container string) error {

inspectedcontainer, err := podman.InspectContainer(container)
if err != nil {
return errors.New("Failed to inspect Metadata.")
Copy link
Member

Choose a reason for hiding this comment

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

The error should be:

fmt.Errorf("failed to inspect container %s", container)

... as is used elsewhere in the code base.

}

labels := inspectedcontainer.Labels()
pkgline := labels["com.github.containers.toolbox.package-manager.update"]
if pkgline == "" {
return errors.New("'com.github.containers.toolbox.package-manager.update' Label not set in Containers Metadata.")
Copy link
Member

Choose a reason for hiding this comment

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

We need an error message that a user, who doesn't know about the internal implementation details, can understand and suggests how to move forward. :)

Something like:

Error: container CONTAINER does not support "upgrade"
Recreate it with a newer image or file a bug.

} else {
// Use runCommand to execute the upgrade
upgradeErr := runCommand(inspectedcontainer.Name(),
false,
"",
"",
0,
[]string{"sh", "-c", pkgline},
Copy link
Member

Choose a reason for hiding this comment

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

I was thinking about this over the past few weeks in the context of the NVIDIAScape security issue, and it struck me that someone can trick a user into using a malicious image that contains a harmful command in its c.gh.containers.toolbx.package-manager.update label.

It will be wise to prompt the user with the command that will be run before running it. The user can use the --assumeyes if they want to override the prompt. Look for the askForConfirmation function for an existing example of this.

This makes me wonder if we should have variants of the c.gh.containers.toolbx.package-manager.update label that use an assume yes option for the package manager and don't. It will be bad if a dnf --assumeyes upgrade breaks the container without giving the user a way to opt out of it as usual.

Copy link
Member

Choose a reason for hiding this comment

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

Immediately after I submitted the comment, it struck me that a malicious image can already trick Toolbx into doing bad things.

Such as, the entry point of Toolbx containers rely on the mount(8) binary. So, if there's an image that has replaced that binary with something else, then toolbox enter can lead to bad things.

We could re-implement the parts of mount(8) that Toolbx needs in Go to avoid this, but then someone can replace the bash(1) binary with something else, and so on.

So, I suppose, the security argument doesn't really hold?

Even then, maybe we should offer a prompt and show what's about to happen just for the sake of politeness? Just like we do before downloading an image.

I am still wondering if this means we should have a prompt before the package manager is invoked, or after it's invoked but before the packages are downloaded and changed, or both.

Copy link
Author

Choose a reason for hiding this comment

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

Probably the best approach would be to tell the user the update command then prompt them and if the user wants via a flag skip it but by default they will be told about it.
And to be honest we should probably have a vetted compatibility list like distrobox.

Copy link
Member

Choose a reason for hiding this comment

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

Probably the best approach would be to tell the user the update command then prompt them and if the user wants via a flag skip it but by default they will be told about it.

You mean prompting the user both before and after invoking the package manager? ie., prompting the user that dnf upgrade will be called, then prompting the user to proceed with what dnf upgrade wants to do?

I suspect we could avoid the first prompt and only inform the user that dnf upgrade is going to be run, if we are sure that it won't generate a lot of network traffic. Otherwise, it will be bad for users with limited Internet connectivity.

I am beginning to think that having too many prompts is annoying, and it's better than the alternative of potentially rudely surprising the users by not letting them have a say.

And to be honest we should probably have a vetted compatibility list like distrobox.

How do you want to implement that?

The first step for declaring full support for an OS is to run the CI on hosts with that OS, which isn't a trivial task. Do you want to skip the upgrade command for those that are on the compatibility list?

Choose a reason for hiding this comment

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

I think it makes sense to have a confirmation prompt before running the package manager if the command has some kind of --assumeyes argument. If it doesn't, then confirmation can happen after invocation.

false,
false,
true)

return upgradeErr

}
}

func upgradeHelp(cmd *cobra.Command, args []string) {
if utils.IsInsideContainer() {
if !utils.IsInsideToolboxContainer() {
fmt.Fprintf(os.Stderr, "Error: this is not a Toolbx container\n")
return
}

if _, err := utils.ForwardToHost(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
return
}

return
}

if err := showManual("toolbox-upgrade"); err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
return
}
}
54 changes: 54 additions & 0 deletions test/system/111-upgrade.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# shellcheck shell=bats
#
# Copyright © 2025 Hadi Chokr <[email protected]>
# Licensed under the Apache License, Version 2.0 (the "License");
# You may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# bats file_tags=commands-options

load 'libs/bats-support/load'
load 'libs/bats-assert/load'
load 'libs/helpers'

setup() {
bats_require_minimum_version 1.8.0
cleanup_all
}

teardown() {
cleanup_all
}

@test "upgrade(Arch): Upgrade Arch container" {
create_distro_container arch latest arch-toolbox-test
assert_success
Copy link
Member

Choose a reason for hiding this comment

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

Is it really necessary to use assert_success here? It's supposed to be used to check the exit code of the command invoked by the run helper.

When we only need to check that a command succeeded, there's no need to use run or check anything, because Bats sets set -e for all tests and the test will automatically fail.


run "$TOOLBX" upgrade --container arch-toolbox-test
assert_success
}

@test "upgrade(Arch): Upgrade Arch container without --container flag" {
create_distro_container arch latest arch-toolbox-test
assert_success

run "$TOOLBX" upgrade arch-toolbox-test
assert_success
}

@test "upgrade(All): Upgrade all containers" {
create_distro_container arch latest arch-toolbox-all-test
create_default_container
assert_success

run "$TOOLBX" upgrade --all
assert_success
}
1 change: 1 addition & 0 deletions test/system/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ test_system = files(
'106-rm.bats',
'107-rmi.bats',
'108-completion.bats',
'111-upgrade.bats',
'201-ipc.bats',
'203-network.bats',
'206-user.bats',
Expand Down