diff --git a/cmd/imageOutdated.go b/cmd/imageOutdated.go index 948ce49..2e3e9bb 100644 --- a/cmd/imageOutdated.go +++ b/cmd/imageOutdated.go @@ -1,20 +1,18 @@ package cmd import ( - "log" + "log/slog" "github.com/spf13/cobra" "github.com/ublue-os/uupd/drv" ) func ImageOutdated(cmd *cobra.Command, args []string) { - systemDriver, err := drv.GetSystemUpdateDriver() + systemUpdater, err := drv.SystemUpdater{}.New(drv.UpdaterInitConfiguration{}) if err != nil { - log.Fatalf("Failed to get system update driver: %v", err) + slog.Error("Failed getting system driver", slog.Any("error", err)) + return } - outdated, err := systemDriver.ImageOutdated() - if err != nil { - log.Fatalf("Cannot determine if image is outdated: %v", err) - } - log.Printf("%t", outdated) + + println(systemUpdater.Outdated) } diff --git a/cmd/update.go b/cmd/update.go index 6de9a19..24c59c9 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -62,20 +62,6 @@ func Update(cmd *cobra.Command, args []string) { initConfiguration.DryRun = dryRun initConfiguration.Verbose = verboseRun - systemUpdater, err := drv.SystemUpdater{}.New(*initConfiguration) - if err != nil { - systemUpdater.Config.Enabled = false - } else { - enableUpd, err := systemUpdater.Check() - if err != nil { - slog.Error("Failed checking for updates") - } - systemUpdater.Config.Enabled = enableUpd - if !enableUpd { - slog.Debug("No system update found, disabiling module") - } - } - brewUpdater, err := drv.BrewUpdater{}.New(*initConfiguration) brewUpdater.Config.Enabled = err == nil @@ -87,7 +73,49 @@ func Update(cmd *cobra.Command, args []string) { distroboxUpdater.Config.Enabled = err == nil distroboxUpdater.SetUsers(users) - totalSteps := brewUpdater.Steps() + systemUpdater.Steps() + flatpakUpdater.Steps() + distroboxUpdater.Steps() + var enableUpd bool = true + var systemOutdated bool + + rpmOstreeUpdater, err := drv.RpmOstreeUpdater{}.New(*initConfiguration) + if err != nil { + enableUpd = false + } + + systemUpdater, err := drv.SystemUpdater{}.New(*initConfiguration) + if err != nil { + enableUpd = false + } + + isBootc, err := drv.BootcCompatible(systemUpdater.BinaryPath) + if err != nil { + isBootc = false + } + + if !isBootc { + slog.Debug("Using rpm-ostree fallback as system driver") + } + + systemUpdater.Config.Enabled = enableUpd && isBootc + rpmOstreeUpdater.Config.Enabled = enableUpd && !isBootc + + var mainSystemDriver drv.SystemUpdateDriver = systemUpdater + if !systemUpdater.Config.Enabled { + mainSystemDriver = rpmOstreeUpdater + } + + enableUpd, err = mainSystemDriver.Check() + if err != nil { + slog.Error("Failed checking for updates") + } + + if !enableUpd { + slog.Debug("No system update found, disabiling module") + } + + totalSteps := brewUpdater.Steps() + flatpakUpdater.Steps() + distroboxUpdater.Steps() + if enableUpd { + totalSteps += mainSystemDriver.Steps() + } pw := lib.NewProgressWriter() pw.SetNumTrackersExpected(1) pw.SetAutoStop(false) @@ -118,7 +146,13 @@ func Update(cmd *cobra.Command, args []string) { var outputs = []drv.CommandOutput{} - if systemUpdater.Outdated { + systemOutdated, err = mainSystemDriver.Outdated() + + if err != nil { + slog.Error("Failed checking if system is out of date") + } + + if systemOutdated { const OUTDATED_WARNING = "There hasn't been an update in over a month. Consider rebooting or running updates manually" err := lib.Notify("System Warning", OUTDATED_WARNING) if err != nil { @@ -127,9 +161,10 @@ func Update(cmd *cobra.Command, args []string) { slog.Warn(OUTDATED_WARNING) } - if systemUpdater.Config.Enabled { + if enableUpd { lib.ChangeTrackerMessageFancy(pw, tracker, progressEnabled, lib.TrackerMessage{Title: systemUpdater.Config.Title, Description: systemUpdater.Config.Description}) - out, err := systemUpdater.Update() + var out *[]drv.CommandOutput + out, err = mainSystemDriver.Update() outputs = append(outputs, *out...) tracker.IncrementSection(err) } diff --git a/cmd/updateCheck.go b/cmd/updateCheck.go index 8b08307..377d295 100644 --- a/cmd/updateCheck.go +++ b/cmd/updateCheck.go @@ -1,19 +1,26 @@ package cmd import ( + "log/slog" + "github.com/spf13/cobra" "github.com/ublue-os/uupd/drv" - "log" ) func UpdateCheck(cmd *cobra.Command, args []string) { - systemDriver, err := drv.GetSystemUpdateDriver() + systemUpdater, err := drv.SystemUpdater{}.New(drv.UpdaterInitConfiguration{}) if err != nil { - log.Fatalf("Failed to get system update driver: %v", err) + slog.Error("Failed getting system driver", slog.Any("error", err)) + return } - update, err := systemDriver.UpdateAvailable() + updateAvailable, err := systemUpdater.Check() if err != nil { - log.Fatalf("Failed to check for updates: %v", err) + slog.Error("Failed checking for updates", slog.Any("error", err)) + return + } + if updateAvailable { + slog.Info("Update Available") + } else { + slog.Info("No updates available") } - log.Printf("Update Available: %v", update) } diff --git a/drv/bootc.go b/drv/bootc.go deleted file mode 100644 index 8c5a820..0000000 --- a/drv/bootc.go +++ /dev/null @@ -1,157 +0,0 @@ -package drv - -import ( - "encoding/json" - "os/exec" - "strings" - "time" -) - -// implementation of bootc and rpm-ostree commands (rpm-ostree support will be removed in the future) - -type bootcStatus struct { - Status struct { - Booted struct { - Incompatible bool `json:"incompatible"` - Image struct { - Timestamp string `json:"timestamp"` - } `json:"image"` - } `json:"booted"` - Staged struct { - Incompatible bool `json:"incompatible"` - Image struct { - Timestamp string `json:"timestamp"` - } `json:"image"` - } - } `json:"status"` -} - -type rpmOstreeStatus struct { - Deployments []struct { - Timestamp int64 `json:"timestamp"` - } `json:"deployments"` -} - -func BootcCompat() (bool, error) { - cmd := exec.Command("bootc", "status", "--format=json") - out, err := cmd.CombinedOutput() - if err != nil { - return false, nil - } - var status bootcStatus - err = json.Unmarshal(out, &status) - if err != nil { - return false, nil - } - return !(status.Status.Booted.Incompatible || status.Status.Staged.Incompatible), nil -} - -func IsBootcImageOutdated() (bool, error) { - cmd := exec.Command("bootc", "status", "--format=json") - out, err := cmd.CombinedOutput() - if err != nil { - return false, err - } - var status bootcStatus - err = json.Unmarshal(out, &status) - if err != nil { - return false, err - } - timestamp, err := time.Parse(time.RFC3339Nano, status.Status.Booted.Image.Timestamp) - if err != nil { - return false, nil - } - oneMonthAgo := time.Now().UTC().AddDate(0, -1, 0) - - return timestamp.Before(oneMonthAgo), nil -} - -func BootcUpdate() ([]byte, error) { - cmd := exec.Command("/usr/bin/bootc", "upgrade") - out, err := cmd.CombinedOutput() - if err != nil { - return out, err - } - return out, nil -} - -func CheckForBootcImageUpdate() (bool, error) { - cmd := exec.Command("/usr/bin/bootc", "upgrade", "--check") - out, err := cmd.CombinedOutput() - if err != nil { - return true, err - } - return !strings.Contains(string(out), "No changes in:"), nil -} - -func IsRpmOstreeImageOutdated() (bool, error) { - cmd := exec.Command("rpm-ostree", "status", "--json", "--booted") - out, err := cmd.CombinedOutput() - if err != nil { - return false, err - } - var status rpmOstreeStatus - err = json.Unmarshal(out, &status) - if err != nil { - return false, err - } - timestamp := time.Unix(status.Deployments[0].Timestamp, 0).UTC() - oneMonthAgo := time.Now().AddDate(0, -1, 0) - - return timestamp.Before(oneMonthAgo), nil -} - -func RpmOstreeUpdate() ([]byte, error) { - cmd := exec.Command("/usr/bin/rpm-ostree", "upgrade") - out, err := cmd.CombinedOutput() - if err != nil { - return out, err - } - return out, nil -} - -func CheckForRpmOstreeImageUpdate() (bool, error) { - // This function may or may not be accurate, rpm-ostree updgrade --check has issues... https://github.com/coreos/rpm-ostree/issues/1579 - // Not worried because we will end up removing rpm-ostree from the equation soon - cmd := exec.Command("/usr/bin/rpm-ostree", "upgrade", "--check") - out, err := cmd.CombinedOutput() - if err != nil { - return true, err - } - return strings.Contains(string(out), "AvailableUpdate"), nil -} - -// Generalize bootc and rpm-ostree drivers into this struct as system updaters -type SystemUpdateDriver struct { - ImageOutdated func() (bool, error) - Update func() ([]byte, error) - UpdateAvailable func() (bool, error) - Name string -} - -func GetSystemUpdateDriver() (SystemUpdateDriver, error) { - useBootc, err := BootcCompat() - if err != nil { - // bootc isn't on the current system if there's an error - return SystemUpdateDriver{ - IsRpmOstreeImageOutdated, - RpmOstreeUpdate, - CheckForRpmOstreeImageUpdate, - "rpm-ostree", - }, nil - } - if useBootc { - return SystemUpdateDriver{ - IsBootcImageOutdated, - BootcUpdate, - CheckForBootcImageUpdate, - "Bootc", - }, nil - } - return SystemUpdateDriver{ - IsRpmOstreeImageOutdated, - RpmOstreeUpdate, - CheckForRpmOstreeImageUpdate, - "rpm-ostree", - }, nil -} diff --git a/drv/rpm-ostree.go b/drv/rpm-ostree.go new file mode 100644 index 0000000..8eae935 --- /dev/null +++ b/drv/rpm-ostree.go @@ -0,0 +1,121 @@ +package drv + +// Temporary: WILL get removed at some point. +// FIXME: Remove this on Spring 2025 when we all move to dnf5 and bootc ideally + +import ( + "encoding/json" + "os/exec" + "strings" + "time" +) + +type rpmOstreeStatus struct { + Deployments []struct { + Timestamp int64 `json:"timestamp"` + } `json:"deployments"` +} + +type RpmOstreeUpdater struct { + Config DriverConfiguration + BinaryPath string +} + +func (dr RpmOstreeUpdater) Outdated() (bool, error) { + if dr.Config.DryRun { + return false, nil + } + oneMonthAgo := time.Now().AddDate(0, -1, 0) + var timestamp time.Time + + cmd := exec.Command(dr.BinaryPath, "status", "--json", "--booted") + out, err := cmd.CombinedOutput() + if err != nil { + return false, err + } + var status rpmOstreeStatus + err = json.Unmarshal(out, &status) + if err != nil { + return false, err + } + timestamp = time.Unix(status.Deployments[0].Timestamp, 0).UTC() + + return timestamp.Before(oneMonthAgo), nil +} + +func (dr RpmOstreeUpdater) Update() (*[]CommandOutput, error) { + var finalOutput = []CommandOutput{} + var cmd *exec.Cmd + binaryPath := dr.BinaryPath + cli := []string{binaryPath, "upgrade"} + cmd = exec.Command(cli[0], cli[1:]...) + out, err := cmd.CombinedOutput() + tmpout := CommandOutput{}.New(out, err) + // tmpout.Cli = cli + tmpout.Failure = err != nil + tmpout.Context = "System Update" + finalOutput = append(finalOutput, *tmpout) + return &finalOutput, err +} + +func (dr RpmOstreeUpdater) UpdateAvailable() (bool, error) { + // This function may or may not be accurate, rpm-ostree updgrade --check has issues... https://github.com/coreos/rpm-ostree/issues/1579 + // Not worried because we will end up removing rpm-ostree from the equation soon + cmd := exec.Command(dr.BinaryPath, "upgrade", "--check") + out, err := cmd.CombinedOutput() + if err != nil { + return true, err + } + return strings.Contains(string(out), "AvailableUpdate"), nil +} + +func (up RpmOstreeUpdater) Steps() int { + if up.Config.Enabled { + return 1 + } + return 0 +} + +func BootcCompatible(binaryPath string) (bool, error) { + cmd := exec.Command(binaryPath, "status", "--format=json") + out, err := cmd.CombinedOutput() + if err != nil { + return false, nil + } + var status bootcStatus + err = json.Unmarshal(out, &status) + if err != nil { + return false, nil + } + return !(status.Status.Booted.Incompatible || status.Status.Staged.Incompatible), nil +} + +func (up RpmOstreeUpdater) New(config UpdaterInitConfiguration) (RpmOstreeUpdater, error) { + up.Config = DriverConfiguration{ + Title: "System", + Description: "System Updates", + Enabled: !config.Ci, + DryRun: config.DryRun, + } + + if up.Config.DryRun { + return up, nil + } + + binaryPath, exists := config.Environment["UUPD_RPMOSTREE_BINARY"] + if !exists || binaryPath == "" { + up.BinaryPath = "/usr/bin/rpm-ostree" + } else { + up.BinaryPath = binaryPath + } + + return up, nil +} + +func (up RpmOstreeUpdater) Check() (bool, error) { + if up.Config.DryRun { + return true, nil + } + + return up.UpdateAvailable() +} diff --git a/drv/system.go b/drv/system.go index d5883b7..21d4fec 100644 --- a/drv/system.go +++ b/drv/system.go @@ -1,10 +1,89 @@ package drv +import ( + "encoding/json" + "os/exec" + "strings" + "time" +) + +type bootcStatus struct { + Status struct { + Booted struct { + Incompatible bool `json:"incompatible"` + Image struct { + Timestamp string `json:"timestamp"` + } `json:"image"` + } `json:"booted"` + Staged struct { + Incompatible bool `json:"incompatible"` + Image struct { + Timestamp string `json:"timestamp"` + } `json:"image"` + } + } `json:"status"` +} + +// Workaround interface to decouple individual drivers +// (TODO: Remove this whenever rpm-ostree driver gets deprecated) +type SystemUpdateDriver interface { + Steps() int + Outdated() (bool, error) + UpdateAvailable() (bool, error) + Check() (bool, error) + Update() (*[]CommandOutput, error) +} + type SystemUpdater struct { - Config DriverConfiguration - SystemDriver SystemUpdateDriver - Outdated bool - UpdateAvailable bool + Config DriverConfiguration + BinaryPath string +} + +func (dr SystemUpdater) Outdated() (bool, error) { + if dr.Config.DryRun { + return false, nil + } + oneMonthAgo := time.Now().AddDate(0, -1, 0) + var timestamp time.Time + cmd := exec.Command(dr.BinaryPath, "status", "--format=json") + out, err := cmd.CombinedOutput() + if err != nil { + return false, err + } + var status bootcStatus + err = json.Unmarshal(out, &status) + if err != nil { + return false, err + } + timestamp, err = time.Parse(time.RFC3339Nano, status.Status.Booted.Image.Timestamp) + if err != nil { + return false, nil + } + return timestamp.Before(oneMonthAgo), nil +} + +func (dr SystemUpdater) Update() (*[]CommandOutput, error) { + var finalOutput = []CommandOutput{} + var cmd *exec.Cmd + binaryPath := dr.BinaryPath + cli := []string{binaryPath, "upgrade"} + cmd = exec.Command(cli[0], cli[1:]...) + out, err := cmd.CombinedOutput() + tmpout := CommandOutput{}.New(out, err) + if err != nil { + tmpout.SetFailureContext("System update") + } + finalOutput = append(finalOutput, *tmpout) + return &finalOutput, err +} + +func (dr SystemUpdater) UpdateAvailable() (bool, error) { + cmd := exec.Command(dr.BinaryPath, "upgrade", "--check") + out, err := cmd.CombinedOutput() + if err != nil { + return true, err + } + return !strings.Contains(string(out), "No changes in:"), nil } func (up SystemUpdater) Steps() int { @@ -14,55 +93,32 @@ func (up SystemUpdater) Steps() int { return 0 } -func (up SystemUpdater) New(initconfig UpdaterInitConfiguration) (SystemUpdater, error) { +func (up SystemUpdater) New(config UpdaterInitConfiguration) (SystemUpdater, error) { up.Config = DriverConfiguration{ Title: "System", Description: "System Updates", - Enabled: !initconfig.Ci, - DryRun: initconfig.DryRun, + Enabled: !config.Ci, + DryRun: config.DryRun, } if up.Config.DryRun { - up.Outdated = false return up, nil } - systemDriver, err := GetSystemUpdateDriver() - if err != nil { - return up, err + bootcBinaryPath, exists := config.Environment["UUPD_BOOTC_BINARY"] + if !exists || bootcBinaryPath == "" { + up.BinaryPath = "/usr/bin/bootc" + } else { + up.BinaryPath = bootcBinaryPath } - up.SystemDriver = systemDriver - outdated, err := up.SystemDriver.ImageOutdated() - if err != nil { - return up, err - } - - up.Outdated = outdated return up, nil } -func (up *SystemUpdater) Check() (bool, error) { +func (up SystemUpdater) Check() (bool, error) { if up.Config.DryRun { return true, nil } - updateAvailable, err := up.SystemDriver.UpdateAvailable() - return updateAvailable, err -} - -func (up SystemUpdater) Update() (*[]CommandOutput, error) { - var final_output = []CommandOutput{} - - if up.Config.DryRun { - return &final_output, nil - } - - out, err := up.SystemDriver.Update() - tmpout := CommandOutput{}.New(out, err) - if err != nil { - tmpout.SetFailureContext("System update") - } - final_output = append(final_output, *tmpout) - return &final_output, nil + return up.UpdateAvailable() }