feat: api/v2/config endpoint displays runtime configuration (#23003)
* feat: api/v2/config endpoint for runtime config * feat: use a type switch * fix: add tests * chore: add config key to returned json * chore: update swagger refpull/23026/head
parent
afb167a2ca
commit
f78f9eda9c
|
@ -892,6 +892,11 @@ func (m *Launcher) run(ctx context.Context, opts *InfluxdOpts) (err error) {
|
|||
),
|
||||
)
|
||||
|
||||
configHandler, err := http.NewConfigHandler(m.log.With(zap.String("handler", "config")), opts.BindCliOpts())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
platformHandler := http.NewPlatformHandler(
|
||||
m.apibackend,
|
||||
http.WithResourceHandler(stacksHTTPServer),
|
||||
|
@ -911,6 +916,7 @@ func (m *Launcher) run(ctx context.Context, opts *InfluxdOpts) (err error) {
|
|||
http.WithResourceHandler(annotationServer),
|
||||
http.WithResourceHandler(remotesServer),
|
||||
http.WithResourceHandler(replicationServer),
|
||||
http.WithResourceHandler(configHandler),
|
||||
)
|
||||
|
||||
httpLogger := m.log.With(zap.String("service", "http"))
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
"github.com/influxdata/influxdb/v2/authorizer"
|
||||
"github.com/influxdata/influxdb/v2/kit/cli"
|
||||
"github.com/influxdata/influxdb/v2/kit/platform"
|
||||
"github.com/influxdata/influxdb/v2/kit/platform/errors"
|
||||
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
|
||||
"github.com/spf13/pflag"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
const prefixConfig = "/api/v2/config"
|
||||
|
||||
func errInvalidType(dest interface{}, flag string) error {
|
||||
return &errors.Error{
|
||||
Code: errors.EInternal,
|
||||
Err: fmt.Errorf("unknown destination type %T for %q", dest, flag),
|
||||
}
|
||||
}
|
||||
|
||||
type parsedOpt map[string]optValue
|
||||
|
||||
type optValue []byte
|
||||
|
||||
func (o optValue) MarshalJSON() ([]byte, error) { return o, nil }
|
||||
|
||||
type ConfigHandler struct {
|
||||
chi.Router
|
||||
|
||||
log *zap.Logger
|
||||
api *kithttp.API
|
||||
|
||||
config parsedOpt
|
||||
}
|
||||
|
||||
// NewConfigHandler creates a handler that will return a JSON object with key/value pairs for the configuration values
|
||||
// used during the launcher startup. The opts slice provides a list of options names along with a pointer to their
|
||||
// value.
|
||||
func NewConfigHandler(log *zap.Logger, opts []cli.Opt) (*ConfigHandler, error) {
|
||||
h := &ConfigHandler{
|
||||
log: log,
|
||||
api: kithttp.NewAPI(kithttp.WithLog(log)),
|
||||
}
|
||||
|
||||
if err := h.parseOptions(opts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Use(
|
||||
middleware.Recoverer,
|
||||
middleware.RequestID,
|
||||
middleware.RealIP,
|
||||
h.mwAuthorize,
|
||||
)
|
||||
|
||||
r.Get("/", h.handleGetConfig)
|
||||
h.Router = r
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (h *ConfigHandler) Prefix() string {
|
||||
return prefixConfig
|
||||
}
|
||||
|
||||
func (h *ConfigHandler) parseOptions(opts []cli.Opt) error {
|
||||
h.config = make(parsedOpt)
|
||||
|
||||
for _, o := range opts {
|
||||
var b []byte
|
||||
switch o.DestP.(type) {
|
||||
// Known types for configuration values. Currently, these can all be encoded directly with json.Marshal.
|
||||
case *string, *int, *int32, *int64, *bool, *time.Duration, *[]string, *map[string]string, pflag.Value, *platform.ID, *zapcore.Level:
|
||||
var err error
|
||||
b, err = json.Marshal(o.DestP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
// Return an error if we don't know how to marshal this type.
|
||||
return errInvalidType(o.DestP, o.Flag)
|
||||
}
|
||||
|
||||
h.config[o.Flag] = b
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ConfigHandler) handleGetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
h.api.Respond(w, r, http.StatusOK, map[string]parsedOpt{"config": h.config})
|
||||
}
|
||||
|
||||
func (h *ConfigHandler) mwAuthorize(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := authorizer.IsAllowedAll(r.Context(), influxdb.OperPermissions()); err != nil {
|
||||
h.api.Err(w, r, &errors.Error{
|
||||
Code: errors.EUnauthorized,
|
||||
Msg: fmt.Sprintf("access to %s requires operator permissions", h.Prefix()),
|
||||
})
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdata/influxdb/v2"
|
||||
influxdbcontext "github.com/influxdata/influxdb/v2/context"
|
||||
"github.com/influxdata/influxdb/v2/kit/cli"
|
||||
"github.com/influxdata/influxdb/v2/kit/platform"
|
||||
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
|
||||
"github.com/influxdata/influxdb/v2/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap/zaptest"
|
||||
)
|
||||
|
||||
func TestConfigHandler(t *testing.T) {
|
||||
t.Run("known types", func(t *testing.T) {
|
||||
stringFlag := "some string"
|
||||
boolFlag := true
|
||||
idFlag := platform.ID(1)
|
||||
|
||||
opts := []cli.Opt{
|
||||
{
|
||||
DestP: &stringFlag,
|
||||
Flag: "string-flag",
|
||||
},
|
||||
{
|
||||
DestP: &boolFlag,
|
||||
Flag: "bool-flag",
|
||||
},
|
||||
{
|
||||
DestP: &idFlag,
|
||||
Flag: "id-flag",
|
||||
},
|
||||
}
|
||||
|
||||
want := map[string]interface{}{
|
||||
"config": map[string]interface{}{
|
||||
"string-flag": stringFlag,
|
||||
"bool-flag": boolFlag,
|
||||
"id-flag": idFlag,
|
||||
},
|
||||
}
|
||||
wantJsonBytes, err := json.Marshal(want)
|
||||
require.NoError(t, err)
|
||||
var wantDecoded map[string]interface{}
|
||||
require.NoError(t, json.NewDecoder(bytes.NewReader(wantJsonBytes)).Decode(&wantDecoded))
|
||||
|
||||
h, err := NewConfigHandler(zaptest.NewLogger(t), opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
r, err := http.NewRequest(http.MethodGet, "/", nil)
|
||||
require.NoError(t, err)
|
||||
ctx := influxdbcontext.SetAuthorizer(context.Background(), mock.NewMockAuthorizer(false, influxdb.OperPermissions()))
|
||||
r = r.WithContext(ctx)
|
||||
h.ServeHTTP(rr, r)
|
||||
rs := rr.Result()
|
||||
|
||||
var gotDecoded map[string]interface{}
|
||||
require.NoError(t, json.NewDecoder(rs.Body).Decode(&gotDecoded))
|
||||
require.Equal(t, gotDecoded, wantDecoded)
|
||||
})
|
||||
|
||||
t.Run("unknown type", func(t *testing.T) {
|
||||
var floatFlag float64
|
||||
|
||||
opts := []cli.Opt{
|
||||
{
|
||||
DestP: &floatFlag,
|
||||
Flag: "float-flag",
|
||||
},
|
||||
}
|
||||
|
||||
h, err := NewConfigHandler(zaptest.NewLogger(t), opts)
|
||||
require.Nil(t, h)
|
||||
require.Equal(t, errInvalidType(&floatFlag, "float-flag"), err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestConfigHandler_Authorization(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
permList []influxdb.Permission
|
||||
wantStatus int
|
||||
}{
|
||||
{
|
||||
"authorized to see config",
|
||||
influxdb.OperPermissions(),
|
||||
http.StatusOK,
|
||||
},
|
||||
{
|
||||
"not authorized to see config",
|
||||
influxdb.ReadAllPermissions(),
|
||||
http.StatusUnauthorized,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
r, err := http.NewRequest(http.MethodGet, "/", nil)
|
||||
require.NoError(t, err)
|
||||
ctx := influxdbcontext.SetAuthorizer(context.Background(), mock.NewMockAuthorizer(false, tt.permList))
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
h := ConfigHandler{
|
||||
api: kithttp.NewAPI(kithttp.WithLog(zaptest.NewLogger(t))),
|
||||
}
|
||||
h.mwAuthorize(next).ServeHTTP(rr, r)
|
||||
rs := rr.Result()
|
||||
|
||||
require.Equal(t, tt.wantStatus, rs.StatusCode)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ declare -r ROOT_DIR=$(dirname ${SCRIPT_DIR})
|
|||
declare -r STATIC_DIR="$ROOT_DIR/static"
|
||||
|
||||
# Pins the swagger that will be downloaded to a specific commit
|
||||
declare -r OPENAPI_SHA=1243aa6c501b26aabb1c32121de1e235152398a6
|
||||
declare -r OPENAPI_SHA=8b5f1bbb2cd388eb454dc9da19e3d2c4061cdf5f
|
||||
|
||||
# Don't do a shallow clone since the commit we want might be several commits
|
||||
# back; but do only clone the main branch.
|
||||
|
|
Loading…
Reference in New Issue