Feature flags API (#10613)

* Added sys/internal/ui/feature-flags endpoint.
* Added documentation for new API endpoint.
* Added integration test.
Co-authored-by: swayne275 <swayne@hashicorp.com>
This commit is contained in:
Mark Gritter 2021-01-06 16:05:00 -06:00 committed by GitHub
parent ad42d8f6ec
commit d076d95d37
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 204 additions and 0 deletions

3
changelog/10613.txt Normal file
View file

@ -0,0 +1,3 @@
```release-node:improvement
core: Added an internal endpoint that lists feature flags.
```

View file

@ -146,6 +146,7 @@ func Handler(props *vault.HandlerProperties) http.Handler {
mux.Handle("/v1/sys/rekey-recovery-key/verify", handleRequestForwarding(core, handleSysRekeyVerify(core, true))) mux.Handle("/v1/sys/rekey-recovery-key/verify", handleRequestForwarding(core, handleSysRekeyVerify(core, true)))
mux.Handle("/v1/sys/storage/raft/bootstrap", handleSysRaftBootstrap(core)) mux.Handle("/v1/sys/storage/raft/bootstrap", handleSysRaftBootstrap(core))
mux.Handle("/v1/sys/storage/raft/join", handleSysRaftJoin(core)) mux.Handle("/v1/sys/storage/raft/join", handleSysRaftJoin(core))
mux.Handle("/v1/sys/internal/ui/feature-flags", handleSysInternalFeatureFlags(core))
for _, path := range injectDataIntoTopRoutes { for _, path := range injectDataIntoTopRoutes {
mux.Handle(path, handleRequestForwarding(core, handleLogicalWithInjector(core))) mux.Handle(path, handleRequestForwarding(core, handleLogicalWithInjector(core)))
} }

52
http/sys_feature_flags.go Normal file
View file

@ -0,0 +1,52 @@
package http
import (
"encoding/json"
"net/http"
"os"
"github.com/hashicorp/vault/vault"
)
type FeatureFlagsResponse struct {
FeatureFlags []string `json:"feature_flags"`
}
var FeatureFlag_EnvVariables = [...]string{
"VAULT_CLOUD_ADMIN_NAMESPACE",
}
func featureFlagIsSet(name string) bool {
switch os.Getenv(name) {
case "", "0":
return false
default:
return true
}
}
func handleSysInternalFeatureFlags(core *vault.Core) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
break
default:
respondError(w, http.StatusMethodNotAllowed, nil)
}
response := &FeatureFlagsResponse{}
for _, f := range FeatureFlag_EnvVariables {
if featureFlagIsSet(f) {
response.FeatureFlags = append(response.FeatureFlags, f)
}
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
// Generate the response
enc := json.NewEncoder(w)
enc.Encode(response)
})
}

View file

