Skip to content

Commit 3591223

Browse files
authored
Environment variable prefix override support (#8)
* use DISABLE_CODE_ prefix for environment variable names * add DISABLE_S2CODE_ prefix as a temporary backward compatibility option * environment variable prefix override support at compile time * support Go back to 1.23.0
1 parent 94b8af4 commit 3591223

File tree

4 files changed

+83
-47
lines changed

4 files changed

+83
-47
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ once.
3636
## Controlling the Gate
3737

3838
Code gates are controlled by environment variables. Code gates are created in the enabled state unless an environment
39-
variable named `DISABLE_<codegate-name>` is found in the process environment. The value of the variable is ignored;
39+
variable named `DISABLE_CODE_<codegate-name>` is found in the process environment. The value of the variable is ignored;
4040
any value, including blank, is sufficient to disable the code gate. The normal use case for a code gate is to disable or
4141
revert some code by defining the code gate environment variable and *restarting* the service or application.
4242

codegate.go

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,38 @@ import (
99
)
1010

1111
// Gate is a code gate, allowing code to be selectively enabled or disabled.
12-
// Gate currently contains only a Name field, but may grow in the future to
13-
// include other metadata about the gate.
1412
type Gate struct {
1513
name string
1614
enabled bool
1715
}
1816

17+
// EnvVarPrefix is the prefix for environment variables used to disable
18+
// code gates. This prefix is defined in an exported variable so that it
19+
// may be changed at compile time with build flags:
20+
//
21+
// go build -ldflags "-X 'github.com/singlestore-labs/codegate.EnvVarPrefix=MY_PREFIX_'"
22+
//
23+
// This variable should not be changed at runtime including in init functions,
24+
// because it is used by the New function which may be called in static
25+
// initializers.
26+
var (
27+
EnvVarPrefix = "DISABLE_CODE_"
28+
)
29+
1930
const (
20-
gateNameMaxLength = 100
21-
gateEnvVarPrefix = "DISABLE_"
31+
// Deprecated: use DISABLE_CODE_ prefix instead
32+
envVarPrefix2 = "DISABLE_S2CODE_"
33+
nameMaxLength = 100
2234
)
2335

2436
var (
37+
// gate names must be valid environment variable names
2538
validName = regexp.MustCompile("^[A-Za-z][A-Za-z0-9_]*$")
2639
usedNames = map[string]struct{}{}
2740
disabledGates []string
41+
gateLock sync.Mutex
2842
)
2943

30-
var gateLock sync.Mutex
31-
3244
// New creates a code gate. Code gate names must be globally unique and should
3345
// be defined in static initializers. For example,
3446
//
@@ -39,20 +51,23 @@ var gateLock sync.Mutex
3951
// for each code domain (e.g., "RBAC" for RBAC related behaviors) is recommended.
4052
// New panics if the name is missing, invalid, or is a duplicate.
4153
func New(name string) Gate {
42-
if !validName.MatchString(name) || len(name) > gateNameMaxLength {
54+
if !validName.MatchString(name) || len(name) > nameMaxLength {
4355
panic(fmt.Errorf(`code gate name (%s) is invalid. Code gate names must begin with an alpha, contain only alphanumerics or underbars, and be no more than %d characters in length`,
44-
name, gateNameMaxLength))
56+
name, nameMaxLength))
4557
}
4658
gateLock.Lock()
4759
defer gateLock.Unlock()
48-
if _, ok := usedNames[name]; ok {
49-
panic(fmt.Errorf(`code gate name (%s) has been used twice. Code gate names must be unique`, name))
60+
if _, found := usedNames[name]; found {
61+
panic(fmt.Errorf(`code gate name (%s) is already in use. Code gate names must be unique`, name))
5062
}
5163
usedNames[name] = struct{}{}
52-
_, ok := os.LookupEnv(gateEnvVarPrefix + name)
64+
_, disabled := os.LookupEnv(EnvVarPrefix + name)
65+
if !disabled {
66+
_, disabled = os.LookupEnv(envVarPrefix2 + name)
67+
}
5368
return Gate{
5469
name: name,
55-
enabled: !ok,
70+
enabled: !disabled,
5671
}
5772
}
5873

@@ -91,21 +106,30 @@ func (gate Gate) String() string {
91106
return label + " (disabled)"
92107
}
93108

94-
// DisabledGates returns the names of all currently disabled code gates. If
95-
// forceRefresh is true, the list is reloaded from the environment variables.
96-
// forceRefresh should only be used for testing purposes.
97-
func DisabledGates(forceRefresh bool) []string {
109+
// DisabledGates returns the names of all currently disabled code gates. The
110+
// list is loaded from the environment variables and includes all variables
111+
// prefixed with the code gate prefix regardless of whether a gate has been
112+
// created for that name.
113+
func DisabledGates() []string {
98114
gateLock.Lock()
99115
defer gateLock.Unlock()
100-
if forceRefresh || disabledGates == nil {
116+
if disabledGates == nil {
101117
disabledGates = []string{}
102118
// Get all disabled code gates from the environment variables.
103119
for _, env := range os.Environ() {
104120
envName, _, _ := strings.Cut(env, "=")
105-
if strings.HasPrefix(envName, gateEnvVarPrefix) {
106-
disabledGates = append(disabledGates, strings.TrimPrefix(envName, gateEnvVarPrefix))
121+
if strings.HasPrefix(envName, EnvVarPrefix) {
122+
disabledGates = append(disabledGates, strings.TrimPrefix(envName, EnvVarPrefix))
123+
} else if strings.HasPrefix(envName, envVarPrefix2) {
124+
disabledGates = append(disabledGates, strings.TrimPrefix(envName, envVarPrefix2))
107125
}
108126
}
109127
}
110128
return disabledGates
111129
}
130+
131+
func resetDisabledGates() {
132+
gateLock.Lock()
133+
defer gateLock.Unlock()
134+
disabledGates = nil
135+
}

codegate_test.go

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
11
package codegate
22

33
import (
4-
"os"
54
"strings"
65
"testing"
76

87
"github.com/stretchr/testify/require"
98
)
109

11-
// Code gates are somewhat troublesome to test due to the reliance on a static environment variable
12-
// at the time of the gate creation. All tests run in the same environment, so setting an environment
13-
// variable in one test may affect other tests (depending on order.) DisableGates() testing only
14-
// works because it is implemented to dynamically inspect the environment at call time and does not
15-
// cache results.
10+
// Code gates are troublesome to test due to the reliance on static environment
11+
// variables at the time of the gate creation. All tests run in the same environment,
12+
// so setting an environment in one test may affect other tests. All tests in this
13+
// file should use unique gate names to avoid conflicts.
1614

1715
func TestNoDisabledGates(t *testing.T) {
1816
gateTestFoo := New("FOO")
1917
require.True(t, gateTestFoo.Enabled(), "arbitrary code behavior should be enabled by default")
20-
require.NotContains(t, DisabledGates(true), "FOO")
18+
require.NotContains(t, DisabledGates(), "FOO")
2119
}
2220

2321
func TestGateNames(t *testing.T) {
@@ -45,31 +43,45 @@ func TestGateNames(t *testing.T) {
4543
}
4644

4745
func TestDisableOneGate(t *testing.T) {
48-
_ = os.Setenv("DISABLE_Bar", "disabled")
46+
t.Setenv("DISABLE_CODE_Bar", "disabled")
4947

5048
// refresh disabled gates to pick up the changes to the environment
5149
// variables
52-
DisabledGates(true)
50+
resetDisabledGates()
5351

5452
gateTestBar := New("Bar")
5553
require.False(t, gateTestBar.Enabled(), "Bar should be disabled")
5654
require.True(t, New("Bar2").Enabled(), "Other gates should be enabled")
57-
require.Contains(t, DisabledGates(false), "Bar")
55+
require.Contains(t, DisabledGates(), "Bar")
56+
}
57+
58+
func TestDisableOldGate(t *testing.T) {
59+
t.Setenv("DISABLE_S2CODE_Deprecated", "disabled")
60+
61+
// refresh disabled gates to pick up the changes to the environment
62+
// variables
63+
resetDisabledGates()
64+
65+
gateTestDeprecated := New("Deprecated")
66+
require.False(t, gateTestDeprecated.Enabled(), "Deprecated should be disabled")
67+
require.True(t, New("Deprecated2").Enabled(), "Other gates should be enabled")
68+
require.Contains(t, DisabledGates(), "Deprecated")
5869
}
5970

6071
func TestDisableMultipleGates(t *testing.T) {
6172
// create some random environment variables
62-
_ = os.Setenv("NOISE", "LOUD")
63-
_ = os.Setenv("MORE_NOISE", "LOUDER")
64-
_ = os.Setenv("DISABLED_CODE", "")
73+
t.Setenv("NOISE", "LOUD")
74+
t.Setenv("MORE_NOISE", "LOUDER")
75+
t.Setenv("DISABLED_CODE", "")
76+
t.Setenv("DISABLED_CODE_TOO", "")
6577

6678
// disable two gates
67-
_ = os.Setenv("DISABLE_Baz1", "disabled")
68-
_ = os.Setenv("DISABLE_Baz3", "disabled")
79+
t.Setenv("DISABLE_CODE_Baz1", "disabled")
80+
t.Setenv("DISABLE_CODE_Baz3", "disabled")
6981

7082
// refresh disabled gates to pick up the changes to the environment
7183
// variables
72-
DisabledGates(true)
84+
resetDisabledGates()
7385

7486
// define four gates
7587
gateTestBaz1 := New("Baz1")
@@ -82,20 +94,20 @@ func TestDisableMultipleGates(t *testing.T) {
8294
require.False(t, gateTestBaz3.Enabled(), "Baz3 should be disabled")
8395
require.True(t, gateTestBaz4.Enabled(), "Baz4 should be enabled")
8496

85-
require.Contains(t, DisabledGates(false), "Baz1")
86-
require.NotContains(t, DisabledGates(false), "Baz2")
87-
require.Contains(t, DisabledGates(false), "Baz3")
88-
require.NotContains(t, DisabledGates(false), "Baz4")
97+
require.Contains(t, DisabledGates(), "Baz1")
98+
require.NotContains(t, DisabledGates(), "Baz2")
99+
require.Contains(t, DisabledGates(), "Baz3")
100+
require.NotContains(t, DisabledGates(), "Baz4")
89101
}
90102

91-
func TestRefreshDisabledGates(t *testing.T) {
103+
func TestResetDisabledGates(t *testing.T) {
92104
// ensure no disabled gates at start
93-
_ = os.Unsetenv("DISABLE_Foo")
94-
require.NotContains(t, DisabledGates(false), "Foo")
105+
require.NotContains(t, DisabledGates(), "Foo")
95106

96107
// disable Foo
97-
_ = os.Setenv("DISABLE_Foo", "disabled")
98-
require.NotContains(t, DisabledGates(false), "Foo")
108+
t.Setenv("DISABLE_CODE_Foo", "disabled")
109+
require.NotContains(t, DisabledGates(), "Foo")
99110
// refresh disabled gates
100-
require.Contains(t, DisabledGates(true), "Foo", "DisabledGates(true) should refresh the disabled gates")
111+
resetDisabledGates()
112+
require.Contains(t, DisabledGates(), "Foo", "DisabledGates(true) should refresh the disabled gates")
101113
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/singlestore-labs/codegate
22

3-
go 1.24.9
3+
go 1.23.0
44

55
require github.com/stretchr/testify v1.11.1
66

0 commit comments

Comments
 (0)