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 ref
pull/23026/head
William Baker 2021-12-23 09:27:39 -05:00 committed by GitHub
parent afb167a2ca
commit f78f9eda9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 250 additions and 1 deletions

View File

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

116
http/config.go Normal file
View File

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

127
http/config_test.go Normal file
View File

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

View File

@ -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.