diff --git a/doc/toolbox-upgrade.1.md b/doc/toolbox-upgrade.1.md new file mode 100644 index 000000000..f9154ef8a --- /dev/null +++ b/doc/toolbox-upgrade.1.md @@ -0,0 +1,63 @@ + +% toolbox-upgrade(1) + +## NAME +toolbox-upgrade - Upgrade packages in Toolbx containers + +## SYNOPSIS +**toolbox upgrade** [*--all* | *-a*] [*--container* | *-c* *CONTAINER*] [*CONTAINER*] + +## 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)` diff --git a/doc/toolbox.1.md b/doc/toolbox.1.md index 6102e7b43..f225ab7d0 100644 --- a/doc/toolbox.1.md +++ b/doc/toolbox.1.md @@ -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)** diff --git a/images/arch/Containerfile b/images/arch/Containerfile index 00163eada..8257a6cd5 100644 --- a/images/arch/Containerfile +++ b/images/arch/Containerfile @@ -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" \ name="arch-toolbox" \ version="base-devel" \ usage="This image is meant to be used with the toolbox command" \ diff --git a/images/ubuntu/24.04/Containerfile b/images/ubuntu/24.04/Containerfile index 7644f51c4..027d44d32 100644 --- a/images/ubuntu/24.04/Containerfile +++ b/images/ubuntu/24.04/Containerfile @@ -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" \ diff --git a/images/ubuntu/24.10/Containerfile b/images/ubuntu/24.10/Containerfile index ee89e0eb9..baa20f81f 100644 --- a/images/ubuntu/24.10/Containerfile +++ b/images/ubuntu/24.10/Containerfile @@ -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" \ diff --git a/images/ubuntu/25.04/Containerfile b/images/ubuntu/25.04/Containerfile index c2f21a971..d417302e4 100644 --- a/images/ubuntu/25.04/Containerfile +++ b/images/ubuntu/25.04/Containerfile @@ -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" \ diff --git a/src/cmd/upgrade.go b/src/cmd/upgrade.go new file mode 100644 index 000000000..aa55c384f --- /dev/null +++ b/src/cmd/upgrade.go @@ -0,0 +1,139 @@ +/* + * Copyright © 2025 Hadi Chokr + * + * 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") + } + + 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.") + } + + 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.") + } else { + // Use runCommand to execute the upgrade + upgradeErr := runCommand(inspectedcontainer.Name(), + false, + "", + "", + 0, + []string{"sh", "-c", pkgline}, + 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 + } +} diff --git a/test/system/111-upgrade.bats b/test/system/111-upgrade.bats new file mode 100644 index 000000000..1324159d5 --- /dev/null +++ b/test/system/111-upgrade.bats @@ -0,0 +1,54 @@ +# shellcheck shell=bats +# +# Copyright © 2025 Hadi Chokr +# 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 + + 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 +} diff --git a/test/system/meson.build b/test/system/meson.build index c53add0cc..d0af9b0c4 100644 --- a/test/system/meson.build +++ b/test/system/meson.build @@ -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',