ipn/{ipnlocal,ipnstate,localapi}: add localapi endpoints for client self-update (#10188)

* ipn/{ipnlocal,ipnstate,localapi}: add localapi endpoints for client self-update

Updates #10187.

Signed-off-by: Naman Sood <mail@nsood.in>

* depaware

Updates #10187.

Signed-off-by: Naman Sood <mail@nsood.in>

* address review feedback

Signed-off-by: Naman Sood <mail@nsood.in>

---------

Signed-off-by: Naman Sood <mail@nsood.in>
This commit is contained in:
Naman Sood
2023-11-09 13:00:47 -08:00
committed by GitHub
parent 55cd5c575b
commit e57fd9cda4
4 changed files with 152 additions and 3 deletions

View File

@@ -27,6 +27,7 @@ import (
"time"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/clientupdate"
"tailscale.com/envknob"
"tailscale.com/health"
"tailscale.com/hostinfo"
@@ -125,6 +126,9 @@ var handler = map[string]localAPIHandler{
"watch-ipn-bus": (*Handler).serveWatchIPNBus,
"whois": (*Handler).serveWhoIs,
"query-feature": (*Handler).serveQueryFeature,
"update/check": (*Handler).serveUpdateCheck,
"update/install": (*Handler).serveUpdateInstall,
"update/progress": (*Handler).serveUpdateProgress,
}
var (
@@ -2418,6 +2422,75 @@ func (h *Handler) serveDebugLog(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}
// serveUpdateCheck returns the ClientVersion from Status, which contains
// information on whether an update is available, and if so, what version,
// *if* we support auto-updates on this platform. If we don't, this endpoint
// always returns a ClientVersion saying we're running the newest version.
// Effectively, it tells us whether serveUpdateInstall will be able to install
// an update for us.
func (h *Handler) serveUpdateCheck(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, "only GET allowed", http.StatusMethodNotAllowed)
return
}
_, err := clientupdate.NewUpdater(clientupdate.Arguments{
ForAutoUpdate: true,
})
if err != nil {
// if we don't support auto-update, just say that we're up to date
if errors.Is(err, errors.ErrUnsupported) {
json.NewEncoder(w).Encode(tailcfg.ClientVersion{RunningLatest: true})
} else {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
cv := h.b.StatusWithoutPeers().ClientVersion
// ipnstate.Status documentation notes that ClientVersion may be nil on some
// platforms where this information is unavailable. In that case, return a
// ClientVersion that says we're up to date, since we have no information on
// whether an update is possible.
if cv == nil {
cv = &tailcfg.ClientVersion{RunningLatest: true}
}
json.NewEncoder(w).Encode(cv)
}
// serveUpdateInstall sends a request to the LocalBackend to start a Tailscale
// self-update. A successful response does not indicate whether the update
// succeeded, only that the request was accepted. Clients should use
// serveUpdateProgress after pinging this endpoint to check how the update is
// going.
func (h *Handler) serveUpdateInstall(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "only POST allowed", http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusAccepted)
go h.b.DoSelfUpdate()
}
// serveUpdateProgress returns the status of an in-progress Tailscale self-update.
// This is provided as a slice of ipnstate.UpdateProgress structs with various
// log messages in order from oldest to newest. If an update is not in progress,
// the returned slice will be empty.
func (h *Handler) serveUpdateProgress(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, "only GET allowed", http.StatusMethodNotAllowed)
return
}
ups := h.b.GetSelfUpdateProgress()
json.NewEncoder(w).Encode(ups)
}
var (
metricInvalidRequests = clientmetric.NewCounter("localapi_invalid_requests")