Skip to content

Commit

Permalink
add /api/v1/admin/updates/install endpoint to trigger update
Browse files Browse the repository at this point in the history
  • Loading branch information
sni committed Oct 8, 2024
1 parent 596ed06 commit 498bae2
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 32 deletions.
1 change: 1 addition & 0 deletions Changes
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ next:
- add /api/v1/inventory/{module} rest endpoint to get specific inventory
- add exporter to inventory list
- fix updates from custom urls
- add /api/v1/admin/updates/install endpoint to trigger update

0.27 Mon Sep 2 19:31:14 CEST 2024
- do not use empty-state if warn/crit conditions contain check on 'count'
Expand Down
26 changes: 25 additions & 1 deletion docs/api/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,12 @@ Returns metrics for given exporter.

These endpoints are available if the `WEBAdminServer` is enabled in the modules section.

It is best practice to use a separate password for the administrative tasks.
It is best practice to use a separate password for the administrative tasks, for example:

```ini
[/settings/WEBAdmin/server]
password = mysecretadminpassword
```

### /api/v1/admin/reload

Expand Down Expand Up @@ -186,3 +191,22 @@ Example:
Returns

{"success":true}

### /api/v1/admin/updates/install

Trigger checking for updates.

Example:

curl \
-u user:changeme \
-X POST \
https://127.0.0.1:8443/api/v1/admin/updates/install

Returns

{
"success": true,
"message": "update found and installed",
"version": "v0.27.0024"
}
2 changes: 2 additions & 0 deletions pkg/snclient/commands/update.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package commands

