No longer enforce version matching between daemon and cli (#4271)
* No longer check version matching between daemon and cli * Add Log Service Handler and report if the daemon is running in background mode
This commit is contained in:
8 files changed
+198
-66
No files matched your search
@@ -201,6 +201,7 @@ func (c *StartCommand) Run(args []string) int {
|
||||
LogWriter: io.MultiWriter(writers...),
|
||||
LogFileName: logFileName,
|
||||
DotDirectory: dotDir,
|
||||
RunningInBackground: os.Getenv(backgroundEnvName) == backgroundEnvVal,
|
||||
}
|
||||
|
||||
srv, err := daemon.New(ctx, cfg)
|
||||
|
||||
@@ -144,6 +144,9 @@ func printStatusTable(status *daemon.StatusResult) string {
|
||||
if len(status.LogLocation) > 0 {
|
||||
nonAttributeMap["Log Location"] = status.LogLocation
|
||||
}
|
||||
if len(status.Version) > 0 {
|
||||
nonAttributeMap["Version"] = status.Version
|
||||
}
|
||||
if status.Uptime > 0 {
|
||||
nonAttributeMap["Uptime"] = status.Uptime.Round(time.Second)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/event"
|
||||
)
|
||||
|
||||
// LogRequest is the request body to this handler.
|
||||
type LogRequest struct {
|
||||
// Message is a required field for all requests
|
||||
Message string `json:"message,omitempty"`
|
||||
Op string `json:"op,omitempty"`
|
||||
}
|
||||
|
||||
// newLogHandlerFunc creates a handler that logs a system event using the
|
||||
// daemon's eventer.
|
||||
func newLogHandlerFunc(ctx context.Context) (http.HandlerFunc, error) {
|
||||
const op = "daemon.newLogHandlerFunc"
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeError(w, "only method POST allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
||||
var perReq LogRequest
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
writeError(w, "unable to read request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := json.Unmarshal(data, &perReq); err != nil {
|
||||
// If, for whatever reason, we can't parse the request body as json
|
||||
// can still log that the request to log was received and print out
|
||||
// the body of the request.
|
||||
event.WriteError(ctx, op, err, event.WithInfo("body", string(data)))
|
||||
writeError(w, "unable to parse request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
event.WriteSysEvent(ctx, op, perReq.Message, "requester_op", perReq.Op)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/event"
|
||||
"github.com/hashicorp/eventlogger/formatter_filters/cloudevents"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLogHandler(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
lh, err := newLogHandlerFunc(ctx)
|
||||
require.NoError(t, err)
|
||||
expectedErroringMux := http.NewServeMux()
|
||||
expectedErroringMux.HandleFunc("/v1/log", lh)
|
||||
|
||||
t.Run("get", func(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/log", nil)
|
||||
expectedErroringMux.ServeHTTP(rec, req)
|
||||
assert.Equal(t, http.StatusMethodNotAllowed, rec.Result().StatusCode)
|
||||
})
|
||||
|
||||
t.Run("delete", func(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodDelete, "/v1/log", nil)
|
||||
expectedErroringMux.ServeHTTP(rec, req)
|
||||
assert.Equal(t, http.StatusMethodNotAllowed, rec.Result().StatusCode)
|
||||
})
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
c := event.TestEventerConfig(t, "TestLogHandler_success")
|
||||
testLock := &sync.Mutex{}
|
||||
testLogger := hclog.New(&hclog.LoggerOptions{
|
||||
Mutex: testLock,
|
||||
Name: "test",
|
||||
JSONFormat: true,
|
||||
})
|
||||
require.NoError(t, event.InitSysEventer(testLogger, testLock, "TestLogHandler_success", event.WithEventerConfig(&c.EventerConfig)))
|
||||
ctx, err := event.NewEventerContext(context.Background(), event.SysEventer())
|
||||
require.NoError(t, err)
|
||||
|
||||
lh, err := newLogHandlerFunc(ctx)
|
||||
require.NoError(t, err)
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/v1/log", lh)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
b, err := json.Marshal(&LogRequest{
|
||||
Message: "test message",
|
||||
Op: "test op",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/log", bytes.NewReader(b))
|
||||
mux.ServeHTTP(rec, req)
|
||||
assert.Equal(t, http.StatusNoContent, rec.Result().StatusCode)
|
||||
|
||||
sinkFileName := c.AllEvents.Name()
|
||||
defer func() { _ = os.WriteFile(sinkFileName, nil, 0o666) }()
|
||||
b, err = os.ReadFile(sinkFileName)
|
||||
require.NoError(t, err)
|
||||
gotEvent := &cloudevents.Event{}
|
||||
err = json.Unmarshal(b, gotEvent)
|
||||
require.NoError(t, err)
|
||||
|
||||
gotData := gotEvent.Data.(map[string]any)["data"].(map[string]any)
|
||||
assert.Equal(t, "test message", gotData["msg"])
|
||||
assert.Equal(t, "test op", gotData["requester_op"])
|
||||
})
|
||||
|
||||
t.Run("success failed unmarshaling", func(t *testing.T) {
|
||||
c := event.TestEventerConfig(t, "TestLogHandler_success")
|
||||
testLock := &sync.Mutex{}
|
||||
testLogger := hclog.New(&hclog.LoggerOptions{
|
||||
Mutex: testLock,
|
||||
Name: "test",
|
||||
JSONFormat: true,
|
||||
})
|
||||
require.NoError(t, event.InitSysEventer(testLogger, testLock, "TestLogHandler_success", event.WithEventerConfig(&c.EventerConfig)))
|
||||
ctx, err := event.NewEventerContext(context.Background(), event.SysEventer())
|
||||
require.NoError(t, err)
|
||||
|
||||
lh, err := newLogHandlerFunc(ctx)
|
||||
require.NoError(t, err)
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/v1/log", lh)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
b := []byte("not json")
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/log", bytes.NewReader(b))
|
||||
mux.ServeHTTP(rec, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rec.Result().StatusCode)
|
||||
|
||||
sinkFileName := c.AllEvents.Name()
|
||||
defer func() { _ = os.WriteFile(sinkFileName, nil, 0o666) }()
|
||||
b, err = os.ReadFile(sinkFileName)
|
||||
require.NoError(t, err)
|
||||
gotEvent := &cloudevents.Event{}
|
||||
err = json.Unmarshal(b, gotEvent)
|
||||
require.NoError(t, err)
|
||||
|
||||
gotData := gotEvent.Data.(map[string]any)
|
||||
assert.NotEmpty(t, gotData["error"])
|
||||
assert.Equal(t, "not json", gotData["info"].(map[string]any)["body"])
|
||||
})
|
||||
}
|
||||
@@ -81,6 +81,7 @@ type Config struct {
|
||||
LogWriter io.Writer
|
||||
LogFileName string
|
||||
DotDirectory string
|
||||
RunningInBackground bool
|
||||
// The amount of time since the last refresh that must have passed for a
|
||||
// search query to trigger an inline refresh.
|
||||
MaxSearchStaleness time.Duration
|
||||
@@ -286,28 +287,34 @@ func (s *CacheServer) Serve(ctx context.Context, cmd Commander, opt ...Option) e
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
mux.Handle("/v1/search", versionEnforcement(searchFn))
|
||||
mux.Handle("/v1/search", serverMetadataInterceptor(searchFn, s.conf.RunningInBackground))
|
||||
|
||||
statusFn, err := newStatusHandlerFunc(ctx, repo, l.Addr().String(), s.conf.LogFileName)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
mux.Handle("/v1/status", versionEnforcement(statusFn))
|
||||
mux.Handle("/v1/status", serverMetadataInterceptor(statusFn, s.conf.RunningInBackground))
|
||||
|
||||
logHandlerFn, err := newLogHandlerFunc(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
mux.Handle("/v1/log", serverMetadataInterceptor(logHandlerFn, s.conf.RunningInBackground))
|
||||
|
||||
tokenFn, err := newTokenHandlerFunc(ctx, repo, tic)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
mux.Handle("/v1/tokens", versionEnforcement(tokenFn))
|
||||
mux.Handle("/v1/tokens", serverMetadataInterceptor(tokenFn, s.conf.RunningInBackground))
|
||||
|
||||
stopFn, err := newStopHandlerFunc(ctx, s.conf.ContextCancel)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
mux.Handle("/v1/stop", versionEnforcement(stopFn))
|
||||
mux.Handle("/v1/stop", serverMetadataInterceptor(stopFn, s.conf.RunningInBackground))
|
||||
|
||||
// Return custom 404 message when requests don't map to any known path.
|
||||
mux.Handle("/", new404Func(ctx))
|
||||
mux.Handle("/", serverMetadataInterceptor(new404Func(ctx), s.conf.RunningInBackground))
|
||||
|
||||
logger, err := event.SysEventer().StandardLogger(ctx, "daemon.serve: ", event.ErrorType)
|
||||
if err != nil {
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/hashicorp/boundary/internal/clientcache/internal/cache"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/util"
|
||||
"github.com/hashicorp/boundary/version"
|
||||
)
|
||||
|
||||
// RefreshTokenStatus is the status of a resource token
|
||||
@@ -69,6 +70,7 @@ type StatusResult struct {
|
||||
Uptime time.Duration `json:"uptime,omitempty"`
|
||||
SocketAddress string `json:"socket_address,omitempty"`
|
||||
LogLocation string `json:"log_location,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Users []UserStatus `json:"users,omitempty"`
|
||||
}
|
||||
|
||||
@@ -123,6 +125,7 @@ func toApiStatus(in *cache.Status, started time.Time, socketAddr, logLocation st
|
||||
out := &StatusResult{
|
||||
Uptime: time.Since(started),
|
||||
SocketAddress: socketAddr,
|
||||
Version: version.Get().FullVersionNumber(true),
|
||||
LogLocation: logLocation,
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/util"
|
||||
@@ -13,23 +12,24 @@ import (
|
||||
|
||||
const (
|
||||
VersionHeaderKey = "boundary_version"
|
||||
BackgroundKey = "background"
|
||||
)
|
||||
|
||||
// versionEnforcement is an interceptor which, if the boundary version is included
|
||||
// in a request, enforces that it matches the version of the daemon currently
|
||||
// running. If no version is provided, the inteceptor passes the request through.
|
||||
func versionEnforcement(h http.Handler) http.Handler {
|
||||
// serverMetadataInterceptor is an interceptor which attaches the daemon's version
|
||||
// number to all responses that it intercepts
|
||||
func serverMetadataInterceptor(h http.Handler, inBackground bool) http.Handler {
|
||||
if util.IsNil(h) {
|
||||
return nil
|
||||
}
|
||||
|
||||
background := "false"
|
||||
if inBackground {
|
||||
background = "true"
|
||||
}
|
||||
needVer := version.Get().VersionNumber()
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotVer := r.Header.Get(VersionHeaderKey)
|
||||
if gotVer != "" && needVer != gotVer {
|
||||
writeError(w, fmt.Sprintf("Version mismatch between requester (%q) and daemon (%q). You may need to restart your daemon.", gotVer, needVer), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.Header().Add(BackgroundKey, background)
|
||||
w.Header().Add(VersionHeaderKey, needVer)
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/version"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestVersionEnforcement(t *testing.T) {
|
||||
var called bool
|
||||
calledHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
called = true
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
h := versionEnforcement(calledHandler)
|
||||
|
||||
t.Run("no version provided", func(t *testing.T) {
|
||||
called = false
|
||||
req := httptest.NewRequest("", "/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, req)
|
||||
assert.True(t, called)
|
||||
assert.Equal(t, http.StatusNoContent, w.Result().StatusCode)
|
||||
})
|
||||
|
||||
t.Run("bad version provided", func(t *testing.T) {
|
||||
called = false
|
||||
req := httptest.NewRequest("", "/test", nil)
|
||||
req.Header.Set(VersionHeaderKey, "badversion")
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, req)
|
||||
assert.False(t, called)
|
||||
assert.Equal(t, http.StatusBadRequest, w.Result().StatusCode)
|
||||
})
|
||||
|
||||
t.Run("correct version provided", func(t *testing.T) {
|
||||
called = false
|
||||
req := httptest.NewRequest("", "/test", nil)
|
||||
req.Header.Set(VersionHeaderKey, version.Get().VersionNumber())
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, req)
|
||||
assert.True(t, called)
|
||||
assert.Equal(t, http.StatusNoContent, w.Result().StatusCode)
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user