(2.14) Feature flags
Signed-off-by: Maurice van Veen <github@mauricevanveen.com>
This commit is contained in:
@@ -7,6 +7,12 @@ server_metadata {
|
||||
key2: value2
|
||||
}
|
||||
|
||||
feature_flags {
|
||||
feature: false
|
||||
fix: true
|
||||
revert_fix: true
|
||||
}
|
||||
|
||||
listen: 127.0.0.1:4242
|
||||
|
||||
http: 8222
|
||||
|
||||
+11
-9
@@ -247,14 +247,15 @@ type ServerCapability uint64
|
||||
|
||||
// ServerInfo identifies remote servers.
|
||||
type ServerInfo struct {
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
ID string `json:"id"`
|
||||
Cluster string `json:"cluster,omitempty"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
Version string `json:"ver"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
ID string `json:"id"`
|
||||
Cluster string `json:"cluster,omitempty"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
Version string `json:"ver"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
FeatureFlags map[string]bool `json:"feature_flags,omitempty"`
|
||||
// Whether JetStream is enabled (deprecated in favor of the `ServerCapability`).
|
||||
JetStream bool `json:"jetstream"`
|
||||
// Generic capability flags
|
||||
@@ -519,7 +520,7 @@ RESET:
|
||||
|
||||
// Grab tags and metadata.
|
||||
opts := s.getOpts()
|
||||
tags, metadata := opts.Tags, opts.Metadata
|
||||
tags, metadata, featureFlags := opts.Tags, opts.Metadata, opts.getMergedFeatureFlags()
|
||||
|
||||
for s.eventsRunning() {
|
||||
select {
|
||||
@@ -537,6 +538,7 @@ RESET:
|
||||
si.Time = time.Now().UTC()
|
||||
si.Tags = tags
|
||||
si.Metadata = metadata
|
||||
si.FeatureFlags = featureFlags
|
||||
si.Flags = 0
|
||||
if js {
|
||||
// New capability based flags.
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright 2026 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var featureFlags = map[string]bool{}
|
||||
|
||||
// getFeatureFlag is used to retrieve either the default or overwritten value for a feature flag.
|
||||
// The user's value takes precedence over the system's default. However, if the flag doesn't exist, it's disabled.
|
||||
// Options read lock should be held.
|
||||
func (o *Options) getFeatureFlag(k string) bool {
|
||||
defaultValue, ok := featureFlags[k]
|
||||
if !ok {
|
||||
return false // Not supported.
|
||||
}
|
||||
if userValue, ok := o.FeatureFlags[k]; ok {
|
||||
return userValue
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// getMergedFeatureFlags returns a merged map of feature flags, with the user's values taking precedence.
|
||||
func (o *Options) getMergedFeatureFlags() map[string]bool {
|
||||
merged := make(map[string]bool)
|
||||
for k, v := range featureFlags {
|
||||
merged[k] = v
|
||||
}
|
||||
for k, v := range o.FeatureFlags {
|
||||
if _, ok := featureFlags[k]; !ok {
|
||||
continue
|
||||
}
|
||||
merged[k] = v
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
// printFeatureFlags logs the currently used feature flags on server startup.
|
||||
func (s *Server) printFeatureFlags(o *Options) {
|
||||
if len(o.FeatureFlags) == 0 {
|
||||
return
|
||||
}
|
||||
keys := slices.Sorted(maps.Keys(o.FeatureFlags))
|
||||
|
||||
var (
|
||||
configured strings.Builder
|
||||
unsupported strings.Builder
|
||||
)
|
||||
|
||||
for _, k := range keys {
|
||||
// Unsupported
|
||||
defaultValue, ok := featureFlags[k]
|
||||
if !ok {
|
||||
if unsupported.Len() > 0 {
|
||||
unsupported.WriteString(", ")
|
||||
}
|
||||
unsupported.WriteString(k)
|
||||
continue
|
||||
}
|
||||
|
||||
v := o.FeatureFlags[k]
|
||||
if configured.Len() > 0 {
|
||||
configured.WriteString(", ")
|
||||
}
|
||||
configured.WriteString(k)
|
||||
configured.WriteString(" (")
|
||||
if defaultValue {
|
||||
if v {
|
||||
configured.WriteString("enabled")
|
||||
} else {
|
||||
configured.WriteString("opt-out")
|
||||
}
|
||||
} else if v {
|
||||
configured.WriteString("opt-in")
|
||||
} else {
|
||||
configured.WriteString("disabled")
|
||||
}
|
||||
configured.WriteString(")")
|
||||
}
|
||||
if configured.Len() == 0 {
|
||||
configured.WriteString("none")
|
||||
}
|
||||
|
||||
s.Noticef(" Feature flags:")
|
||||
s.Noticef(" Configured: %s", configured.String())
|
||||
if unsupported.Len() > 0 {
|
||||
s.Noticef(" Unsupported: %s", unsupported.String())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2026 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetFeatureFlag(t *testing.T) {
|
||||
featureFlags["test_flag"] = true
|
||||
t.Cleanup(func() { delete(featureFlags, "test_flag") })
|
||||
|
||||
// Unset feature flags gets defaults.
|
||||
o := &Options{}
|
||||
require_Equal(t, o.getFeatureFlag(""), false)
|
||||
require_Equal(t, o.getFeatureFlag("test_flag"), true)
|
||||
|
||||
// User disables the feature flag, it takes precedence.
|
||||
o.FeatureFlags = map[string]bool{"test_flag": false}
|
||||
require_Equal(t, o.getFeatureFlag("test_flag"), false)
|
||||
|
||||
// User enables the feature flag, but we don't support it anymore.
|
||||
// It's allowed for the user to provide stale flags, but we shouldn't return they're enabled.
|
||||
delete(featureFlags, "test_flag")
|
||||
o.FeatureFlags = map[string]bool{"test_flag": true}
|
||||
require_Equal(t, o.getFeatureFlag("test_flag"), false)
|
||||
}
|
||||
|
||||
func TestGetMergedFeatureFlags(t *testing.T) {
|
||||
previous := featureFlags
|
||||
featureFlags = make(map[string]bool)
|
||||
featureFlags["test_flag"] = true
|
||||
featureFlags["test_flag_default"] = true
|
||||
t.Cleanup(func() { featureFlags = previous })
|
||||
|
||||
o := &Options{}
|
||||
o.FeatureFlags = map[string]bool{"test_flag": false, "unknown_flag": false}
|
||||
|
||||
// The merged flags should only contain known flags, with the user's values taking precedence.
|
||||
merged := o.getMergedFeatureFlags()
|
||||
expected := map[string]bool{"test_flag": false, "test_flag_default": true}
|
||||
if !reflect.DeepEqual(expected, merged) {
|
||||
t.Fatalf("Feature flags are incorrect.\nexpected: %+v\ngot: %+v", expected, merged)
|
||||
}
|
||||
}
|
||||
@@ -1271,6 +1271,7 @@ type Varz struct {
|
||||
ConfigDigest string `json:"config_digest"` // ConfigDigest is a calculated hash of the current configuration
|
||||
Tags jwt.TagList `json:"tags,omitempty"` // Tags are the tags assigned to the server in configuration
|
||||
Metadata map[string]string `json:"metadata,omitempty"` // Metadata is the metadata assigned to the server in configuration
|
||||
FeatureFlags map[string]bool `json:"feature_flags,omitempty"` // FeatureFlags is the feature flags enabled/disabled in configuration
|
||||
TrustedOperatorsJwt []string `json:"trusted_operators_jwt,omitempty"` // TrustedOperatorsJwt is the JWTs for all trusted operators
|
||||
TrustedOperatorsClaim []*jwt.OperatorClaims `json:"trusted_operators_claim,omitempty"` // TrustedOperatorsClaim is the decoded claims for each trusted operator
|
||||
SystemAccount string `json:"system_account,omitempty"` // SystemAccount is the name of the System account
|
||||
@@ -1793,6 +1794,7 @@ func (s *Server) updateVarzConfigReloadableFields(v *Varz) {
|
||||
v.ConfigDigest = opts.configDigest
|
||||
v.Tags = opts.Tags
|
||||
v.Metadata = opts.Metadata
|
||||
v.FeatureFlags = opts.getMergedFeatureFlags()
|
||||
// Update route URLs if applicable
|
||||
if s.varzUpdateRouteURLs {
|
||||
v.Cluster.URLs = urlsToStrings(opts.Routes)
|
||||
|
||||
+23
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright 2013-2025 The NATS Authors
|
||||
// Copyright 2013-2026 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
@@ -56,6 +56,7 @@ func DefaultMonitorOptions() *Options {
|
||||
NoSigs: true,
|
||||
Tags: []string{"tag"},
|
||||
Metadata: map[string]string{"key1": "value1", "key2": "value2"},
|
||||
FeatureFlags: map[string]bool{"feature": false, "fix": true, "revert_fix": true},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6559,6 +6560,27 @@ func TestMonitorVarzMetadata(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMonitorVarzFeatureFlags(t *testing.T) {
|
||||
featureFlags["fix"] = false
|
||||
t.Cleanup(func() { delete(featureFlags, "fix") })
|
||||
|
||||
expected := make(map[string]bool)
|
||||
for k, v := range featureFlags {
|
||||
expected[k] = v
|
||||
}
|
||||
expected["fix"] = true
|
||||
|
||||
s := runMonitorServer()
|
||||
defer s.Shutdown()
|
||||
|
||||
v, err := s.Varz(nil)
|
||||
require_NoError(t, err)
|
||||
|
||||
if !reflect.DeepEqual(expected, v.FeatureFlags) {
|
||||
t.Fatalf("expected: %v, got: %v", expected, v.FeatureFlags)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMonitorVarzTLSCertEndDate(t *testing.T) {
|
||||
resetPreviousHTTPConnections()
|
||||
opts := DefaultMonitorOptions()
|
||||
|
||||
@@ -479,6 +479,9 @@ type Options struct {
|
||||
// Metadata describing the server. They will be included in 'Z' responses.
|
||||
Metadata map[string]string `json:"-"`
|
||||
|
||||
// FeatureFlags the server opts-in to (or opts-out of). They will be included in 'Z' responses.
|
||||
FeatureFlags map[string]bool `json:"-"`
|
||||
|
||||
// OCSPConfig enables OCSP Stapling in the server.
|
||||
OCSPConfig *OCSPConfig
|
||||
tlsConfigOpts *TLSConfigOpts
|
||||
@@ -1749,6 +1752,24 @@ func (o *Options) processConfigFileLine(k string, v any, errors *[]error, warnin
|
||||
*errors = append(*errors, err)
|
||||
return
|
||||
}
|
||||
case "feature_flags":
|
||||
var err error
|
||||
switch v := v.(type) {
|
||||
case map[string]any:
|
||||
for mk, mv := range v {
|
||||
tk, mv = unwrapValue(mv, <)
|
||||
if o.FeatureFlags == nil {
|
||||
o.FeatureFlags = make(map[string]bool)
|
||||
}
|
||||
o.FeatureFlags[mk] = mv.(bool)
|
||||
}
|
||||
default:
|
||||
err = &configErr{tk, fmt.Sprintf("error parsing feature flags: unsupported type %T", v)}
|
||||
}
|
||||
if err != nil {
|
||||
*errors = append(*errors, err)
|
||||
return
|
||||
}
|
||||
case "default_js_domain":
|
||||
vv, ok := v.(map[string]any)
|
||||
if !ok {
|
||||
|
||||
+5
-3
@@ -1,4 +1,4 @@
|
||||
// Copyright 2012-2025 The NATS Authors
|
||||
// Copyright 2012-2026 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
@@ -125,7 +125,8 @@ func TestConfigFile(t *testing.T) {
|
||||
ConnectErrorReports: 86400,
|
||||
ReconnectErrorReports: 5,
|
||||
Metadata: map[string]string{"key1": "value1", "key2": "value2"},
|
||||
configDigest: "sha256:a1104db0c8e838096a4f0509ec4d1e7c2c26ff60261ecb8f6a12dde1317872c3",
|
||||
FeatureFlags: map[string]bool{"feature": false, "fix": true, "revert_fix": true},
|
||||
configDigest: "sha256:f10eacddb9ce83a6bdc79b42c851b9628d49cf8b3c6ba95b1ecf090307504948",
|
||||
authBlockDefined: true,
|
||||
}
|
||||
|
||||
@@ -299,7 +300,8 @@ func TestMergeOverrides(t *testing.T) {
|
||||
StoreDir: "/store/dir",
|
||||
authBlockDefined: true,
|
||||
Metadata: map[string]string{"key1": "value1", "key2": "value2"},
|
||||
configDigest: "sha256:a1104db0c8e838096a4f0509ec4d1e7c2c26ff60261ecb8f6a12dde1317872c3",
|
||||
FeatureFlags: map[string]bool{"feature": false, "fix": true, "revert_fix": true},
|
||||
configDigest: "sha256:f10eacddb9ce83a6bdc79b42c851b9628d49cf8b3c6ba95b1ecf090307504948",
|
||||
}
|
||||
fopts, err := ProcessConfigFile("./configs/test.conf")
|
||||
if err != nil {
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
// Copyright 2017-2025 The NATS Authors
|
||||
// Copyright 2017-2026 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
@@ -1258,7 +1258,7 @@ func imposeOrder(value any) error {
|
||||
slices.Sort(value.AllowedOrigins)
|
||||
case string, bool, uint8, uint16, uint64, int, int32, int64, time.Duration, float64, nil, LeafNodeOpts, ClusterOpts, *tls.Config, PinnedCertSet,
|
||||
*URLAccResolver, *MemAccResolver, *DirAccResolver, *CacheDirAccResolver, Authentication, MQTTOpts, jwt.TagList,
|
||||
*OCSPConfig, map[string]string, JSLimitOpts, StoreCipher, *OCSPResponseCacheConfig, *ProxiesConfig, WriteTimeoutPolicy:
|
||||
*OCSPConfig, map[string]string, map[string]bool, JSLimitOpts, StoreCipher, *OCSPResponseCacheConfig, *ProxiesConfig, WriteTimeoutPolicy:
|
||||
// explicitly skipped types
|
||||
case *AuthCallout:
|
||||
case JSTpmOpts:
|
||||
|
||||
@@ -2294,6 +2294,7 @@ func (s *Server) Start() {
|
||||
s.Noticef(" Node: %s", getHash(s.info.Name))
|
||||
}
|
||||
s.Noticef(" ID: %s", s.info.ID)
|
||||
s.printFeatureFlags(opts)
|
||||
|
||||
defer s.Noticef("Server is ready")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user