|
|
@@ -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")
|
|
|
|