Skip to content

Commit 12e798f

Browse files
committed
Manager, with scoping
Signed-off-by: Danny Kopping <danny@coder.com>
1 parent 87e8d61 commit 12e798f

File tree

6 files changed

+160
-209
lines changed

6 files changed

+160
-209
lines changed

coderd/runtimeconfig/config.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ func (e *Entry[T]) StartupValue() T {
104104
}
105105

106106
// SetRuntimeValue attempts to update the runtime value of this field in the store via the given Mutator.
107-
func (e *Entry[T]) SetRuntimeValue(ctx context.Context, m Mutator, val T) error {
107+
func (e *Entry[T]) SetRuntimeValue(ctx context.Context, m Manager, val T) error {
108108
name, err := e.name()
109109
if err != nil {
110110
return err
@@ -114,7 +114,7 @@ func (e *Entry[T]) SetRuntimeValue(ctx context.Context, m Mutator, val T) error
114114
}
115115

116116
// UnsetRuntimeValue removes the runtime value from the store.
117-
func (e *Entry[T]) UnsetRuntimeValue(ctx context.Context, m Mutator) error {
117+
func (e *Entry[T]) UnsetRuntimeValue(ctx context.Context, m Manager) error {
118118
name, err := e.name()
119119
if err != nil {
120120
return err
@@ -124,7 +124,7 @@ func (e *Entry[T]) UnsetRuntimeValue(ctx context.Context, m Mutator) error {
124124
}
125125

126126
// Resolve attempts to resolve the runtime value of this field from the store via the given Resolver.
127-
func (e *Entry[T]) Resolve(ctx context.Context, r Resolver) (T, error) {
127+
func (e *Entry[T]) Resolve(ctx context.Context, r Manager) (T, error) {
128128
var zero T
129129

130130
name, err := e.name()
@@ -144,9 +144,9 @@ func (e *Entry[T]) Resolve(ctx context.Context, r Resolver) (T, error) {
144144
return inst, nil
145145
}
146146

147-
// Coalesce attempts to resolve the runtime value of this field from the store via the given Resolver. Should no runtime
147+
// Coalesce attempts to resolve the runtime value of this field from the store via the given Manager. Should no runtime
148148
// value be found, the startup value will be used.
149-
func (e *Entry[T]) Coalesce(ctx context.Context, r Resolver) (T, error) {
149+
func (e *Entry[T]) Coalesce(ctx context.Context, r Manager) (T, error) {
150150
var zero T
151151

152152
resolved, err := e.Resolve(ctx, r)

coderd/runtimeconfig/config_test.go

Lines changed: 68 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,14 @@ import (
44
"context"
55
"testing"
66

7-
"github.com/coder/serpent"
7+
"github.com/google/uuid"
88
"github.com/stretchr/testify/require"
99

10-
"github.com/coder/coder/v2/coderd/coderdtest"
10+
"github.com/coder/serpent"
11+
1112
"github.com/coder/coder/v2/coderd/database/dbmem"
1213
"github.com/coder/coder/v2/coderd/runtimeconfig"
1314
"github.com/coder/coder/v2/coderd/util/ptr"
14-
"github.com/coder/coder/v2/codersdk"
15-
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
16-
"github.com/coder/coder/v2/enterprise/coderd/license"
1715
"github.com/coder/coder/v2/testutil"
1816
)
1917

@@ -38,12 +36,8 @@ func TestUsage(t *testing.T) {
3836
t.Run("deployment value with runtimeconfig", func(t *testing.T) {
3937
t.Parallel()
4038

41-
_, altOrg := setup(t)
42-
4339
ctx := testutil.Context(t, testutil.WaitShort)
44-
store := dbmem.New()
45-
resolver := runtimeconfig.NewOrgResolver(altOrg.ID, runtimeconfig.NewStoreResolver(store))
46-
mutator := runtimeconfig.NewOrgMutator(altOrg.ID, runtimeconfig.NewStoreMutator(store))
40+
mgr := runtimeconfig.NewStoreManager(dbmem.New())
4741

4842
// NOTE: this field is now wrapped
4943
var field runtimeconfig.Entry[*serpent.HostPort]
@@ -63,32 +57,30 @@ func TestUsage(t *testing.T) {
6357

6458
// One new constraint is that we have to set the name on the runtimeconfig.Entry.
6559
// Attempting to perform any operation which accesses the store will enforce the need for a name.
66-
_, err := field.Resolve(ctx, resolver)
60+
_, err := field.Resolve(ctx, mgr)
6761
require.ErrorIs(t, err, runtimeconfig.ErrNameNotSet)
6862

6963
// Let's set that name; the environment var name is likely to be the most stable.
7064
field.Initialize(opt.Env)
7165

7266
newVal := serpent.HostPort{Host: "12.34.56.78", Port: "1234"}
7367
// Now that we've set it, we can update the runtime value of this field, which modifies given store.
74-
require.NoError(t, field.SetRuntimeValue(ctx, mutator, &newVal))
68+
require.NoError(t, field.SetRuntimeValue(ctx, mgr, &newVal))
7569

7670
// ...and we can retrieve the value, as well.
77-
resolved, err := field.Resolve(ctx, resolver)
71+
resolved, err := field.Resolve(ctx, mgr)
7872
require.NoError(t, err)
7973
require.Equal(t, newVal.String(), resolved.String())
8074

8175
// We can also remove the runtime config.
82-
require.NoError(t, field.UnsetRuntimeValue(ctx, mutator))
76+
require.NoError(t, field.UnsetRuntimeValue(ctx, mgr))
8377
})
8478
}
8579

8680
// TestConfig demonstrates creating org-level overrides for deployment-level settings.
8781
func TestConfig(t *testing.T) {
8882
t.Parallel()
8983

90-
_, altOrg := setup(t)
91-
9284
t.Run("new", func(t *testing.T) {
9385
t.Parallel()
9486

@@ -105,6 +97,8 @@ func TestConfig(t *testing.T) {
10597
t.Run("zero", func(t *testing.T) {
10698
t.Parallel()
10799

100+
mgr := runtimeconfig.NewNoopManager()
101+
108102
// A zero-value declaration of a runtimeconfig.Entry should behave as a zero value of the generic type.
109103
// NB! A name has not been set for this entry; it is "uninitialized".
110104
var field runtimeconfig.Entry[*serpent.Bool]
@@ -115,20 +109,18 @@ func TestConfig(t *testing.T) {
115109
require.NoError(t, field.SetStartupValue("true"))
116110

117111
// But attempting to resolve will produce an error.
118-
_, err := field.Resolve(context.Background(), runtimeconfig.NewNoopResolver())
112+
_, err := field.Resolve(context.Background(), mgr)
119113
require.ErrorIs(t, err, runtimeconfig.ErrNameNotSet)
120114
// But attempting to set the runtime value will produce an error.
121115
val := serpent.BoolOf(ptr.Ref(true))
122-
require.ErrorIs(t, field.SetRuntimeValue(context.Background(), runtimeconfig.NewNoopMutator(), val), runtimeconfig.ErrNameNotSet)
116+
require.ErrorIs(t, field.SetRuntimeValue(context.Background(), mgr, val), runtimeconfig.ErrNameNotSet)
123117
})
124118

125119
t.Run("simple", func(t *testing.T) {
126120
t.Parallel()
127121

128122
ctx := testutil.Context(t, testutil.WaitShort)
129-
store := dbmem.New()
130-
resolver := runtimeconfig.NewOrgResolver(altOrg.ID, runtimeconfig.NewStoreResolver(store))
131-
mutator := runtimeconfig.NewOrgMutator(altOrg.ID, runtimeconfig.NewStoreMutator(store))
123+
mgr := runtimeconfig.NewStoreManager(dbmem.New())
132124

133125
var (
134126
base = serpent.String("system@dev.coder.com")
@@ -141,16 +133,16 @@ func TestConfig(t *testing.T) {
141133
// Validate that it returns that value.
142134
require.Equal(t, base.String(), field.String())
143135
// Validate that there is no org-level override right now.
144-
_, err := field.Resolve(ctx, resolver)
136+
_, err := field.Resolve(ctx, mgr)
145137
require.ErrorIs(t, err, runtimeconfig.EntryNotFound)
146138
// Coalesce returns the deployment-wide value.
147-
val, err := field.Coalesce(ctx, resolver)
139+
val, err := field.Coalesce(ctx, mgr)
148140
require.NoError(t, err)
149141
require.Equal(t, base.String(), val.String())
150142
// Set an org-level override.
151-
require.NoError(t, field.SetRuntimeValue(ctx, mutator, &override))
143+
require.NoError(t, field.SetRuntimeValue(ctx, mgr, &override))
152144
// Coalesce now returns the org-level value.
153-
val, err = field.Coalesce(ctx, resolver)
145+
val, err = field.Coalesce(ctx, mgr)
154146
require.NoError(t, err)
155147
require.Equal(t, override.String(), val.String())
156148
})
@@ -159,9 +151,7 @@ func TestConfig(t *testing.T) {
159151
t.Parallel()
160152

161153
ctx := testutil.Context(t, testutil.WaitShort)
162-
store := dbmem.New()
163-
resolver := runtimeconfig.NewOrgResolver(altOrg.ID, runtimeconfig.NewStoreResolver(store))
164-
mutator := runtimeconfig.NewOrgMutator(altOrg.ID, runtimeconfig.NewStoreMutator(store))
154+
mgr := runtimeconfig.NewStoreManager(dbmem.New())
165155

166156
var (
167157
base = serpent.Struct[map[string]string]{
@@ -180,34 +170,65 @@ func TestConfig(t *testing.T) {
180170
// Check that default has been set.
181171
require.Equal(t, base.String(), field.StartupValue().String())
182172
// Validate that there is no org-level override right now.
183-
_, err := field.Resolve(ctx, resolver)
173+
_, err := field.Resolve(ctx, mgr)
184174
require.ErrorIs(t, err, runtimeconfig.EntryNotFound)
185175
// Coalesce returns the deployment-wide value.
186-
val, err := field.Coalesce(ctx, resolver)
176+
val, err := field.Coalesce(ctx, mgr)
187177
require.NoError(t, err)
188178
require.Equal(t, base.Value, val.Value)
189179
// Set an org-level override.
190-
require.NoError(t, field.SetRuntimeValue(ctx, mutator, &override))
180+
require.NoError(t, field.SetRuntimeValue(ctx, mgr, &override))
191181
// Coalesce now returns the org-level value.
192-
structVal, err := field.Resolve(ctx, resolver)
182+
structVal, err := field.Resolve(ctx, mgr)
193183
require.NoError(t, err)
194184
require.Equal(t, override.Value, structVal.Value)
195185
})
196186
}
197187

198-
// setup creates a new API, enabled notifications + multi-org experiments, and returns the API client and a new org.
199-
func setup(t *testing.T) (*codersdk.Client, codersdk.Organization) {
200-
t.Helper()
201-
202-
vals := coderdtest.DeploymentValues(t)
203-
vals.Experiments = []string{string(codersdk.ExperimentMultiOrganization)}
204-
adminClient, _, _, _ := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
205-
Options: &coderdtest.Options{DeploymentValues: vals},
206-
LicenseOptions: &coderdenttest.LicenseOptions{
207-
Features: license.Features{
208-
codersdk.FeatureMultipleOrganizations: 1,
209-
},
210-
},
211-
})
212-
return adminClient, coderdenttest.CreateOrganization(t, adminClient, coderdenttest.CreateOrganizationOptions{})
188+
func TestScoped(t *testing.T) {
189+
orgId := uuid.New()
190+
191+
ctx := testutil.Context(t, testutil.WaitShort)
192+
193+
// Set up a config manager and a field which will have runtime configs.
194+
mgr := runtimeconfig.NewStoreManager(dbmem.New())
195+
field := runtimeconfig.MustNew[*serpent.HostPort]("addr", "localhost:3000")
196+
197+
// No runtime value set at this point, Coalesce will return startup value.
198+
_, err := field.Resolve(ctx, mgr)
199+
require.ErrorIs(t, err, runtimeconfig.EntryNotFound)
200+
val, err := field.Coalesce(ctx, mgr)
201+
require.NoError(t, err)
202+
require.Equal(t, field.StartupValue().String(), val.String())
203+
204+
// Set a runtime value which is NOT org-scoped.
205+
host, port := "localhost", "1234"
206+
require.NoError(t, field.SetRuntimeValue(ctx, mgr, &serpent.HostPort{Host: host, Port: port}))
207+
val, err = field.Resolve(ctx, mgr)
208+
require.NoError(t, err)
209+
require.Equal(t, host, val.Host)
210+
require.Equal(t, port, val.Port)
211+
212+
orgMgr := mgr.Scoped(orgId.String())
213+
// Using the org scope, nothing will be returned.
214+
_, err = field.Resolve(ctx, orgMgr)
215+
require.ErrorIs(t, err, runtimeconfig.EntryNotFound)
216+
217+
// Now set an org-scoped value.
218+
host, port = "localhost", "4321"
219+
require.NoError(t, field.SetRuntimeValue(ctx, orgMgr, &serpent.HostPort{Host: host, Port: port}))
220+
val, err = field.Resolve(ctx, orgMgr)
221+
require.NoError(t, err)
222+
require.Equal(t, host, val.Host)
223+
require.Equal(t, port, val.Port)
224+
225+
// Ensure the two runtime configs are NOT equal to each other nor the startup value.
226+
global, err := field.Resolve(ctx, mgr)
227+
require.NoError(t, err)
228+
org, err := field.Resolve(ctx, orgMgr)
229+
require.NoError(t, err)
230+
231+
require.NotEqual(t, global.String(), org.String())
232+
require.NotEqual(t, field.StartupValue().String(), global.String())
233+
require.NotEqual(t, field.StartupValue().String(), org.String())
213234
}

coderd/runtimeconfig/manager.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package runtimeconfig
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"errors"
7+
"fmt"
8+
9+
"golang.org/x/xerrors"
10+
11+
"github.com/coder/coder/v2/coderd/database"
12+
)
13+
14+
type NoopManager struct{}
15+
16+
func NewNoopManager() *NoopManager {
17+
return &NoopManager{}
18+
}
19+
20+
func (n NoopManager) GetRuntimeSetting(context.Context, string) (string, error) {
21+
return "", EntryNotFound
22+
}
23+
24+
func (n NoopManager) UpsertRuntimeSetting(context.Context, string, string) error {
25+
return EntryNotFound
26+
}
27+
28+
func (n NoopManager) DeleteRuntimeSetting(context.Context, string) error {
29+
return EntryNotFound
30+
}
31+
32+
func (n NoopManager) Scoped(string) Manager {
33+
return n
34+
}
35+
36+
type StoreManager struct {
37+
Store
38+
39+
ns string
40+
}
41+
42+
func NewStoreManager(store Store) *StoreManager {
43+
if store == nil {
44+
panic("developer error: store must not be nil")
45+
}
46+
return &StoreManager{Store: store}
47+
}
48+
49+
func (m StoreManager) GetRuntimeSetting(ctx context.Context, key string) (string, error) {
50+
key = m.namespacedKey(key)
51+
val, err := m.Store.GetRuntimeConfig(ctx, key)
52+
if err != nil {
53+
if errors.Is(err, sql.ErrNoRows) {
54+
return "", xerrors.Errorf("%q: %w", key, EntryNotFound)
55+
}
56+
return "", xerrors.Errorf("fetch %q: %w", key, err)
57+
}
58+
59+
return val, nil
60+
}
61+
62+
func (m StoreManager) UpsertRuntimeSetting(ctx context.Context, key, val string) error {
63+
err := m.Store.UpsertRuntimeConfig(ctx, database.UpsertRuntimeConfigParams{Key: m.namespacedKey(key), Value: val})
64+
if err != nil {
65+
return xerrors.Errorf("update %q: %w", err)
66+
}
67+
return nil
68+
}
69+
70+
func (m StoreManager) DeleteRuntimeSetting(ctx context.Context, key string) error {
71+
return m.Store.DeleteRuntimeConfig(ctx, m.namespacedKey(key))
72+
}
73+
74+
func (m StoreManager) Scoped(ns string) Manager {
75+
return &StoreManager{Store: m.Store, ns: ns}
76+
}
77+
78+
func (m StoreManager) namespacedKey(k string) string {
79+
return fmt.Sprintf("%s:%s", m.ns, k)
80+
}

0 commit comments

Comments
 (0)