(2.14) Feature flags

Signed-off-by: Maurice van Veen <github@mauricevanveen.com>
This commit is contained in:
Maurice van Veen
2026-04-08 11:01:19 +02:00
parent 15056de099
commit 26b6597fc3
10 changed files with 232 additions and 15 deletions
+6
View File
@@ -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
View File
@@ -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.
+104
View File
@@ -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())
}
}
+57
View File
@@ -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)
}
}
+2
View File
@@ -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
View File
@@ -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()
+21
View File
@@ -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, &lt)
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
View File
@@ -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
View File
@@ -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:
+1
View File
@@ -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")