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:
parent
ad42d8f6ec
commit
d076d95d37
|
@ -0,0 +1,3 @@
|
|||
```release-node:improvement
|
||||
core: Added an internal endpoint that lists feature flags.
|
||||
```
|
|
@ -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/storage/raft/bootstrap", handleSysRaftBootstrap(core))
|
||||
mux.Handle("/v1/sys/storage/raft/join", handleSysRaftJoin(core))
|
||||
mux.Handle("/v1/sys/internal/ui/feature-flags", handleSysInternalFeatureFlags(core))
|
||||
for _, path := range injectDataIntoTopRoutes {
|
||||
mux.Handle(path, handleRequestForwarding(core, handleLogicalWithInjector(core)))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -4367,6 +4367,10 @@ This path responds to the following HTTP methods.
|
|||
"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": {
|
||||
"Information about mounts returned according to their tuned visibility. Internal API; its location, inputs, and outputs may change.",
|
||||
"",
|
||||
|
|
|
@ -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",
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
|
|
|
@ -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": []
|
||||
}
|
||||
```
|
|
@ -111,6 +111,7 @@ export default [
|
|||
'init',
|
||||
'internal-counters',
|
||||
'internal-specs-openapi',
|
||||
'internal-ui-feature',
|
||||
'internal-ui-mounts',
|
||||
'key-status',
|
||||
'leader',
|
||||
|
|
Loading…
Reference in New Issue