mirror of
https://github.com/containers/podman.git
synced 2026-02-05 06:45:31 +01:00
Add podman machine os upgrade command
Implements automatic OS upgrade functionality for Podman machines that requires no user input beyond running the command. The upgrade logic automatically determines the appropriate upgrade path using a three-way comparison between client version, machine version, and OCI registry: * When the client version is older than the machine version, no action is taken and an error is returned. * When the client version matches the machine version, the OCI registry is queried to check for in-band updates by comparing image digests. This handles minor, patch level, and updates oci image use cases. * When the client version is newer than the machine version, the machine is upgraded to match the client's major.minor version. * No manual image selection or version specification required. The command supports dry-run mode and JSON (only) output format for automation. Signed-off-by: Brent Baude <bbaude@redhat.com>
This commit is contained in:
98
cmd/podman/machine/os/upgrade.go
Normal file
98
cmd/podman/machine/os/upgrade.go
Normal file
@@ -0,0 +1,98 @@
|
||||
//go:build amd64 || arm64
|
||||
|
||||
package os
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/containers/podman/v6/cmd/podman/common"
|
||||
"github.com/containers/podman/v6/cmd/podman/machine"
|
||||
"github.com/containers/podman/v6/cmd/podman/registry"
|
||||
"github.com/containers/podman/v6/cmd/podman/validate"
|
||||
"github.com/containers/podman/v6/pkg/machine/os"
|
||||
"github.com/containers/podman/v6/version"
|
||||
"github.com/spf13/cobra"
|
||||
"go.podman.io/common/pkg/completion"
|
||||
)
|
||||
|
||||
var upgradeCmd = &cobra.Command{
|
||||
Use: "upgrade [options] [NAME]",
|
||||
Short: "Upgrade machine os",
|
||||
Long: "Upgrade the machine operating system to a newer version",
|
||||
PersistentPreRunE: validate.NoOp,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: upgrade,
|
||||
ValidArgsFunction: common.AutocompleteImages,
|
||||
Example: `podman machine os upgrade`,
|
||||
}
|
||||
|
||||
type upgradeOpts struct {
|
||||
dryRun bool
|
||||
format string
|
||||
hostVersion string
|
||||
restart bool
|
||||
}
|
||||
|
||||
var opts upgradeOpts
|
||||
|
||||
func init() {
|
||||
registry.Commands = append(registry.Commands, registry.CliCommand{
|
||||
Command: upgradeCmd,
|
||||
Parent: machine.OSCmd,
|
||||
})
|
||||
flags := upgradeCmd.Flags()
|
||||
|
||||
dryRunFlagName := "dry-run"
|
||||
flags.BoolVarP(&opts.dryRun, dryRunFlagName, "n", false, "Only check if an upgrade is available")
|
||||
hostVersionFlagName := "host-version"
|
||||
flags.StringVar(&opts.hostVersion, hostVersionFlagName, "", "Podman host version (advanced use only)")
|
||||
_ = upgradeCmd.RegisterFlagCompletionFunc(hostVersionFlagName, completion.AutocompleteNone)
|
||||
_ = flags.MarkHidden(hostVersionFlagName)
|
||||
formatFlagName := "format"
|
||||
flags.StringVarP(&opts.format, formatFlagName, "f", "", "suppress output except for specified format. Implies -n")
|
||||
_ = upgradeCmd.RegisterFlagCompletionFunc(formatFlagName, completion.AutocompleteNone)
|
||||
restartFlagName := "restart"
|
||||
flags.BoolVar(&opts.restart, restartFlagName, false, "Restart VM to upgrade")
|
||||
}
|
||||
|
||||
func upgrade(_ *cobra.Command, args []string) error {
|
||||
var vmName string
|
||||
if len(args) == 1 {
|
||||
vmName = args[0]
|
||||
}
|
||||
|
||||
managerOpts := ManagerOpts{
|
||||
VMName: vmName,
|
||||
CLIArgs: args,
|
||||
Restart: opts.restart,
|
||||
}
|
||||
|
||||
osManager, err := NewOSManager(managerOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
upgradeOpts := os.UpgradeOptions{ClientVersion: version.Version, DryRun: opts.dryRun}
|
||||
if opts.hostVersion != "" {
|
||||
callerVersion, err := semver.ParseTolerant(opts.hostVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
upgradeOpts.MachineVersion = callerVersion
|
||||
}
|
||||
if kind := opts.format; len(opts.format) > 0 {
|
||||
// For now, we only support one output format value of `json`
|
||||
// But in the future, we may add additional formats as needed
|
||||
if kind != "json" {
|
||||
return errors.New("only value of `json` is supported")
|
||||
}
|
||||
upgradeOpts.Format = kind
|
||||
}
|
||||
if opts.restart && (len(upgradeOpts.Format) > 0 || opts.dryRun) {
|
||||
return errors.New("--restart cannot be used with --dry-run or --format")
|
||||
}
|
||||
|
||||
return osManager.Upgrade(context.Background(), upgradeOpts)
|
||||
}
|
||||
91
docs/source/markdown/podman-machine-os-upgrade.1.md
Normal file
91
docs/source/markdown/podman-machine-os-upgrade.1.md
Normal file
@@ -0,0 +1,91 @@
|
||||
% podman-machine-os-upgrade 1
|
||||
|
||||
## NAME
|
||||
podman\-machine\-os\-upgrade - Upgrade a Podman Machine's OS
|
||||
|
||||
## SYNOPSIS
|
||||
**podman machine os upgrade** [*options*] [vm]
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
Upgrade the OS of a Podman Machine
|
||||
|
||||
Automatically perform an upgrade of the Podman Machine OS according to the logic below.
|
||||
To apply a custom image or an image with a specific digest, use **podman machine os apply** instead.
|
||||
|
||||
The default machine name is `podman-machine-default`. If a machine name is not specified as an argument,
|
||||
then the OS changes will be applied to `podman-machine-default`.
|
||||
|
||||
The machine must be started for this command to be run.
|
||||
|
||||
### UPGRADE LOGIC
|
||||
|
||||
The upgrade function compares the client version against the machine version using semantic versioning (major.minor
|
||||
only, ignoring patch levels). When versions match, it also queries the online registry to check for image updates:
|
||||
|
||||
**Client version older than machine version (downgrade):**
|
||||
Returns an error. Downgrading is not supported to prevent incompatibilities. Update your Podman client to match or exceed the machine version.
|
||||
|
||||
**Client version equals machine version (in-band update):**
|
||||
Checks for updates to the same version stream by comparing the local OS image digest against the registry's digest. This handles two scenarios:
|
||||
- Patch version updates (e.g., 6.0.1 → 6.0.2)
|
||||
- OS image refreshes with the same Podman version (e.g., 6.0.1 → newer OS build with 6.0.1)
|
||||
|
||||
If an in-band update exists, the machine is updated to the new image using its digest reference. If no update is available, the function reports that the system is current.
|
||||
|
||||
**Client version newer than machine version (major/minor upgrade):**
|
||||
Upgrades the machine OS to match the client's major.minor version. The upgrade targets a new version tag constructed from the client's major and minor version numbers (e.g., upgrading from 6.0.x to 6.1 when the client is version 6.1.0).
|
||||
|
||||
## OPTIONS
|
||||
|
||||
#### **--dry-run**, **-n**
|
||||
|
||||
Only perform a dry-run of checking for the upgrade. No content is downloaded or applied. This option
|
||||
cannot be used with --restart
|
||||
|
||||
#### **--format**, **-f**
|
||||
|
||||
Define a structured output format. The only valid value for this is `json`. Using this option
|
||||
imples a dry-run. This option cannot be used with --restart.
|
||||
|
||||
#### **--help**
|
||||
|
||||
Print usage statement.
|
||||
|
||||
#### **--restart**
|
||||
|
||||
Restart VM after applying changes.
|
||||
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
Update the default Podman machine to the latest in-band version or update the machine to
|
||||
match the client Podman version.
|
||||
```
|
||||
$ podman machine os upgrade
|
||||
```
|
||||
|
||||
Same as above but specifying a specific machine.
|
||||
|
||||
```
|
||||
$ podman machine os upgrade mymachine
|
||||
```
|
||||
|
||||
Check if an update is available but do not download or apply the upgrade.
|
||||
```
|
||||
$ podman machine os upgrade -n
|
||||
```
|
||||
|
||||
Check if an update is available but the response will be in JSON format. Note this
|
||||
exposes a boolean field that indicates if an update is available.
|
||||
|
||||
Note: using a format option implies a dry-run.
|
||||
```
|
||||
$ podman machine os upgrade -f json
|
||||
```
|
||||
|
||||
## SEE ALSO
|
||||
**[podman(1)](podman.1.md)**, **[podman-machine(1)](podman-machine.1.md)**, **[podman-machine-os(1)](podman-machine-os.1.md)**
|
||||
|
||||
## HISTORY
|
||||
January 2026, Originally compiled by Brent Baude <bbaude@redhat.com>
|
||||
@@ -11,12 +11,13 @@ podman\-machine\-os - Manage a Podman virtual machine's OS
|
||||
|
||||
## SUBCOMMANDS
|
||||
|
||||
| Command | Man Page | Description |
|
||||
|---------|--------------------------------------------------------------|----------------------------------------------|
|
||||
| apply | [podman-machine-os-apply(1)](podman-machine-os-apply.1.md) | Apply an OCI image to a Podman Machine's OS |
|
||||
| Command | Man Page | Description |
|
||||
|---------|--------------------------------------------------------------|---------------------------------------------|
|
||||
| apply | [podman-machine-os-apply(1)](podman-machine-os-apply.1.md) | Apply an OCI image to a Podman Machine's OS |
|
||||
| upgrade | [podman-machine-os-upgrade(1)](podman-machine-os-upgrade.1.md) | Upgrade a Podman Machine's OS |
|
||||
|
||||
## SEE ALSO
|
||||
**[podman(1)](podman.1.md)**, **[podman-machine(1)](podman-machine.1.md)**, **[podman-machine-os-apply(1)](podman-machine-os-apply.1.md)**
|
||||
**[podman(1)](podman.1.md)**, **[podman-machine(1)](podman-machine.1.md)**, **[podman-machine-os-apply(1)](podman-machine-os-apply.1.md)**, **[podman-machine-os-upgrade(1)](podman-machine-os-upgrade.1.md)**
|
||||
|
||||
## HISTORY
|
||||
February 2023, Originally compiled by Ashley Cui <acui@redhat.com>
|
||||
|
||||
@@ -92,6 +92,7 @@ my %Format_Option_Is_Special = map { $_ => 1 } (
|
||||
'diff', 'container diff', 'image diff', # only supports "json"
|
||||
'generate systemd', # " " " "
|
||||
'mount', 'container mount', 'image mount', # " " " "
|
||||
'machine os upgrade', # " " " "
|
||||
'push', 'image push', 'manifest push', # oci | v2s*
|
||||
'save', 'image save', # image formats (oci-*, ...)
|
||||
'inspect', # ambiguous (container/image)
|
||||
|
||||
152
pkg/machine/os/bootc.go
Normal file
152
pkg/machine/os/bootc.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package os
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/opencontainers/go-digest"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/sirupsen/logrus"
|
||||
"go.podman.io/image/v5/docker/reference"
|
||||
"go.podman.io/image/v5/manifest"
|
||||
)
|
||||
|
||||
// BootcHost represents the top-level bootc status structure
|
||||
type BootcHost struct {
|
||||
APIVersion string `json:"apiVersion"`
|
||||
Kind string `json:"kind"`
|
||||
Metadata bootcMetadata `json:"metadata"`
|
||||
Spec bootcSpec `json:"spec"`
|
||||
Status bootcStatus `json:"status"`
|
||||
}
|
||||
|
||||
// bootcMetadata contains metadata about the host
|
||||
type bootcMetadata struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// bootcSpec contains the specification for the bootc host
|
||||
type bootcSpec struct {
|
||||
BootOrder string `json:"bootOrder"`
|
||||
Image imageRef `json:"image"`
|
||||
}
|
||||
|
||||
// imageRef represents a container image reference
|
||||
type imageRef struct {
|
||||
Image string `json:"image"`
|
||||
Transport string `json:"transport"`
|
||||
}
|
||||
|
||||
// bootcStatus contains the current status of the bootc host
|
||||
type bootcStatus struct {
|
||||
Booted *bootEntry `json:"booted"`
|
||||
Rollback *bootEntry `json:"rollback"`
|
||||
Staged *bootEntry `json:"staged"`
|
||||
RollbackQueued bool `json:"rollbackQueued"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// bootEntry represents a boot entry (booted, rollback, or staged)
|
||||
type bootEntry struct {
|
||||
CachedUpdate *imageStatus `json:"cachedUpdate"`
|
||||
Image imageStatus `json:"image"`
|
||||
Incompatible bool `json:"incompatible"`
|
||||
Ostree ostreeInfo `json:"ostree"`
|
||||
Pinned bool `json:"pinned"`
|
||||
SoftRebootCapable bool `json:"softRebootCapable"`
|
||||
Store string `json:"store"`
|
||||
}
|
||||
|
||||
// imageStatus contains detailed information about a container image
|
||||
type imageStatus struct {
|
||||
Architecture string `json:"architecture"`
|
||||
Image imageRefWithSig `json:"image"`
|
||||
ImageDigest string `json:"imageDigest"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// imageRefWithSig is an image reference that may include a signature
|
||||
type imageRefWithSig struct {
|
||||
Image string `json:"image"`
|
||||
Transport string `json:"transport"`
|
||||
Signature *signature `json:"signature,omitempty"`
|
||||
}
|
||||
|
||||
// signature contains signature information for an image
|
||||
type signature struct {
|
||||
OstreeRemote string `json:"ostreeRemote"`
|
||||
}
|
||||
|
||||
// ostreeInfo contains OSTree-specific information
|
||||
type ostreeInfo struct {
|
||||
Checksum string `json:"checksum"`
|
||||
DeploySerial int `json:"deploySerial"`
|
||||
Stateroot string `json:"stateroot"`
|
||||
}
|
||||
|
||||
// GetBootedImageRef returns the booted image reference (status -> booted -> image -> image)
|
||||
// Returns an empty string if booted is nil
|
||||
func (b *BootcHost) GetBootedImageRef() string {
|
||||
if b.Status.Booted == nil {
|
||||
return ""
|
||||
}
|
||||
return b.Status.Booted.Image.Image.Image
|
||||
}
|
||||
|
||||
// GetBootedOSTreeChecksum returns the booted OSTree checksum
|
||||
// Returns an empty string if booted is nil
|
||||
func (b *BootcHost) GetBootedOSTreeChecksum() string {
|
||||
if b.Status.Booted == nil {
|
||||
return ""
|
||||
}
|
||||
return b.Status.Booted.Ostree.Checksum
|
||||
}
|
||||
|
||||
func (b *BootcHost) getLocalOSOCIInfo() (reference.Named, *semver.Version, error) {
|
||||
ref, err := reference.ParseNormalizedNamed(b.GetBootedImageRef())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
named, ok := ref.(reference.NamedTagged)
|
||||
if !ok {
|
||||
return nil, nil, errors.New("failed to parse ostree origin")
|
||||
}
|
||||
tagAsVersion, err := semver.ParseTolerant(named.Tag())
|
||||
return named, &tagAsVersion, err
|
||||
}
|
||||
|
||||
func (b *BootcHost) getLocalOsImageDigest() (digest.Digest, error) {
|
||||
args := []string{"container", "image", "metadata", "--repo", "/ostree/repo", "docker://" + b.GetBootedImageRef()}
|
||||
logrus.Debugf("executing : ostree %s", strings.Join(args, " "))
|
||||
cmd := exec.Command("ostree", args...)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
localOs, err := manifest.FromBlob(out, v1.MediaTypeImageManifest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return localOs.ConfigInfo().Digest, nil
|
||||
}
|
||||
|
||||
// newBootcHost creates a new BootcHost by executing 'sudo bootc status --format json'
|
||||
// and unmarshaling the output
|
||||
func newBootcHost() (*BootcHost, error) {
|
||||
cmd := exec.Command("sudo", "bootc", "status", "--format", "json")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var host BootcHost
|
||||
if err := json.Unmarshal(output, &host); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &host, nil
|
||||
}
|
||||
@@ -2,13 +2,47 @@
|
||||
|
||||
package os
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/opencontainers/go-digest"
|
||||
)
|
||||
|
||||
// Manager is the interface for operations on a Podman machine's OS
|
||||
type Manager interface {
|
||||
// Apply machine OS changes from an OCI image.
|
||||
Apply(image string, opts ApplyOptions) error
|
||||
// Upgrade the machine OS
|
||||
Upgrade(ctx context.Context, opts UpgradeOptions) error
|
||||
}
|
||||
|
||||
// ApplyOptions are the options for applying an image into a Podman machine VM
|
||||
type ApplyOptions struct {
|
||||
Image string
|
||||
}
|
||||
|
||||
type UpgradeOptions struct {
|
||||
MachineVersion semver.Version
|
||||
DryRun bool
|
||||
Format string
|
||||
ClientVersion semver.Version
|
||||
}
|
||||
|
||||
type Host struct {
|
||||
Version semver.Version `json:"version"`
|
||||
}
|
||||
|
||||
type Machine struct {
|
||||
CurrentHash digest.Digest `json:"current_hash"`
|
||||
NewHash digest.Digest `json:"new_hash"`
|
||||
Version semver.Version `json:"version"`
|
||||
InBandUpgradeAvailable bool `json:"inband_update_available"`
|
||||
}
|
||||
|
||||
// UpgradeOutput is an output struct and is only exported so json tags work
|
||||
// correctly
|
||||
type UpgradeOutput struct {
|
||||
Host Host `json:"host"`
|
||||
Machine Machine `json:"machine"`
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package os
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/containers/podman/v6/pkg/machine"
|
||||
@@ -39,3 +40,31 @@ func (m *MachineOS) Apply(image string, _ ApplyOptions) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MachineOS) Upgrade(_ context.Context, opts UpgradeOptions) error {
|
||||
isDryRun := opts.DryRun
|
||||
if len(opts.Format) > 0 {
|
||||
isDryRun = true
|
||||
}
|
||||
args := []string{"podman", "machine", "os", "upgrade", "--host-version=" + opts.ClientVersion.String()}
|
||||
if opts.DryRun {
|
||||
args = append(args, "-n")
|
||||
}
|
||||
if opts.Format != "" {
|
||||
args = append(args, "-f", opts.Format)
|
||||
}
|
||||
if err := machine.LocalhostSSHShellForceTerm(m.VM.SSH.RemoteUsername, m.VM.SSH.IdentityPath, m.VMName, m.VM.SSH.Port, args); err != nil {
|
||||
return err
|
||||
}
|
||||
if m.Restart && !isDryRun {
|
||||
var off bool
|
||||
if err := shim.Stop(m.VM, m.Provider, off); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := shim.Start(m.VM, m.Provider, machine.StartOptions{NoInfo: true}, &off); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Machine %q restarted successfully\n", m.VMName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,8 +3,20 @@
|
||||
package os
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/sirupsen/logrus"
|
||||
"go.podman.io/image/v5/docker"
|
||||
"go.podman.io/image/v5/docker/reference"
|
||||
"go.podman.io/image/v5/image"
|
||||
"go.podman.io/image/v5/manifest"
|
||||
"go.podman.io/image/v5/types"
|
||||
)
|
||||
|
||||
// OSTree deals with operations on ostree based os's
|
||||
@@ -21,3 +33,155 @@ func (dist *OSTree) Apply(image string, _ ApplyOptions) error {
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func (dist *OSTree) Upgrade(ctx context.Context, opts UpgradeOptions) error {
|
||||
var (
|
||||
updateMessage string
|
||||
args []string
|
||||
)
|
||||
clientInfo := Host{
|
||||
Version: opts.ClientVersion,
|
||||
}
|
||||
|
||||
machineInfo := Machine{
|
||||
Version: opts.MachineVersion,
|
||||
}
|
||||
|
||||
bootcStatus, err := newBootcHost()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
originNamed, originVersion, err := bootcStatus.getLocalOSOCIInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// compareMajorMinor returns 0 if A == B, -1 if A < B, and 1 if A > B
|
||||
switch compareMajorMinor(opts.ClientVersion, opts.MachineVersion) {
|
||||
case 1:
|
||||
// if caller version < machine version, return an error because this is bad
|
||||
// this error message looks odd bc the client version %s is a machineversion, but that
|
||||
// is because of how this command is called through ssh.
|
||||
return fmt.Errorf("client version %s is older than your machine version %s: upgrade your client", opts.MachineVersion, opts.ClientVersion)
|
||||
case 0:
|
||||
// If caller version == machine version AND there is actually an update
|
||||
// on the registry, then update "in band"
|
||||
if compareMajorMinor(*originVersion, opts.ClientVersion) != 0 {
|
||||
return fmt.Errorf("version mismatch between podman version (%s) and host os (%s)", originVersion.String(), opts.ClientVersion.String())
|
||||
}
|
||||
|
||||
localDigest, err := bootcStatus.getLocalOsImageDigest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check if an in band update exists. this covers two scenarios
|
||||
// 5.7.1 -> 5.7.2
|
||||
// 5.7.1 -> newer OS image with 5.7.1
|
||||
registryDigest, updateExists, err := checkInBandUpgrade(ctx, originNamed, localDigest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
machineInfo.CurrentHash = localDigest
|
||||
machineInfo.NewHash = registryDigest
|
||||
if !updateExists {
|
||||
// If more formats are ever added, we maybe break this out to a
|
||||
// func called print() which does a switch on type and marshalls
|
||||
if opts.Format == "json" {
|
||||
return printJSON(UpgradeOutput{
|
||||
Host: clientInfo,
|
||||
Machine: machineInfo,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
machineInfo.InBandUpgradeAvailable = true
|
||||
updateMessage = fmt.Sprintf("Updating OS from %s to %s\n", localDigest.String(), registryDigest.String())
|
||||
args = []string{"bootc", "upgrade"}
|
||||
default:
|
||||
// if caller version > machine version, then update to the caller version
|
||||
newVersion := fmt.Sprintf("%d.%d", opts.MachineVersion.Major, opts.MachineVersion.Minor)
|
||||
updateReference := fmt.Sprintf("%s:%s", originNamed.Name(), newVersion)
|
||||
updateMessage = fmt.Sprintf("Updating OS from version %d.%d to %s\n", opts.ClientVersion.Major, opts.ClientVersion.Minor, newVersion)
|
||||
args = []string{"bootc", "switch", updateReference}
|
||||
}
|
||||
|
||||
if len(opts.Format) > 0 {
|
||||
return printJSON(UpgradeOutput{
|
||||
Host: clientInfo,
|
||||
Machine: machineInfo,
|
||||
})
|
||||
}
|
||||
// if you change conditions above this, pay attention to this
|
||||
// next line because you might need to wrap it in a conditional
|
||||
fmt.Print(updateMessage)
|
||||
|
||||
if opts.DryRun {
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := exec.Command("sudo", args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// compareMajorMinor returns 0 if A == B, -1 if A < B, and 1 if A > B
|
||||
func compareMajorMinor(versionA, versionB semver.Version) int {
|
||||
// ignore patch versions for comparison
|
||||
versionA.Patch = 0
|
||||
versionB.Patch = 0
|
||||
versionA.Pre = nil
|
||||
versionB.Pre = nil
|
||||
// https://pkg.go.dev/github.com/blang/semver/v4#Version.Compare
|
||||
return versionA.Compare(versionB)
|
||||
}
|
||||
|
||||
// checkInBandUpgrade takes a named and the digest of the image that made the operating system. we then check if the image
|
||||
// on the registry is different.
|
||||
func checkInBandUpgrade(ctx context.Context, named reference.Named, localHostDigest digest.Digest) (digest.Digest, bool, error) {
|
||||
sysCtx := types.SystemContext{}
|
||||
logrus.Debugf("Checking if %s has upgrade available", named.Name())
|
||||
// Lookup the image from the os
|
||||
ir, err := docker.NewReference(named)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
src, err := ir.NewImageSource(ctx, &sysCtx)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
defer src.Close()
|
||||
rawManifest, manType, err := image.UnparsedInstance(src, nil).Manifest(ctx)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
list, err := manifest.ListFromBlob(rawManifest, manType)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
// Now get the blob for the image
|
||||
d, err := list.ChooseInstance(&sysCtx)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
imageManifest, imageManType, err := image.UnparsedInstance(src, &d).Manifest(ctx)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
registryImageManifest, err := manifest.FromBlob(imageManifest, imageManType)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
return registryImageManifest.ConfigInfo().Digest, registryImageManifest.ConfigInfo().Digest != localHostDigest, nil
|
||||
}
|
||||
|
||||
func printJSON(out UpgradeOutput) error {
|
||||
b, err := json.Marshal(out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(b))
|
||||
return nil
|
||||
}
|
||||
|
||||
91
pkg/machine/os/ostree_test.go
Normal file
91
pkg/machine/os/ostree_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package os
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
)
|
||||
|
||||
func Test_compareMajorMinor(t *testing.T) {
|
||||
type args struct {
|
||||
versionA semver.Version
|
||||
versionB semver.Version
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want int
|
||||
}{
|
||||
{
|
||||
name: "equal major and minor versions and different patch",
|
||||
args: args{
|
||||
versionA: semver.MustParse("1.2.3"),
|
||||
versionB: semver.MustParse("1.2.5"),
|
||||
},
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "A major version less than B",
|
||||
args: args{
|
||||
versionA: semver.MustParse("1.5.0"),
|
||||
versionB: semver.MustParse("2.5.0"),
|
||||
},
|
||||
want: -1,
|
||||
},
|
||||
{
|
||||
name: "A major version greater than B",
|
||||
args: args{
|
||||
versionA: semver.MustParse("3.2.0"),
|
||||
versionB: semver.MustParse("2.9.0"),
|
||||
},
|
||||
want: 1,
|
||||
},
|
||||
{
|
||||
name: "A minor version less than B (same major)",
|
||||
args: args{
|
||||
versionA: semver.MustParse("1.2.0"),
|
||||
versionB: semver.MustParse("1.5.0"),
|
||||
},
|
||||
want: -1,
|
||||
},
|
||||
{
|
||||
name: "A minor version greater than B (same major)",
|
||||
args: args{
|
||||
versionA: semver.MustParse("1.8.0"),
|
||||
versionB: semver.MustParse("1.3.0"),
|
||||
},
|
||||
want: 1,
|
||||
},
|
||||
{
|
||||
name: "completely equal versions",
|
||||
args: args{
|
||||
versionA: semver.MustParse("1.2.3"),
|
||||
versionB: semver.MustParse("1.2.3"),
|
||||
},
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "zero versions",
|
||||
args: args{
|
||||
versionA: semver.MustParse("0.0.0"),
|
||||
versionB: semver.MustParse("0.0.1"),
|
||||
},
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "A is zero, B is not",
|
||||
args: args{
|
||||
versionA: semver.MustParse("0.0.0"),
|
||||
versionB: semver.MustParse("1.0.0"),
|
||||
},
|
||||
want: -1,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := compareMajorMinor(tt.args.versionA, tt.args.versionB); got != tt.want {
|
||||
t.Errorf("compareMajorMinor() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user