Auto updater APIs (#1523)
* APIs for querying and executing an update in place. For #924 * Use the process pid to query systemd for status * Use parent pid and invocation ID to guess if running from systemd * Stream cmd output to client + report errors * Update comment to refer to INVOCATION_ID
This commit is contained in:
parent
71abb3cfb5
commit
83eb9229ad
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -10,6 +10,7 @@
|
|||||||
"Mbps",
|
"Mbps",
|
||||||
"nolint",
|
"nolint",
|
||||||
"Owncast",
|
"Owncast",
|
||||||
|
"ppid",
|
||||||
"preact",
|
"preact",
|
||||||
"RTMP",
|
"RTMP",
|
||||||
"rtmpserverport",
|
"rtmpserverport",
|
||||||
@ -20,4 +21,4 @@
|
|||||||
"Warnf",
|
"Warnf",
|
||||||
"Warnln"
|
"Warnln"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -71,7 +71,7 @@ build() {
|
|||||||
|
|
||||||
pushd dist/${NAME} >> /dev/null
|
pushd dist/${NAME} >> /dev/null
|
||||||
|
|
||||||
CGO_ENABLED=1 ~/go/bin/xgo --branch ${GIT_BRANCH} -ldflags "-s -w -X github.com/owncast/owncast/config.GitCommit=${GIT_COMMIT} -X github.com/owncast/owncast/config.BuildVersion=${VERSION} -X github.com/owncast/owncast/config.BuildPlatform=${NAME}" -targets "${OS}/${ARCH}" github.com/owncast/owncast
|
CGO_ENABLED=1 ~/go/bin/xgo --branch ${GIT_BRANCH} -ldflags "-s -w -X github.com/owncast/owncast/config.GitCommit=${GIT_COMMIT} -X github.com/owncast/owncast/config.BuildVersion=${VERSION} -X github.com/owncast/owncast/config.BuildPlatform=${NAME}" -tags enable_updates -targets "${OS}/${ARCH}" github.com/owncast/owncast
|
||||||
mv owncast-*-${ARCH} owncast
|
mv owncast-*-${ARCH} owncast
|
||||||
|
|
||||||
zip -r -q -8 ../owncast-$VERSION-$NAME.zip .
|
zip -r -q -8 ../owncast-$VERSION-$NAME.zip .
|
||||||
|
@ -34,6 +34,9 @@ var GitCommit = ""
|
|||||||
// BuildPlatform is the optional platform this release was built for.
|
// BuildPlatform is the optional platform this release was built for.
|
||||||
var BuildPlatform = "dev"
|
var BuildPlatform = "dev"
|
||||||
|
|
||||||
|
// EnableAutoUpdate will explicitly enable in-place auto-updates via the admin.
|
||||||
|
var EnableAutoUpdate = false
|
||||||
|
|
||||||
// GetCommit will return an identifier used for identifying the point in time this build took place.
|
// GetCommit will return an identifier used for identifying the point in time this build took place.
|
||||||
func GetCommit() string {
|
func GetCommit() string {
|
||||||
if GitCommit == "" {
|
if GitCommit == "" {
|
||||||
@ -53,9 +56,9 @@ const MaxSocketPayloadSize = 2048
|
|||||||
|
|
||||||
// GetReleaseString gets the version string.
|
// GetReleaseString gets the version string.
|
||||||
func GetReleaseString() string {
|
func GetReleaseString() string {
|
||||||
var versionNumber = VersionNumber
|
versionNumber := VersionNumber
|
||||||
var buildPlatform = BuildPlatform
|
buildPlatform := BuildPlatform
|
||||||
var gitCommit = GetCommit()
|
gitCommit := GetCommit()
|
||||||
|
|
||||||
return fmt.Sprintf("Owncast v%s-%s (%s)", versionNumber, buildPlatform, gitCommit)
|
return fmt.Sprintf("Owncast v%s-%s (%s)", versionNumber, buildPlatform, gitCommit)
|
||||||
}
|
}
|
||||||
|
8
config/updaterConfig_enabled.go
Normal file
8
config/updaterConfig_enabled.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
//go:build enable_updates
|
||||||
|
// +build enable_updates
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
EnableAutoUpdate = true
|
||||||
|
}
|
203
controllers/admin/update.go
Normal file
203
controllers/admin/update.go
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/config"
|
||||||
|
"github.com/owncast/owncast/controllers"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
The auto-update relies on some guesses and hacks to determine how the binary
|
||||||
|
is being run.
|
||||||
|
|
||||||
|
It determines if is running under systemd by asking systemd about the PID
|
||||||
|
and checking the parent pid or INVOCATION_ID property is set for it.
|
||||||
|
|
||||||
|
It also determines if the binary is running under a container by figuring out
|
||||||
|
the container ID as a fallback to refuse an in-place update within a container.
|
||||||
|
|
||||||
|
In general it's disabled for everyone and the features are enabled only if
|
||||||
|
specific conditions are met.
|
||||||
|
|
||||||
|
1. Cannot be run inside a container.
|
||||||
|
2. Cannot be run from source (aka platform is "dev")
|
||||||
|
3. Must be run under systemd to support auto-restart.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// AutoUpdateOptions will return what auto update options are available.
|
||||||
|
func AutoUpdateOptions(w http.ResponseWriter, r *http.Request) {
|
||||||
|
type autoUpdateOptionsResponse struct {
|
||||||
|
SupportsUpdate bool `json:"supportsUpdate"`
|
||||||
|
CanRestart bool `json:"canRestart"`
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOptions := autoUpdateOptionsResponse{
|
||||||
|
SupportsUpdate: false,
|
||||||
|
CanRestart: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing is supported when running under "dev" or the feature is
|
||||||
|
// explicitly disabled.
|
||||||
|
if config.BuildPlatform == "dev" || !config.EnableAutoUpdate {
|
||||||
|
controllers.WriteResponse(w, updateOptions)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we are not in a container then we can update in place.
|
||||||
|
if getContainerID() == "" {
|
||||||
|
updateOptions.SupportsUpdate = true
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOptions.CanRestart = isRunningUnderSystemD()
|
||||||
|
|
||||||
|
controllers.WriteResponse(w, updateOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoUpdateStart will begin the auto update process.
|
||||||
|
func AutoUpdateStart(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// We return the console output directly to the client.
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
|
||||||
|
// Download the installer and save it to a temp file.
|
||||||
|
updater, err := downloadInstaller()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln(err)
|
||||||
|
controllers.WriteSimpleResponse(w, false, "failed to download and run installer")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fw := flushWriter{w: w}
|
||||||
|
if f, ok := w.(http.Flusher); ok {
|
||||||
|
fw.f = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the installer.
|
||||||
|
cmd := exec.Command("bash", updater)
|
||||||
|
cmd.Env = append(os.Environ(), "NO_COLOR=true")
|
||||||
|
cmd.Stdout = &fw
|
||||||
|
cmd.Stderr = &fw
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
log.Debugln(err)
|
||||||
|
if _, err := w.Write([]byte("Unable to complete update: " + err.Error())); err != nil {
|
||||||
|
log.Errorln(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoUpdateForceQuit will force quit the service.
|
||||||
|
func AutoUpdateForceQuit(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Warnln("Owncast is exiting due to request.")
|
||||||
|
go func() {
|
||||||
|
os.Exit(0)
|
||||||
|
}()
|
||||||
|
controllers.WriteSimpleResponse(w, true, "forcing quit")
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadInstaller() (string, error) {
|
||||||
|
installer := "https://owncast.online/install.sh"
|
||||||
|
out, err := os.CreateTemp(os.TempDir(), "updater.sh")
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln(err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
// Get the installer script
|
||||||
|
resp, err := http.Get(installer)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Write the installer to file
|
||||||
|
_, err = io.Copy(out, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.Name(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check to see if owncast is listed as a running service under systemd.
|
||||||
|
func isRunningUnderSystemD() bool {
|
||||||
|
// Our current PID
|
||||||
|
ppid := os.Getppid()
|
||||||
|
|
||||||
|
// A randomized, unique 128-bit ID identifying each runtime cycle of the unit.
|
||||||
|
invocationID, hasInvocationID := os.LookupEnv("INVOCATION_ID")
|
||||||
|
|
||||||
|
// systemd's pid should be 1, so if our process' parent pid is 1
|
||||||
|
// then we are running under systemd.
|
||||||
|
return ppid == 1 || (hasInvocationID && invocationID != "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Taken from https://stackoverflow.com/questions/23513045/how-to-check-if-a-process-is-running-inside-docker-container
|
||||||
|
func getContainerID() string {
|
||||||
|
pid := os.Getppid()
|
||||||
|
cgroupPath := fmt.Sprintf("/proc/%s/cgroup", strconv.Itoa(pid))
|
||||||
|
containerID := ""
|
||||||
|
content, err := ioutil.ReadFile(cgroupPath) //nolint:gosec
|
||||||
|
if err != nil {
|
||||||
|
return containerID
|
||||||
|
}
|
||||||
|
lines := strings.Split(string(content), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
field := strings.Split(line, ":")
|
||||||
|
if len(field) < 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cgroupPath := field[2]
|
||||||
|
if len(cgroupPath) < 64 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Non-systemd Docker
|
||||||
|
// 5:net_prio,net_cls:/docker/de630f22746b9c06c412858f26ca286c6cdfed086d3b302998aa403d9dcedc42
|
||||||
|
// 3:net_cls:/kubepods/burstable/pod5f399c1a-f9fc-11e8-bf65-246e9659ebfc/9170559b8aadd07d99978d9460cf8d1c71552f3c64fefc7e9906ab3fb7e18f69
|
||||||
|
pos := strings.LastIndex(cgroupPath, "/")
|
||||||
|
if pos > 0 {
|
||||||
|
idLen := len(cgroupPath) - pos - 1
|
||||||
|
if idLen == 64 {
|
||||||
|
// docker id
|
||||||
|
containerID = cgroupPath[pos+1 : pos+1+64]
|
||||||
|
return containerID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// systemd Docker
|
||||||
|
// 5:net_cls:/system.slice/docker-afd862d2ed48ef5dc0ce8f1863e4475894e331098c9a512789233ca9ca06fc62.scope
|
||||||
|
dockerStr := "docker-"
|
||||||
|
pos = strings.Index(cgroupPath, dockerStr)
|
||||||
|
if pos > 0 {
|
||||||
|
posScope := strings.Index(cgroupPath, ".scope")
|
||||||
|
idLen := posScope - pos - len(dockerStr)
|
||||||
|
if posScope > 0 && idLen == 64 {
|
||||||
|
containerID = cgroupPath[pos+len(dockerStr) : pos+len(dockerStr)+64]
|
||||||
|
return containerID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return containerID
|
||||||
|
}
|
||||||
|
|
||||||
|
type flushWriter struct {
|
||||||
|
f http.Flusher
|
||||||
|
w io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fw *flushWriter) Write(p []byte) (n int, err error) {
|
||||||
|
n, err = fw.w.Write(p)
|
||||||
|
if fw.f != nil {
|
||||||
|
fw.f.Flush()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
@ -163,6 +163,15 @@ func Start() error {
|
|||||||
// Create a single access token
|
// Create a single access token
|
||||||
http.HandleFunc("/api/admin/accesstokens/create", middleware.RequireAdminAuth(admin.CreateExternalAPIUser))
|
http.HandleFunc("/api/admin/accesstokens/create", middleware.RequireAdminAuth(admin.CreateExternalAPIUser))
|
||||||
|
|
||||||
|
// Return the auto-update features that are supported for this instance.
|
||||||
|
http.HandleFunc("/api/admin/update/options", middleware.RequireAdminAuth(admin.AutoUpdateOptions))
|
||||||
|
|
||||||
|
// Begin the auto update
|
||||||
|
http.HandleFunc("/api/admin/update/start", middleware.RequireAdminAuth(admin.AutoUpdateStart))
|
||||||
|
|
||||||
|
// Force quit the service to restart it
|
||||||
|
http.HandleFunc("/api/admin/update/forcequit", middleware.RequireAdminAuth(admin.AutoUpdateForceQuit))
|
||||||
|
|
||||||
// Send a system message to chat
|
// Send a system message to chat
|
||||||
http.HandleFunc("/api/integrations/chat/system", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendSystemMessages, admin.SendSystemMessage))
|
http.HandleFunc("/api/integrations/chat/system", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendSystemMessages, admin.SendSystemMessage))
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user