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(
|
platformHandler := http.NewPlatformHandler(
|
||||||
m.apibackend,
|
m.apibackend,
|
||||||
http.WithResourceHandler(stacksHTTPServer),
|
http.WithResourceHandler(stacksHTTPServer),
|
||||||
|
@ -911,6 +916,7 @@ func (m *Launcher) run(ctx context.Context, opts *InfluxdOpts) (err error) {
|
||||||
http.WithResourceHandler(annotationServer),
|
http.WithResourceHandler(annotationServer),
|
||||||
http.WithResourceHandler(remotesServer),
|
http.WithResourceHandler(remotesServer),
|
||||||
http.WithResourceHandler(replicationServer),
|
http.WithResourceHandler(replicationServer),
|
||||||
|
http.WithResourceHandler(configHandler),
|
||||||
)
|
)
|
||||||
|
|
||||||
httpLogger := m.log.With(zap.String("service", "http"))
|
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"
|
declare -r STATIC_DIR="$ROOT_DIR/static"
|
||||||
|
|
||||||
# Pins the swagger that will be downloaded to a specific commit
|
# 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
|
# Don't do a shallow clone since the commit we want might be several commits
|
||||||
# back; but do only clone the main branch.
|
# back; but do only clone the main branch.
|
||||||
|
|
Loading…
Reference in New Issue