Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

System Upgrades #81

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions cmd/_dbxroot/cmd/dbx-upgrade.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package cmd

import (
"fmt"
"log"
"os"
"strings"

"github.com/dogeorg/dogeboxd/cmd/_dbxroot/utils"
"github.com/dogeorg/dogeboxd/pkg/system"
"github.com/spf13/cobra"
)

var dbxUpgradeCmd = &cobra.Command{
Use: "dbx-upgrade",
Short: "Upgrade Dogebox to a specific release.",
Long: `Upgrade Dogebox to a specific release.
This command requires --release flag.

Example:
_dbxroot dbx-upgrade --package dogebox --release v0.5.0-beta`,
Run: func(cmd *cobra.Command, args []string) {
pkg, _ := cmd.Flags().GetString("package")
release, _ := cmd.Flags().GetString("release")

if pkg != "dogebox" {
log.Printf("Invalid package to upgrade: %s", pkg)
os.Exit(1)
}

if release == "" {
log.Printf("Release tag is required")
os.Exit(1)
}

upgradableReleases, err := system.GetUpgradableReleases()
if err != nil {
log.Printf("Failed to get upgradable releases: %v", err)
os.Exit(1)
}

ok := false
for _, upgradableRelease := range upgradableReleases {
if upgradableRelease.Version == release {
ok = true
break
}
}

if !ok {
log.Printf("Release %s is not available for %s", release, pkg)
os.Exit(1)
}

existingChannels := utils.RunCommand("nix-channel", "--list")

channels := map[string]string{}
for _, line := range strings.Split(strings.TrimSpace(existingChannels), "\n") {
parts := strings.Fields(line)
if len(parts) >= 2 {
channels[parts[0]] = parts[1]
}
}

if _, ok := channels["dogebox"]; !ok {
log.Printf("Dogebox channel does not exist. Aborting. Please raise an issue on Github.")
os.Exit(1)
}

defer func() {
if r := recover(); r != nil {
log.Printf("Failed to update system: %v", r)
log.Printf("Trying to re-add old channels")

os.Exit(1)
}
}()

utils.RunCommand("nix-channel", "--remove", "dogebox")
utils.RunCommand("nix-channel", "--add", fmt.Sprintf("https://github.com/dogeorg/dogebox-nur-packages/archive/%s.tar.gz", release), "dogebox")
utils.RunCommand("nix-channel", "--update", "dogebox")
utils.RunCommand("nixos-rebuild", "switch")
},
}

func init() {
rootCmd.AddCommand(dbxUpgradeCmd)
dbxUpgradeCmd.Flags().StringP("package", "p", "", "Package to upgrade (required)")
dbxUpgradeCmd.Flags().StringP("release", "r", "", "Release tag to upgrade to (required)")
dbxUpgradeCmd.MarkFlagRequired("package")
dbxUpgradeCmd.MarkFlagRequired("release")
}
2 changes: 2 additions & 0 deletions cmd/dogeboxd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ func (t server) Start() {
rest := web.RESTAPI(t.config, t.sm, dbx, pups, sourceManager, lifecycleManager, nixManager, dkm, wsh)
internalRouter := web.NewInternalRouter(t.config, dbx, pups, dkm)
ui := dogeboxd.ServeUI(t.config)
updateChecker := web.NewUpdateChecker(&dbx)

/* ----------------------------------------------------------------------- */
// Create a conductor to manage all the above services startup/shutdown
Expand All @@ -103,6 +104,7 @@ func (t server) Start() {
c.Service("REST API", rest)
c.Service("UI Server", ui)
c.Service("System Updater", systemUpdater)
c.Service("Update Checker", updateChecker)

if !t.config.Recovery {
c.Service("System Monitor", systemMonitor)
Expand Down
7 changes: 7 additions & 0 deletions pkg/dogeboxd.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,9 @@ func (t Dogeboxd) jobDispatcher(j Job) {
case RemoveSSHKey:
t.enqueue(j)

case SystemUpdateRequest:
t.enqueue(j)

// Pup router actions
case UpdateMetrics:
t.Pups.UpdateMetrics(a)
Expand Down Expand Up @@ -421,6 +424,10 @@ func (t Dogeboxd) sendProgress(p ActionProgress) {
t.sendChange(Change{ID: p.ActionID, Type: "progress", Update: p})
}

func (t Dogeboxd) SendSystemUpdateAvailable() {
t.sendChange(Change{ID: "system", Type: "system-update-available", Update: true})
}

// helper to attach PupState to a job and send it to the SystemUpdater
func (t Dogeboxd) sendSystemJobWithPupDetails(j Job, PupID string) {
p, _, err := t.Pups.GetPup(PupID)
Expand Down
5 changes: 5 additions & 0 deletions pkg/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ type RemoveSSHKey struct {
ID string
}

type SystemUpdateRequest struct {
Package string
Version string
}

/* Updates are responses to Actions or simply
* internal state changes that the frontend needs,
* these are wrapped in a 'change' and sent via
Expand Down
13 changes: 13 additions & 0 deletions pkg/system/updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@ func (t SystemUpdater) Run(started, stopped chan bool, stop chan context.Context
}
t.done <- j

case dogeboxd.SystemUpdateRequest:
err := t.doSystemUpdate(a.Package, a.Version, j)
if err != nil {
j.Err = "Failed to do system update"
}
t.done <- j

default:
fmt.Printf("Unknown action type: %v\n", a)
}
Expand Down Expand Up @@ -388,3 +395,9 @@ func (t SystemUpdater) disablePup(j dogeboxd.Job) error {

return nil
}

func (t SystemUpdater) doSystemUpdate(packageName, version string, j dogeboxd.Job) error {
log := j.Logger.Step("System Update")
log.Logf("Updating system package %s to %s", packageName, version)
return DoSystemUpdate(packageName, version)
}
114 changes: 114 additions & 0 deletions pkg/system/updates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package system

import (
"fmt"
"log"
"os"
"os/exec"

"github.com/dogeorg/dogeboxd/pkg/version"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/storage/memory"
"golang.org/x/mod/semver"
)

const RELEASE_NUR_REPO = "https://github.com/dogeorg/dogebox-nur-packages.git"

type RepositoryTag struct {
Tag string
}

func getRepoTags(repo string) ([]RepositoryTag, error) {
rem := git.NewRemote(memory.NewStorage(), &config.RemoteConfig{
Name: "origin",
URLs: []string{repo},
})

refs, err := rem.List(&git.ListOptions{
PeelingOption: git.AppendPeeled,
})
if err != nil {
log.Printf("Failed to get repo %s tags: %v", repo, err)
return []RepositoryTag{}, err
}

var tags []RepositoryTag
for _, ref := range refs {
if ref.Name().IsTag() && semver.IsValid(ref.Name().Short()) {
tags = append(tags, RepositoryTag{
Tag: ref.Name().Short(),
})
}
}

return tags, nil
}

type UpgradableRelease struct {
Version string
ReleaseURL string
Summary string
}

func GetUpgradableReleases() ([]UpgradableRelease, error) {
dbxRelease := version.GetDBXRelease()

tags, err := getRepoTags(RELEASE_NUR_REPO)
if err != nil {
return []UpgradableRelease{}, err
}

unsupportedUpgradesEnabled := os.Getenv("ENABLE_UNSUPPORTED_UPGRADES") == "true"

var upgradableTags []UpgradableRelease
for _, tag := range tags {
release := UpgradableRelease{
Version: tag.Tag,
ReleaseURL: fmt.Sprintf("https://github.com/dogeorg/dogebox/releases/tag/%s", tag.Tag),
Summary: "Update for Dogeboxd / DKM / DPanel",
}

// Allow any version to be displayed if the user has enabled unsupported upgrades.
// We probably want to limit this eventually, but for now it's useful for testing.
if unsupportedUpgradesEnabled || semver.Compare(tag.Tag, dbxRelease.Release) > 0 {
upgradableTags = append(upgradableTags, release)
}
}

return upgradableTags, nil
}

func DoSystemUpdate(pkg string, updateVersion string) error {
upgradableReleases, err := GetUpgradableReleases()
if err != nil {
return err
}

// We _only_ support the dogebox package for now.
if pkg != "dogebox" {
return fmt.Errorf("Invalid package to upgrade: %s", pkg)
}

ok := false
for _, release := range upgradableReleases {
if release.Version == updateVersion {
ok = true
break
}
}

if !ok {
return fmt.Errorf("Release %s is not available for %s", updateVersion, pkg)
}

cmd := exec.Command("sudo", "_dbxroot", "dbx-upgrade", "--package", pkg, "--release", updateVersion)
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to run dbx-upgrade: %w", err)
}

// We might not even get here if dogeboxd is restarted/upgraded during this process.
return nil
}
21 changes: 14 additions & 7 deletions pkg/web/rest.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,22 @@ func RESTAPI(
"POST /pup/{ID}/{action}": a.pupAction,
"PUT /pup": a.installPup,
"POST /config/{PupID}": a.updateConfig,

"POST /providers/{PupID}": a.updateProviders,
"GET /providers/{PupID}": a.getPupProviders,
"POST /hooks/{PupID}": a.updateHooks,
"GET /sources": a.getSources,
"PUT /source": a.createSource,
"GET /sources/store": a.getStoreList,
"DELETE /source/{id}": a.deleteSource,
"/ws/log/{PupID}": a.getLogSocket,
"/ws/state/": a.getUpdateSocket,

"POST /hooks/{PupID}": a.updateHooks,

"GET /sources": a.getSources,
"PUT /source": a.createSource,
"GET /sources/store": a.getStoreList,
"DELETE /source/{id}": a.deleteSource,

"GET /system/updates": a.getSystemUpdatesForWeb,
"POST /system/update": a.doSystemUpdate,

"/ws/log/{PupID}": a.getLogSocket,
"/ws/state/": a.getUpdateSocket,
}

// We always want to load recovery routes.
Expand Down
Loading