import (
"context"
"fmt"
"os"
"strings"
Expand Down Expand Up @@ -75,6 +76,7 @@ func runUpdates(cmd *cobra.Command, args []string) {
preRelease := convert.Bool(cmd.Flag("prerelease").Value.String())
force := convert.Bool(cmd.Flag("force").Value.String())
version, err := mod.CheckUpdates(
context.TODO(),
true,
!checkOnly,
false,
Expand Down
38 changes: 38 additions & 0 deletions pkg/snclient/listen_web_admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ func (l *HandlerWebAdmin) ServeHTTP(res http.ResponseWriter, req *http.Request)
l.serveReload(res, req)
case "/api/v1/admin/certs/replace":
l.serveCertsReplace(res, req)
case "/api/v1/admin/updates/install":
l.serveUpdate(res, req)
default:
res.WriteHeader(http.StatusNotFound)
LogError2(res.Write([]byte("404 - nothing here\n")))
Expand Down Expand Up @@ -211,6 +213,42 @@ func (l *HandlerWebAdmin) serveCertsReplace(res http.ResponseWriter, req *http.R
}
}

func (l *HandlerWebAdmin) serveUpdate(res http.ResponseWriter, req *http.Request) {
if !l.requirePostMethod(res, req) {
return
}

task := l.Handler.snc.Tasks.Get("Updates")
mod, ok := task.(*UpdateHandler)
if !ok {
l.sendError(res, fmt.Errorf("could not load update handler"))

return
}

version, err := mod.CheckUpdates(req.Context(), true, true, true, false, "", "", false)
if err != nil {
l.sendError(res, fmt.Errorf("failed to fetch updates: %s", err.Error()))

return
}

res.Header().Set("Content-Type", "application/json")
res.WriteHeader(http.StatusOK)
if version != "" {
LogError(json.NewEncoder(res).Encode(map[string]interface{}{
"success": true,
"message": "update found and installed",
"version": version,
}))
} else {
LogError(json.NewEncoder(res).Encode(map[string]interface{}{
"success": true,
"message": "no new update available",
}))
}
}

// check if request used method POST
func (l *HandlerWebAdmin) requirePostMethod(res http.ResponseWriter, req *http.Request) bool {
if req.Method == http.MethodPost {
Expand Down
62 changes: 31 additions & 31 deletions pkg/snclient/task_updates.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ func (u *UpdateHandler) mainLoop() {
return
case <-ticker.C:
ticker.Reset(interval)
_, err := u.CheckUpdates(false, true, u.automaticRestart, u.preRelease, "", u.channel, false)
_, err := u.CheckUpdates(*u.ctx, false, true, u.automaticRestart, u.preRelease, "", u.channel, false)
if err != nil {
log.Errorf("[updates] checking for updates failed: %s", err.Error())
}
Expand All @@ -215,7 +215,7 @@ func (u *UpdateHandler) mainLoop() {
}
}

func (u *UpdateHandler) CheckUpdates(force, download, restarts, preRelease bool, downgrade, channel string, forceUpdate bool) (version string, err error) {
func (u *UpdateHandler) CheckUpdates(ctx context.Context, force, download, restarts, preRelease bool, downgrade, channel string, forceUpdate bool) (version string, err error) {
if !force {
if !u.updatePreChecks() {
return "", nil
Expand Down Expand Up @@ -245,7 +245,7 @@ func (u *UpdateHandler) CheckUpdates(force, download, restarts, preRelease bool,

// check for updates unless file specified
if updateFile == "" {
available := u.fetchAvailableUpdates(preRelease, channel)
available := u.fetchAvailableUpdates(ctx, preRelease, channel)
if len(available) == 0 {
return "", nil
}
Expand All @@ -260,16 +260,16 @@ func (u *UpdateHandler) CheckUpdates(force, download, restarts, preRelease bool,
}
}

return u.finishUpdateCheck(best, restarts)
return u.finishUpdateCheck(ctx, best, restarts)
}

func (u *UpdateHandler) finishUpdateCheck(best *updatesAvailable, restarts bool) (version string, err error) {
updateFile, err := u.downloadUpdate(best)
func (u *UpdateHandler) finishUpdateCheck(ctx context.Context, best *updatesAvailable, restarts bool) (version string, err error) {
updateFile, err := u.downloadUpdate(ctx, best)
if err != nil {
return "", err
}

newVersion, err := u.verifyUpdate(updateFile)
newVersion, err := u.verifyUpdate(ctx, updateFile)
if err != nil {
LogError(os.Remove(updateFile))

Expand Down Expand Up @@ -356,7 +356,7 @@ func (u *UpdateHandler) chooseBestUpdate(updates []updatesAvailable, downgrade s
return best
}

func (u *UpdateHandler) fetchAvailableUpdates(preRelease bool, channel string) (updates []updatesAvailable) {
func (u *UpdateHandler) fetchAvailableUpdates(ctx context.Context, preRelease bool, channel string) (updates []updatesAvailable) {
available := []updatesAvailable{}
channelConfSection := u.snc.config.Section("/settings/updates/channel")
if channel == "all" {
Expand All @@ -378,7 +378,7 @@ func (u *UpdateHandler) fetchAvailableUpdates(preRelease bool, channel string) (

log.Tracef("next: %s channel: %s", channel, url)

updates, err := u.checkUpdate(url, preRelease, channel)
updates, err := u.checkUpdate(ctx, url, preRelease, channel)
if err != nil {
log.Warnf("channel %s failed: %s", channel, err.Error())

Expand All @@ -391,15 +391,15 @@ func (u *UpdateHandler) fetchAvailableUpdates(preRelease bool, channel string) (
return available
}

func (u *UpdateHandler) checkUpdate(url string, preRelease bool, channel string) (updates []updatesAvailable, err error) {
func (u *UpdateHandler) checkUpdate(ctx context.Context, url string, preRelease bool, channel string) (updates []updatesAvailable, err error) {
if ok, _ := regexp.MatchString(`^https://api\.github\.com/repos/.*/releases`, url); ok {
updates, err = u.checkUpdateGithubRelease(url, channel, preRelease)
updates, err = u.checkUpdateGithubRelease(ctx, url, channel, preRelease)
} else if ok, _ := regexp.MatchString(`^https://api\.github\.com/repos/.*/actions/artifacts`, url); ok {
updates, err = u.checkUpdateGithubActions(url, channel)
updates, err = u.checkUpdateGithubActions(ctx, url, channel)
} else if ok, _ := regexp.MatchString(`^file:`, url); ok {
updates, err = u.checkUpdateFile(url)
updates, err = u.checkUpdateFile(ctx, url)
} else {
updates, err = u.checkUpdateCustomURL(url)
updates, err = u.checkUpdateCustomURL(ctx, url)
}

if err != nil {
Expand All @@ -420,7 +420,7 @@ func (u *UpdateHandler) checkUpdate(url string, preRelease bool, channel string)
}

// check available updates from github release page
func (u *UpdateHandler) checkUpdateGithubRelease(url, channel string, preRelease bool) (updates []updatesAvailable, err error) {
func (u *UpdateHandler) checkUpdateGithubRelease(ctx context.Context, url, channel string, preRelease bool) (updates []updatesAvailable, err error) {
log.Tracef("[update] checking github release url at: %s", url)

conf := u.snc.config.Section("/settings/updates/channel/" + channel)
Expand All @@ -432,7 +432,7 @@ func (u *UpdateHandler) checkUpdateGithubRelease(url, channel string, preRelease
header["Authorization"] = "Bearer " + token
}

resp, err := u.snc.httpDo(*u.ctx, u.httpOptions, "GET", url, header)
resp, err := u.snc.httpDo(ctx, u.httpOptions, "GET", url, header)
if err != nil {
return nil, fmt.Errorf("http: %s", err.Error())
}
Expand Down Expand Up @@ -487,7 +487,7 @@ func (u *UpdateHandler) checkUpdateGithubRelease(url, channel string, preRelease
}

// check available updates from github actions page
func (u *UpdateHandler) checkUpdateGithubActions(url, channel string) (updates []updatesAvailable, err error) {
func (u *UpdateHandler) checkUpdateGithubActions(ctx context.Context, url, channel string) (updates []updatesAvailable, err error) {
log.Tracef("[update] checking github action url at: %s", url)
conf := u.snc.config.Section("/settings/updates/channel/" + channel)
token, ok := conf.GetString("github token")
Expand All @@ -498,7 +498,7 @@ func (u *UpdateHandler) checkUpdateGithubActions(url, channel string) (updates [
"Authorization": "Bearer " + token,
}
// show some more than the default 30, 100 seems to be maximum
resp, err := u.snc.httpDo(*u.ctx, u.httpOptions, "GET", url+"?per_page=100", header)
resp, err := u.snc.httpDo(ctx, u.httpOptions, "GET", url+"?per_page=100", header)
if err != nil {
return nil, fmt.Errorf("http: %s", err.Error())
}
Expand Down Expand Up @@ -554,9 +554,9 @@ func (u *UpdateHandler) checkUpdateGithubActions(url, channel string) (updates [
}

// check available update from any url
func (u *UpdateHandler) checkUpdateCustomURL(url string) (updates []updatesAvailable, err error) {
func (u *UpdateHandler) checkUpdateCustomURL(ctx context.Context, url string) (updates []updatesAvailable, err error) {
log.Tracef("[update] checking custom url at: %s", url)
resp, err := u.snc.httpDo(*u.ctx, u.httpOptions, "HEAD", url, nil)
resp, err := u.snc.httpDo(ctx, u.httpOptions, "HEAD", url, nil)
if err != nil {
return nil, fmt.Errorf("http: %s", err.Error())
}
Expand Down Expand Up @@ -613,7 +613,7 @@ func (u *UpdateHandler) checkUpdateCustomURL(url string) (updates []updatesAvail
}

log.Tracef("[update] need to refresh cache for %s", url)
version, err := u.getVersionFromURL(url)
version, err := u.getVersionFromURL(ctx, url)
if err != nil {
return nil, fmt.Errorf("failed to fetch version: %s", err.Error())
}
Expand All @@ -627,7 +627,7 @@ func (u *UpdateHandler) checkUpdateCustomURL(url string) (updates []updatesAvail
}

// check available update from local or remote filesystem
func (u *UpdateHandler) checkUpdateFile(url string) (updates []updatesAvailable, err error) {
func (u *UpdateHandler) checkUpdateFile(ctx context.Context, url string) (updates []updatesAvailable, err error) {
localPath := strings.TrimPrefix(url, "file://")
log.Tracef("[update] checking local file at: %s", localPath)
_, err = os.Stat(localPath)
Expand All @@ -654,7 +654,7 @@ func (u *UpdateHandler) checkUpdateFile(url string) (updates []updatesAvailable,
}

// get version from that executable
version, err := u.verifyUpdate(tempUpdate)
version, err := u.verifyUpdate(ctx, tempUpdate)
if err != nil {
return nil, err
}
Expand All @@ -663,7 +663,7 @@ func (u *UpdateHandler) checkUpdateFile(url string) (updates []updatesAvailable,
}

// fetch update file into tmp file
func (u *UpdateHandler) downloadUpdate(update *updatesAvailable) (binPath string, err error) {
func (u *UpdateHandler) downloadUpdate(ctx context.Context, update *updatesAvailable) (binPath string, err error) {
url := update.url
var src io.ReadCloser
if strings.HasPrefix(url, "file://") {
Expand All @@ -676,7 +676,7 @@ func (u *UpdateHandler) downloadUpdate(update *updatesAvailable) (binPath string
src = file
} else {
log.Tracef("[update] downloading update from %s", url)
resp, err2 := u.snc.httpDo(*u.ctx, u.httpOptions, "GET", url, update.header)
resp, err2 := u.snc.httpDo(ctx, u.httpOptions, "GET", url, update.header)
if err2 != nil {
return "", fmt.Errorf("fetching update failed %s: %s", url, err2.Error())
}
Expand Down Expand Up @@ -754,11 +754,11 @@ func (u *UpdateHandler) extractUpdate(updateFile string) (err error) {
return nil
}

func (u *UpdateHandler) verifyUpdate(newBinPath string) (version string, err error) {
func (u *UpdateHandler) verifyUpdate(ctx context.Context, newBinPath string) (version string, err error) {
log.Tracef("[update] checking update file %s", newBinPath)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
ctx2, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, newBinPath, "-V")
cmd := exec.CommandContext(ctx2, newBinPath, "-V")
output, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("checking new version failed %s: %s", newBinPath, err.Error())
Expand All @@ -773,15 +773,15 @@ func (u *UpdateHandler) verifyUpdate(newBinPath string) (version string, err err
return version, nil
}

func (u *UpdateHandler) getVersionFromURL(url string) (version string, err error) {
func (u *UpdateHandler) getVersionFromURL(ctx context.Context, url string) (version string, err error) {
log.Tracef("[update] trying to determine version for url %s", url)
filePath, err := u.downloadUpdate(&updatesAvailable{url: url})
filePath, err := u.downloadUpdate(ctx, &updatesAvailable{url: url})
if err != nil {
return "", err
}
defer os.Remove(filePath)

version, err = u.verifyUpdate(filePath)
version, err = u.verifyUpdate(ctx, filePath)
if err != nil {
return "", err
}
Expand Down

0 comments on commit 498bae2

Please sign in to comment.