@ -0,0 +1,93 @@
package api
import (
"encoding/json"
"io/ioutil"
"net/http"
"os"
"testing"
"github.com/hashicorp/go-cleanhttp"
vaulthttp "github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/vault"
"golang.org/x/net/http2"
)
func TestFeatureFlags(t *testing.T) {
cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
// Wait for core to start
core := cluster.Cores[0].Core
vault.TestWaitActive(t, core)
client := cluster.Cores[0].Client
// Create a raw http connection copying the configuration
// created by NewTestCluster
transport := cleanhttp.DefaultPooledTransport()
transport.TLSClientConfig = cluster.Cores[0].TLSConfig.Clone()
if err := http2.ConfigureTransport(transport); err != nil {
t.Fatal(err)
}
httpClient := &http.Client{
Transport: transport,
}
callApi := func() map[string]interface{} {
// Use the normal API client to construct the URL
req := client.NewRequest("GET", "/v1/sys/internal/ui/feature-flags")
httpReq, err := req.ToHTTP()
if err != nil {
t.Fatal(err)
}
resp, err := httpClient.Do(httpReq)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
httpRespBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
httpResp := make(map[string]interface{})
err = json.Unmarshal(httpRespBody, &httpResp)
if err != nil {
t.Fatal(err)
}
return httpResp
}
// First try with no environment variable set
httpResp := callApi()
featureFlags, ok := httpResp["feature_flags"]
if !ok {
t.Fatal("Missing 'feature_flags' in response")
}
if featureFlags != nil {
t.Fatal("Nonempty 'feature_flags'")
}
// Now try with the environment variable temporarily set
envVar := "VAULT_CLOUD_ADMIN_NAMESPACE"
os.Setenv(envVar, "1")
defer os.Unsetenv(envVar)
httpResp = callApi()
featureFlags, ok = httpResp["feature_flags"]
if !ok {
t.Fatal("Missing 'feature_flags' in response")
}
flagList := featureFlags.([]interface{})
if len(flagList) != 1 {
t.Fatalf("Bad length for 'feature_flags': %v", flagList)
}
flag := flagList[0].(string)
if flag != envVar {
t.Fatalf("Bad environment variable in `feature_flags`: %q", flag)
}
}

View file

@ -4367,6 +4367,10 @@ This path responds to the following HTTP methods.
"Write, Read, and Delete data directly in the Storage backend.", "Write, Read, and Delete data directly in the Storage backend.",
"", "",
}, },
"internal-ui-feature-flags": {
"Enabled feature flags. Internal API; its location, inputs, and outputs may change.",
"",
},
"internal-ui-mounts": { "internal-ui-mounts": {
"Information about mounts returned according to their tuned visibility. Internal API; its location, inputs, and outputs may change.", "Information about mounts returned according to their tuned visibility. Internal API; its location, inputs, and outputs may change.",
"", "",

View file

@ -829,6 +829,17 @@ func (b *SystemBackend) internalPaths() []*framework.Path {
}, },
}, },
}, },
{
Pattern: "internal/ui/feature-flags",
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
// callback is absent because this is an unauthenticated method
Summary: "Lists enabled feature flags.",
},
},
HelpSynopsis: strings.TrimSpace(sysHelp["internal-ui-feature-flags"][0]),
HelpDescription: strings.TrimSpace(sysHelp["internal-ui-feature-flags"][1]),
},
{ {
Pattern: "internal/ui/mounts", Pattern: "internal/ui/mounts",
Operations: map[logical.Operation]framework.OperationHandler{ Operations: map[logical.Operation]framework.OperationHandler{

View file

@ -0,0 +1,39 @@
---
layout: api
page_title: /sys/internal/ui/feature-flags - HTTP API
sidebar_title: <code>/sys/internal/ui/feature-flags</code>
description: >-
The `/sys/internal/ui/feature-flags` endpoint exposes feature flags to the UI.
---
# `/sys/internal/ui/feature-flags`
The `/sys/internal/ui/feature-flags` endpoint is used to expose feature flags
to the UI so that it can change its behavior in response, even before a user logs in.
This is currently only being used internally for the UI and is
an unauthenticated endpoint. Due to the nature of its intended usage, there is no
guarantee on backwards compatibility for this endpoint.
## Get Enabled Feature Flags
This endpoint lists the enabled feature flags relevant to the UI.
| Method | Path |
| :----- | :------------------------------- |
| `GET` | `/sys/internal/ui/feature-flags` |
### Sample Request
```shell-session
$ curl \
http://127.0.0.1:8200/v1/sys/internal/ui/feature-flags
```
### Sample Response
```json
{
"feature-flags": []
}
```

View file

@ -111,6 +111,7 @@ export default [
'init', 'init',
'internal-counters', 'internal-counters',
'internal-specs-openapi', 'internal-specs-openapi',
'internal-ui-feature',
'internal-ui-mounts', 'internal-ui-mounts',
'key-status', 'key-status',
'leader', 'leader',