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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy