From f78f9eda9ca4cbe1b0e22265235b8b8739b91640 Mon Sep 17 00:00:00 2001
From: William Baker <wbaker@gmail.com>
Date: Thu, 23 Dec 2021 09:27:39 -0500
Subject: [PATCH] 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
---
 cmd/influxd/launcher/launcher.go |   6 ++
 http/config.go                   | 116 ++++++++++++++++++++++++++++
 http/config_test.go              | 127 +++++++++++++++++++++++++++++++
 scripts/fetch-swagger.sh         |   2 +-
 4 files changed, 250 insertions(+), 1 deletion(-)
 create mode 100644 http/config.go
 create mode 100644 http/config_test.go

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.