diff --git a/cmd/influxd/launcher/launcher.go b/cmd/influxd/launcher/launcher.go index 10444d755d..f54f8b753d 100644 --- a/cmd/influxd/launcher/launcher.go +++ b/cmd/influxd/launcher/launcher.go @@ -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")) diff --git a/http/config.go b/http/config.go new file mode 100644 index 0000000000..def5beeda2 --- /dev/null +++ b/http/config.go @@ -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) +} diff --git a/http/config_test.go b/http/config_test.go new file mode 100644 index 0000000000..0b14beaf68 --- /dev/null +++ b/http/config_test.go @@ -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) + }) + } +} diff --git a/scripts/fetch-swagger.sh b/scripts/fetch-swagger.sh index a423f4a69a..08f202a9d6 100755 --- a/scripts/fetch-swagger.sh +++ b/scripts/fetch-swagger.sh @@ -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.