diff --git a/app/app.go b/app/app.go index de9ecf04..1d42b5f0 100644 --- a/app/app.go +++ b/app/app.go @@ -33,6 +33,7 @@ type App interface { Status(extended bool) error Version() error Runtime() (string, error) + Update() error Kubernetes() (environment.Container, error) } @@ -454,6 +455,25 @@ func (c colimaApp) Active() bool { return c.guest.Running(context.Background()) } +func (c *colimaApp) Update() error { + ctx := context.Background() + if !c.guest.Running(ctx) { + return fmt.Errorf("%s is not running", config.CurrentProfile().DisplayName) + } + + runtime, err := c.currentRuntime(ctx) + if err != nil { + return err + } + + container, err := c.containerEnvironment(runtime) + if err != nil { + return err + } + + return container.Update(ctx) +} + func generateSSHConfig(modifySSHConfig bool) error { instances, err := limautil.Instances() if err != nil { diff --git a/cmd/root/root.go b/cmd/root/root.go index f061b0f8..df0c5aad 100644 --- a/cmd/root/root.go +++ b/cmd/root/root.go @@ -22,13 +22,14 @@ var rootCmd = &cobra.Command{ switch cmd.Name() { // special case handling for commands directly interacting with the VM - // start, stop, restart, delete, status, version, ssh-config + // start, stop, restart, delete, status, version, update, ssh-config case "start", "stop", "restart", "delete", "status", "version", + "update", "ssh-config": // if an arg is passed, assume it to be the profile (provided --profile is unset) diff --git a/cmd/update.go b/cmd/update.go new file mode 100644 index 00000000..37461a46 --- /dev/null +++ b/cmd/update.go @@ -0,0 +1,22 @@ +package cmd + +import ( + "github.com/abiosoft/colima/cmd/root" + "github.com/spf13/cobra" +) + +// statusCmd represents the status command +var updateCmd = &cobra.Command{ + Use: "update [profile]", + Aliases: []string{"u", "up"}, + Short: "update the container runtime", + Long: `Update the current container runtime.`, + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return newApp().Update() + }, +} + +func init() { + root.Cmd().AddCommand(updateCmd) +} diff --git a/environment/container.go b/environment/container.go index b8b4a05b..24151ced 100644 --- a/environment/container.go +++ b/environment/container.go @@ -22,6 +22,8 @@ type Container interface { Stop(ctx context.Context) error // Teardown tears down/uninstall the container runtime. Teardown(ctx context.Context) error + // Update the container runtime. + Update(ctx context.Context) error // Version returns the container runtime version. Version(ctx context.Context) string // Running returns if the container runtime is currently running. diff --git a/environment/container/containerd/containerd.go b/environment/container/containerd/containerd.go index 09e9fd0f..0129777c 100644 --- a/environment/container/containerd/containerd.go +++ b/environment/container/containerd/containerd.go @@ -3,6 +3,7 @@ package containerd import ( "context" _ "embed" + "fmt" "path/filepath" "time" @@ -101,3 +102,7 @@ func (c containerdRuntime) Version(ctx context.Context) string { version, _ := c.guest.RunOutput("sudo", "nerdctl", "version", "--format", `client: {{.Client.Version}}{{printf "\n"}}server: {{(index .Server.Components 0).Version}}`) return version } + +func (c *containerdRuntime) Update(ctx context.Context) error { + return fmt.Errorf("update not supported for the %s runtime", Name) +} diff --git a/environment/container/docker/docker.go b/environment/container/docker/docker.go index 7d79086e..9dd7fcb6 100644 --- a/environment/container/docker/docker.go +++ b/environment/container/docker/docker.go @@ -7,6 +7,7 @@ import ( "github.com/abiosoft/colima/cli" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/environment" + "github.com/abiosoft/colima/util/debutil" ) // Name is container runtime name. @@ -133,3 +134,13 @@ func (d dockerRuntime) Version(ctx context.Context) string { version, _ := d.host.RunOutput("docker", "version", "--format", `client: v{{.Client.Version}}{{printf "\n"}}server: v{{.Server.Version}}`) return version } + +func (d *dockerRuntime) Update(ctx context.Context) error { + packages := []string{ + "docker-ce", + "docker-ce-cli", + "containerd.io", + } + + return debutil.UpdateRuntime(ctx, d.guest, d, Name, packages...) +} diff --git a/environment/container/incus/config.yaml b/environment/container/incus/config.yaml index 8d4ef53d..c0cb67f6 100644 --- a/environment/container/incus/config.yaml +++ b/environment/container/incus/config.yaml @@ -2,7 +2,7 @@ config: core.https_address: '[::]:8443' networks: - config: - ipv4.address: 10.0.0.1/8 + ipv4.address: 192.100.0.1/24 ipv4.nat: "true" ipv6.address: auto description: "" diff --git a/environment/container/incus/incus.go b/environment/container/incus/incus.go index 71ee1d67..6c95dbe6 100644 --- a/environment/container/incus/incus.go +++ b/environment/container/incus/incus.go @@ -14,6 +14,7 @@ import ( "github.com/abiosoft/colima/environment" "github.com/abiosoft/colima/environment/vm/lima/limautil" "github.com/abiosoft/colima/util" + "github.com/abiosoft/colima/util/debutil" ) const incusBridgeInterface = "incusbr0" @@ -278,3 +279,15 @@ type networkInfo struct { Managed bool `json:"managed"` Type string `json:"type"` } + +func (c *incusRuntime) Update(ctx context.Context) error { + packages := []string{ + "incus", + "incus-base", + "incus-client", + "incus-extra", + "incus-ui-canonical", + } + + return debutil.UpdateRuntime(ctx, c.guest, c, Name, packages...) +} diff --git a/environment/container/kubernetes/kubernetes.go b/environment/container/kubernetes/kubernetes.go index 81220fdb..ed44de16 100644 --- a/environment/container/kubernetes/kubernetes.go +++ b/environment/container/kubernetes/kubernetes.go @@ -263,3 +263,8 @@ func (c kubernetesRuntime) Version(context.Context) string { version, _ := c.host.RunOutput("kubectl", "--context", config.CurrentProfile().ID, "version", "--short") return version } + +func (c *kubernetesRuntime) Update(ctx context.Context) error { + // update not supported + return nil +} diff --git a/util/debutil/debutil.go b/util/debutil/debutil.go new file mode 100644 index 00000000..3e14d24e --- /dev/null +++ b/util/debutil/debutil.go @@ -0,0 +1,84 @@ +package debutil + +import ( + "context" + "fmt" + "strings" + + "github.com/abiosoft/colima/cli" + "github.com/abiosoft/colima/environment" +) + +// packages is list of deb package names. +type packages []string + +// Upgradable returns the shell command to check if the packages are upgradable with apt. +// The returned command should be passed to 'sh -c' or equivalent. +func (p packages) Upgradable() string { + cmd := "sudo apt list --upgradable | grep" + for _, v := range p { + cmd += fmt.Sprintf(" -e '^%s/'", v) + } + return cmd +} + +// Install returns the shell command to install the packages with apt. +// The returned command should be passed to 'sh -c' or equivalent. +func (p packages) Install() string { + return "sudo apt-get install -y --allow-change-held-packages " + strings.Join(p, " ") +} + +func UpdateRuntime( + ctx context.Context, + guest environment.GuestActions, + chain cli.CommandChain, + runtime string, + packageNames ...string, +) error { + a := chain.Init(ctx) + log := a.Logger() + + packages := packages(packageNames) + + updatesAvailable := false + + a.Stage("refreshing package manager") + a.Add(func() error { + return guest.RunQuiet( + "sh", + "-c", + "sudo apt-get update -y", + ) + }) + + a.Stage("checking for updates") + a.Add(func() error { + err := guest.RunQuiet( + "sh", + "-c", + packages.Upgradable(), + ) + updatesAvailable = err == nil + return nil + }) + + a.Add(func() (err error) { + if !updatesAvailable { + log.Warnf("no updates available for %s runtime", runtime) + return + } + + log.Println("updating packages ...") + err = guest.RunQuiet( + "sh", + "-c", + packages.Install(), + ) + if err == nil { + log.Println("done") + } + return + }) + + return a.Exec() +}