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
pull/4286/head
Todd 2 years ago committed by GitHub
parent 27bf15f3f3
commit e32a25f2a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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)
})
}
Loading…
Cancel
Save