Pull request: websvc: add system info
Merge in DNS/adguard-home from websvc-system-info to master Squashed commit of the following: commit 333aaa0602da254e25e0262a10080bf44a3718a7 Author: Ainar Garipov <A.Garipov@AdGuard.COM> Date: Thu May 12 16:32:32 2022 +0300 websvc: fmt commit d8a35bf71dcc59fdd595494e5b220e3d24516728 Author: Ainar Garipov <A.Garipov@AdGuard.COM> Date: Thu May 12 16:10:11 2022 +0300 websvc: refactor, imp tests commit dfeb24f3f35513bf51323d3ab6f717f582a1defc Author: Ainar Garipov <A.Garipov@AdGuard.COM> Date: Wed May 11 20:52:02 2022 +0300 websvc: add system info
This commit is contained in:
parent
58515fce43
commit
f289f4b1b6
1
go.mod
1
go.mod
|
@ -9,6 +9,7 @@ require (
|
||||||
github.com/NYTimes/gziphandler v1.1.1
|
github.com/NYTimes/gziphandler v1.1.1
|
||||||
github.com/ameshkov/dnscrypt/v2 v2.2.3
|
github.com/ameshkov/dnscrypt/v2 v2.2.3
|
||||||
github.com/digineo/go-ipset/v2 v2.2.1
|
github.com/digineo/go-ipset/v2 v2.2.1
|
||||||
|
github.com/dimfeld/httptreemux/v5 v5.4.0
|
||||||
github.com/fsnotify/fsnotify v1.5.4
|
github.com/fsnotify/fsnotify v1.5.4
|
||||||
github.com/go-ping/ping v0.0.0-20211130115550-779d1e919534
|
github.com/go-ping/ping v0.0.0-20211130115550-779d1e919534
|
||||||
github.com/google/go-cmp v0.5.7
|
github.com/google/go-cmp v0.5.7
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -52,6 +52,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/digineo/go-ipset/v2 v2.2.1 h1:k6skY+0fMqeUjjeWO/m5OuWPSZUAn7AucHMnQ1MX77g=
|
github.com/digineo/go-ipset/v2 v2.2.1 h1:k6skY+0fMqeUjjeWO/m5OuWPSZUAn7AucHMnQ1MX77g=
|
||||||
github.com/digineo/go-ipset/v2 v2.2.1/go.mod h1:wBsNzJlZlABHUITkesrggFnZQtgW5wkqw1uo8Qxe0VU=
|
github.com/digineo/go-ipset/v2 v2.2.1/go.mod h1:wBsNzJlZlABHUITkesrggFnZQtgW5wkqw1uo8Qxe0VU=
|
||||||
|
github.com/dimfeld/httptreemux/v5 v5.4.0 h1:IiHYEjh+A7pYbhWyjmGnj5HZK6gpOOvyBXCJ+BE8/Gs=
|
||||||
|
github.com/dimfeld/httptreemux/v5 v5.4.0/go.mod h1:QeEylH57C0v3VO0tkKraVz9oD3Uu93CKPnTLbsidvSw=
|
||||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc=
|
github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc=
|
||||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||||
|
|
|
@ -20,7 +20,8 @@ import (
|
||||||
func Main(clientBuildFS fs.FS) {
|
func Main(clientBuildFS fs.FS) {
|
||||||
// # Initial Configuration
|
// # Initial Configuration
|
||||||
|
|
||||||
rand.Seed(time.Now().UnixNano())
|
start := time.Now()
|
||||||
|
rand.Seed(start.UnixNano())
|
||||||
|
|
||||||
// TODO(a.garipov): Set up logging.
|
// TODO(a.garipov): Set up logging.
|
||||||
|
|
||||||
|
@ -35,6 +36,7 @@ func Main(clientBuildFS fs.FS) {
|
||||||
IP: net.IP{127, 0, 0, 1},
|
IP: net.IP{127, 0, 0, 1},
|
||||||
Port: 3001,
|
Port: 3001,
|
||||||
}},
|
}},
|
||||||
|
Start: start,
|
||||||
Timeout: 60 * time.Second,
|
Timeout: 60 * time.Second,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JSON Utilities
|
||||||
|
|
||||||
|
// jsonTime is a time.Time that can be decoded from JSON and encoded into JSON
|
||||||
|
// according to our API conventions.
|
||||||
|
type jsonTime time.Time
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ json.Marshaler = jsonTime{}
|
||||||
|
|
||||||
|
// nsecPerMsec is the number of nanoseconds in a millisecond.
|
||||||
|
const nsecPerMsec = float64(time.Millisecond / time.Nanosecond)
|
||||||
|
|
||||||
|
// MarshalJSON implements the json.Marshaler interface for jsonTime. err is
|
||||||
|
// always nil.
|
||||||
|
func (t jsonTime) MarshalJSON() (b []byte, err error) {
|
||||||
|
msec := float64(time.Time(t).UnixNano()) / nsecPerMsec
|
||||||
|
b = strconv.AppendFloat(nil, msec, 'f', 3, 64)
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ json.Unmarshaler = (*jsonTime)(nil)
|
||||||
|
|
||||||
|
// UnmarshalJSON implements the json.Marshaler interface for *jsonTime.
|
||||||
|
func (t *jsonTime) UnmarshalJSON(b []byte) (err error) {
|
||||||
|
if t == nil {
|
||||||
|
return fmt.Errorf("json time is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
msec, err := strconv.ParseFloat(string(b), 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parsing json time: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*t = jsonTime(time.Unix(0, int64(msec*nsecPerMsec)).UTC())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeJSONResponse encodes v into w and logs any errors it encounters. r is
|
||||||
|
// used to get additional information from the request.
|
||||||
|
func writeJSONResponse(w io.Writer, r *http.Request, v interface{}) {
|
||||||
|
err := json.NewEncoder(w).Encode(v)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("websvc: writing resp to %s %s: %s", r.Method, r.URL.Path, err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package websvc
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
// Middlewares
|
||||||
|
|
||||||
|
// jsonMw sets the content type of the response to application/json.
|
||||||
|
func jsonMw(h http.Handler) (wrapped http.HandlerFunc) {
|
||||||
|
f := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(f)
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package websvc
|
||||||
|
|
||||||
|
// Path constants
|
||||||
|
const (
|
||||||
|
PathHealthCheck = "/health-check"
|
||||||
|
|
||||||
|
PathV1SystemInfo = "/api/v1/system/info"
|
||||||
|
)
|
|
@ -0,0 +1,35 @@
|
||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
// System Handlers
|
||||||
|
|
||||||
|
// RespGetV1SystemInfo describes the response of the GET /api/v1/system/info
|
||||||
|
// HTTP API.
|
||||||
|
type RespGetV1SystemInfo struct {
|
||||||
|
Arch string `json:"arch"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
OS string `json:"os"`
|
||||||
|
NewVersion string `json:"new_version,omitempty"`
|
||||||
|
Start jsonTime `json:"start"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleGetV1SystemInfo is the handler for the GET /api/v1/system/info HTTP
|
||||||
|
// API.
|
||||||
|
func (svc *Service) handleGetV1SystemInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
writeJSONResponse(w, r, &RespGetV1SystemInfo{
|
||||||
|
Arch: runtime.GOARCH,
|
||||||
|
Channel: version.Channel(),
|
||||||
|
OS: runtime.GOOS,
|
||||||
|
// TODO(a.garipov): Fill this when we have an updater.
|
||||||
|
NewVersion: "",
|
||||||
|
Start: jsonTime(svc.start),
|
||||||
|
Version: version.Version(),
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package websvc_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/v1/websvc"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestService_handleGetV1SystemInfo(t *testing.T) {
|
||||||
|
_, addr := newTestServer(t)
|
||||||
|
u := &url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: addr,
|
||||||
|
Path: websvc.PathV1SystemInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
body := httpGet(t, u, http.StatusOK)
|
||||||
|
resp := &websvc.RespGetV1SystemInfo{}
|
||||||
|
err := json.Unmarshal(body, resp)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// TODO(a.garipov): Consider making version.Channel and version.Version
|
||||||
|
// testable and test these better.
|
||||||
|
assert.NotEmpty(t, resp.Channel)
|
||||||
|
|
||||||
|
assert.Equal(t, resp.Arch, runtime.GOARCH)
|
||||||
|
assert.Equal(t, resp.OS, runtime.GOOS)
|
||||||
|
assert.Equal(t, testStart, time.Time(resp.Start))
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
|
httptreemux "github.com/dimfeld/httptreemux/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config is the AdGuard Home web service configuration structure.
|
// Config is the AdGuard Home web service configuration structure.
|
||||||
|
@ -32,6 +33,9 @@ type Config struct {
|
||||||
// SecureAddresses is not empty, TLS must not be nil.
|
// SecureAddresses is not empty, TLS must not be nil.
|
||||||
SecureAddresses []*netutil.IPPort
|
SecureAddresses []*netutil.IPPort
|
||||||
|
|
||||||
|
// Start is the time of start of AdGuard Home.
|
||||||
|
Start time.Time
|
||||||
|
|
||||||
// Timeout is the timeout for all server operations.
|
// Timeout is the timeout for all server operations.
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
}
|
}
|
||||||
|
@ -41,6 +45,7 @@ type Config struct {
|
||||||
type Service struct {
|
type Service struct {
|
||||||
tls *tls.Config
|
tls *tls.Config
|
||||||
servers []*http.Server
|
servers []*http.Server
|
||||||
|
start time.Time
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,11 +58,11 @@ func New(c *Config) (svc *Service) {
|
||||||
|
|
||||||
svc = &Service{
|
svc = &Service{
|
||||||
tls: c.TLS,
|
tls: c.TLS,
|
||||||
|
start: c.Start,
|
||||||
timeout: c.Timeout,
|
timeout: c.Timeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := newMux(svc)
|
||||||
mux.HandleFunc("/health-check", svc.handleGetHealthCheck)
|
|
||||||
|
|
||||||
for _, a := range c.Addresses {
|
for _, a := range c.Addresses {
|
||||||
addr := a.String()
|
addr := a.String()
|
||||||
|
@ -91,6 +96,43 @@ func New(c *Config) (svc *Service) {
|
||||||
return svc
|
return svc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newMux returns a new HTTP request multiplexor for the AdGuard Home web
|
||||||
|
// service.
|
||||||
|
func newMux(svc *Service) (mux *httptreemux.ContextMux) {
|
||||||
|
mux = httptreemux.NewContextMux()
|
||||||
|
|
||||||
|
routes := []struct {
|
||||||
|
handler http.HandlerFunc
|
||||||
|
method string
|
||||||
|
path string
|
||||||
|
isJSON bool
|
||||||
|
}{{
|
||||||
|
handler: svc.handleGetHealthCheck,
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: PathHealthCheck,
|
||||||
|
isJSON: false,
|
||||||
|
}, {
|
||||||
|
handler: svc.handleGetV1SystemInfo,
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: PathV1SystemInfo,
|
||||||
|
isJSON: true,
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, r := range routes {
|
||||||
|
var h http.HandlerFunc
|
||||||
|
if r.isJSON {
|
||||||
|
// TODO(a.garipov): Consider using httptreemux's MiddlewareFunc.
|
||||||
|
h = jsonMw(r.handler)
|
||||||
|
} else {
|
||||||
|
h = r.handler
|
||||||
|
}
|
||||||
|
|
||||||
|
mux.Handle(r.method, r.path, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mux
|
||||||
|
}
|
||||||
|
|
||||||
// Addrs returns all addresses on which this server serves the HTTP API. Addrs
|
// Addrs returns all addresses on which this server serves the HTTP API. Addrs
|
||||||
// must not be called until Start returns.
|
// must not be called until Start returns.
|
||||||
func (svc *Service) Addrs() (addrs []string) {
|
func (svc *Service) Addrs() (addrs []string) {
|
||||||
|
|
|
@ -18,7 +18,17 @@ import (
|
||||||
|
|
||||||
const testTimeout = 1 * time.Second
|
const testTimeout = 1 * time.Second
|
||||||
|
|
||||||
func TestService_Start_getHealthCheck(t *testing.T) {
|
// testStart is the server start value for tests.
|
||||||
|
var testStart = time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
// newTestServer creates and starts a new web service instance as well as its
|
||||||
|
// sole address. It also registers a cleanup procedure, which shuts the
|
||||||
|
// instance down.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Use svc or remove it.
|
||||||
|
func newTestServer(t testing.TB) (svc *websvc.Service, addr string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
c := &websvc.Config{
|
c := &websvc.Config{
|
||||||
TLS: nil,
|
TLS: nil,
|
||||||
Addresses: []*netutil.IPPort{{
|
Addresses: []*netutil.IPPort{{
|
||||||
|
@ -27,9 +37,10 @@ func TestService_Start_getHealthCheck(t *testing.T) {
|
||||||
}},
|
}},
|
||||||
SecureAddresses: nil,
|
SecureAddresses: nil,
|
||||||
Timeout: testTimeout,
|
Timeout: testTimeout,
|
||||||
|
Start: testStart,
|
||||||
}
|
}
|
||||||
|
|
||||||
svc := websvc.New(c)
|
svc = websvc.New(c)
|
||||||
|
|
||||||
err := svc.Start()
|
err := svc.Start()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -44,26 +55,43 @@ func TestService_Start_getHealthCheck(t *testing.T) {
|
||||||
addrs := svc.Addrs()
|
addrs := svc.Addrs()
|
||||||
require.Len(t, addrs, 1)
|
require.Len(t, addrs, 1)
|
||||||
|
|
||||||
u := &url.URL{
|
return svc, addrs[0]
|
||||||
Scheme: "http",
|
}
|
||||||
Host: addrs[0],
|
|
||||||
Path: "/health-check",
|
// httpGet is a helper that performs an HTTP GET request and returns the body of
|
||||||
}
|
// the response as well as checks that the status code is correct.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Add helpers for other methods.
|
||||||
|
func httpGet(t testing.TB, u *url.URL, wantCode int) (body []byte) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||||
require.NoError(t, err)
|
require.NoErrorf(t, err, "creating req")
|
||||||
|
|
||||||
httpCli := &http.Client{
|
httpCli := &http.Client{
|
||||||
Timeout: testTimeout,
|
Timeout: testTimeout,
|
||||||
}
|
}
|
||||||
resp, err := httpCli.Do(req)
|
resp, err := httpCli.Do(req)
|
||||||
require.NoError(t, err)
|
require.NoErrorf(t, err, "performing req")
|
||||||
|
require.Equal(t, wantCode, resp.StatusCode)
|
||||||
|
|
||||||
testutil.CleanupAndRequireSuccess(t, resp.Body.Close)
|
testutil.CleanupAndRequireSuccess(t, resp.Body.Close)
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
body, err = io.ReadAll(resp.Body)
|
||||||
|
require.NoErrorf(t, err, "reading body")
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
return body
|
||||||
require.NoError(t, err)
|
}
|
||||||
|
|
||||||
|
func TestService_Start_getHealthCheck(t *testing.T) {
|
||||||
|
_, addr := newTestServer(t)
|
||||||
|
u := &url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: addr,
|
||||||
|
Path: websvc.PathHealthCheck,
|
||||||
|
}
|
||||||
|
|
||||||
|
body := httpGet(t, u, http.StatusOK)
|
||||||
|
|
||||||
assert.Equal(t, []byte("OK"), body)
|
assert.Equal(t, []byte("OK"), body)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3393,11 +3393,17 @@
|
||||||
'description': >
|
'description': >
|
||||||
Information about the AdGuard Home server.
|
Information about the AdGuard Home server.
|
||||||
'example':
|
'example':
|
||||||
|
'arch': 'amd64'
|
||||||
'channel': 'release'
|
'channel': 'release'
|
||||||
'new_version': 'v0.106.1'
|
'new_version': 'v0.108.1'
|
||||||
|
'os': 'linux'
|
||||||
'start': 1614345496000
|
'start': 1614345496000
|
||||||
'version': 'v0.106.0'
|
'version': 'v0.108.0'
|
||||||
'properties':
|
'properties':
|
||||||
|
'arch':
|
||||||
|
'description': >
|
||||||
|
CPU architecture.
|
||||||
|
'type': 'string'
|
||||||
'channel':
|
'channel':
|
||||||
'$ref': '#/components/schemas/Channel'
|
'$ref': '#/components/schemas/Channel'
|
||||||
'new_version':
|
'new_version':
|
||||||
|
@ -3405,6 +3411,10 @@
|
||||||
New available version of AdGuard Home to which the server can be
|
New available version of AdGuard Home to which the server can be
|
||||||
updated, if any. If there are none, this field is absent.
|
updated, if any. If there are none, this field is absent.
|
||||||
'type': 'string'
|
'type': 'string'
|
||||||
|
'os':
|
||||||
|
'description': >
|
||||||
|
Operating system type.
|
||||||
|
'type': 'string'
|
||||||
'start':
|
'start':
|
||||||
'description': >
|
'description': >
|
||||||
Unix time at which AdGuard Home started working, in milliseconds.
|
Unix time at which AdGuard Home started working, in milliseconds.
|
||||||
|
@ -3415,7 +3425,9 @@
|
||||||
Current AdGuard Home version.
|
Current AdGuard Home version.
|
||||||
'type': 'string'
|
'type': 'string'
|
||||||
'required':
|
'required':
|
||||||
|
- 'arch'
|
||||||
- 'channel'
|
- 'channel'
|
||||||
|
- 'os'
|
||||||
- 'start'
|
- 'start'
|
||||||
- 'version'
|
- 'version'
|
||||||
'type': 'object'
|
'type': 'object'
|
||||||
|
|
Loading…
Reference in New Issue