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
3
changelog/10613.txt
Normal file
3
changelog/10613.txt
Normal file
|
@ -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/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
52
http/sys_feature_flags.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
93
vault/external_tests/api/feature_flag_ext_test.go
Normal file
93
vault/external_tests/api/feature_flag_ext_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.",
|
||||||
"",
|
"",
|
||||||
|
|
|
@ -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{
|
||||||
|
|
39
website/content/api-docs/system/internal-ui-feature.mdx
Normal file
39
website/content/api-docs/system/internal-ui-feature.mdx
Normal 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": []
|
||||||
|
}
|
||||||
|
```
|
|
@ -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',
|
||||||
|
|
Loading…
Reference in a new issue