Skip to content

Commit 13ea99a

Browse files
committed
chore: comments, cleanup, error fixes
1 parent 7452a23 commit 13ea99a

File tree

10 files changed

+196
-120
lines changed

10 files changed

+196
-120
lines changed

coderd/database/dbmem/dbmem.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import (
2222
"golang.org/x/xerrors"
2323

2424
"github.com/coder/coder/v2/coderd/notifications/types"
25-
"github.com/coder/coder/v2/coderd/runtimeconfig"
2625

2726
"github.com/coder/coder/v2/coderd/database"
2827
"github.com/coder/coder/v2/coderd/database/dbtime"
@@ -3522,7 +3521,7 @@ func (q *FakeQuerier) GetRuntimeConfig(_ context.Context, key string) (string, e
35223521

35233522
val, ok := q.runtimeConfig[key]
35243523
if !ok {
3525-
return "", runtimeconfig.EntryNotFound
3524+
return "", sql.ErrNoRows
35263525
}
35273526

35283527
return val, nil

coderd/runtimeconfig/deploymententry.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type SerpentEntry interface {
2121

2222
// DeploymentEntry extends a runtime entry with a startup value.
2323
// This allows for a single entry to source its value from startup or runtime.
24+
// DeploymentEntry will never return ErrEntryNotFound, as it will always return a value.
2425
type DeploymentEntry[T SerpentEntry] struct {
2526
RuntimeEntry[T]
2627
startupValue T
@@ -52,7 +53,7 @@ func (e *DeploymentEntry[T]) Coalesce(ctx context.Context, r Resolver) (T, error
5253

5354
resolved, err := e.Resolve(ctx, r)
5455
if err != nil {
55-
if errors.Is(err, EntryNotFound) {
56+
if errors.Is(err, ErrEntryNotFound) {
5657
return e.StartupValue(), nil
5758
}
5859
return zero, err
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package runtimeconfig_test
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"testing"
7+
8+
"github.com/google/uuid"
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/coder/coder/v2/coderd/database/dbmem"
12+
"github.com/coder/coder/v2/coderd/database/dbtestutil"
13+
"github.com/coder/coder/v2/coderd/runtimeconfig"
14+
"github.com/coder/coder/v2/coderd/util/ptr"
15+
"github.com/coder/coder/v2/codersdk"
16+
"github.com/coder/coder/v2/testutil"
17+
"github.com/coder/serpent"
18+
)
19+
20+
func ExampleDeploymentValues() {
21+
ctx := context.Background()
22+
db := dbmem.New()
23+
st := runtimeconfig.NewStoreManager()
24+
25+
// Define the field, this will usually live on Deployment Values.
26+
var stringField runtimeconfig.DeploymentEntry[*serpent.String]
27+
// All fields need to be initialized with their "key". This will be used
28+
// to uniquely identify the field in the store.
29+
stringField.Initialize("string-field")
30+
31+
// The startup value configured by the deployment env vars
32+
// This acts as a default value if no runtime value is set.
33+
// Can be used to support migrating a value from startup to runtime.
34+
_ = stringField.SetStartupValue("default")
35+
36+
// Runtime values take priority over startup values.
37+
_ = stringField.SetRuntimeValue(ctx, st.Resolver(db), serpent.StringOf(ptr.Ref("hello world")))
38+
39+
// Resolve the value of the field.
40+
val, err := stringField.Resolve(ctx, st.Resolver(db))
41+
if err != nil {
42+
panic(err)
43+
}
44+
fmt.Println(val)
45+
// Output: hello world
46+
}
47+
48+
// TestSerpentDeploymentEntry uses the package as the serpent options will use it.
49+
// Some of the usage might feel awkward, since the serpent package values come from
50+
// the serpent parsing (strings), not manual assignment.
51+
func TestSerpentDeploymentEntry(t *testing.T) {
52+
t.Parallel()
53+
54+
ctx := testutil.Context(t, testutil.WaitMedium)
55+
db, _ := dbtestutil.NewDB(t)
56+
st := runtimeconfig.NewStoreManager()
57+
58+
// TestEntries is how entries are defined in deployment values.
59+
type TestEntries struct {
60+
String runtimeconfig.DeploymentEntry[*serpent.String]
61+
Bool runtimeconfig.DeploymentEntry[*serpent.Bool]
62+
// codersdk.Feature is arbitrary, just using an actual struct to test.
63+
Struct runtimeconfig.DeploymentEntry[*serpent.Struct[codersdk.Feature]]
64+
}
65+
66+
var entries TestEntries
67+
// Init fields
68+
entries.String.Initialize("string-field")
69+
entries.Bool.Initialize("bool-field")
70+
entries.Struct.Initialize("struct-field")
71+
72+
// When using Coalesce, the default value is the empty value
73+
stringVal, err := entries.String.Coalesce(ctx, st.Resolver(db))
74+
require.NoError(t, err)
75+
require.Equal(t, "", stringVal.String())
76+
77+
// Set some defaults for some
78+
_ = entries.String.SetStartupValue("default")
79+
_ = entries.Struct.SetStartupValue((&serpent.Struct[codersdk.Feature]{
80+
Value: codersdk.Feature{
81+
Entitlement: codersdk.EntitlementEntitled,
82+
Enabled: false,
83+
Limit: ptr.Ref(int64(100)),
84+
Actual: nil,
85+
},
86+
}).String())
87+
88+
// Retrieve startup values
89+
stringVal, err = entries.String.Coalesce(ctx, st.Resolver(db))
90+
require.NoError(t, err)
91+
require.Equal(t, "default", stringVal.String())
92+
93+
structVal, err := entries.Struct.Coalesce(ctx, st.Resolver(db))
94+
require.NoError(t, err)
95+
require.Equal(t, structVal.Value.Entitlement, codersdk.EntitlementEntitled)
96+
require.Equal(t, structVal.Value.Limit, ptr.Ref(int64(100)))
97+
98+
// Override some defaults
99+
err = entries.String.SetRuntimeValue(ctx, st.Resolver(db), serpent.StringOf(ptr.Ref("hello world")))
100+
require.NoError(t, err)
101+
102+
err = entries.Struct.SetRuntimeValue(ctx, st.Resolver(db), &serpent.Struct[codersdk.Feature]{
103+
Value: codersdk.Feature{
104+
Entitlement: codersdk.EntitlementGracePeriod,
105+
},
106+
})
107+
require.NoError(t, err)
108+
109+
// Retrieve runtime values
110+
stringVal, err = entries.String.Coalesce(ctx, st.Resolver(db))
111+
require.NoError(t, err)
112+
require.Equal(t, "hello world", stringVal.String())
113+
114+
structVal, err = entries.Struct.Coalesce(ctx, st.Resolver(db))
115+
require.NoError(t, err)
116+
require.Equal(t, structVal.Value.Entitlement, codersdk.EntitlementGracePeriod)
117+
118+
// Test using org scoped resolver
119+
orgID := uuid.New()
120+
orgResolver := st.OrganizationResolver(db, orgID)
121+
// No org runtime set
122+
stringVal, err = entries.String.Coalesce(ctx, orgResolver)
123+
require.NoError(t, err)
124+
require.Equal(t, "default", stringVal.String())
125+
// Update org runtime
126+
err = entries.String.SetRuntimeValue(ctx, orgResolver, serpent.StringOf(ptr.Ref("hello organizations")))
127+
require.NoError(t, err)
128+
// Verify org runtime
129+
stringVal, err = entries.String.Coalesce(ctx, orgResolver)
130+
require.NoError(t, err)
131+
require.Equal(t, "hello organizations", stringVal.String())
132+
}

coderd/runtimeconfig/doc.go

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Package runtimeconfig contains logic for managing runtime configuration values
2+
// stored in the database. Each coderd should have a Manager singleton instance
3+
// that can create a Resolver for runtime configuration CRUD.
4+
//
5+
// TODO: Implement a caching layer for the Resolver so that we don't hit the
6+
// database on every request. Configuration values are not expected to change
7+
// frequently, so we should use pubsub to notify for updates.
8+
// When implemented, the runtimeconfig will essentially be an in memory lookup
9+
// with a database for persistence.
10+
package runtimeconfig

coderd/runtimeconfig/entry.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import (
77
"golang.org/x/xerrors"
88
)
99

10-
var ErrNameNotSet = xerrors.New("name is not set")
11-
10+
// EntryMarshaller requires all entries to marshal to and from a string.
11+
// The final store value is a database `text` column.
12+
// This also is compatible with serpent values.
1213
type EntryMarshaller interface {
1314
fmt.Stringer
1415
}

coderd/runtimeconfig/entry_test.go

+10-10
Original file line numberDiff line numberDiff line change
@@ -79,16 +79,16 @@ func TestEntry(t *testing.T) {
7979
// Validate that it returns that value.
8080
require.Equal(t, base.String(), field.String())
8181
// Validate that there is no org-level override right now.
82-
_, err := field.Resolve(ctx, mgr.DeploymentResolver(db))
83-
require.ErrorIs(t, err, runtimeconfig.EntryNotFound)
82+
_, err := field.Resolve(ctx, mgr.Resolver(db))
83+
require.ErrorIs(t, err, runtimeconfig.ErrEntryNotFound)
8484
// Coalesce returns the deployment-wide value.
85-
val, err := field.Coalesce(ctx, mgr.DeploymentResolver(db))
85+
val, err := field.Coalesce(ctx, mgr.Resolver(db))
8686
require.NoError(t, err)
8787
require.Equal(t, base.String(), val.String())
8888
// Set an org-level override.
89-
require.NoError(t, field.SetRuntimeValue(ctx, mgr.DeploymentResolver(db), &override))
89+
require.NoError(t, field.SetRuntimeValue(ctx, mgr.Resolver(db), &override))
9090
// Coalesce now returns the org-level value.
91-
val, err = field.Coalesce(ctx, mgr.DeploymentResolver(db))
91+
val, err = field.Coalesce(ctx, mgr.Resolver(db))
9292
require.NoError(t, err)
9393
require.Equal(t, override.String(), val.String())
9494
})
@@ -119,16 +119,16 @@ func TestEntry(t *testing.T) {
119119
// Check that default has been set.
120120
require.Equal(t, base.String(), field.StartupValue().String())
121121
// Validate that there is no org-level override right now.
122-
_, err := field.Resolve(ctx, mgr.DeploymentResolver(db))
123-
require.ErrorIs(t, err, runtimeconfig.EntryNotFound)
122+
_, err := field.Resolve(ctx, mgr.Resolver(db))
123+
require.ErrorIs(t, err, runtimeconfig.ErrEntryNotFound)
124124
// Coalesce returns the deployment-wide value.
125-
val, err := field.Coalesce(ctx, mgr.DeploymentResolver(db))
125+
val, err := field.Coalesce(ctx, mgr.Resolver(db))
126126
require.NoError(t, err)
127127
require.Equal(t, base.Value, val.Value)
128128
// Set an org-level override.
129-
require.NoError(t, field.SetRuntimeValue(ctx, mgr.DeploymentResolver(db), &override))
129+
require.NoError(t, field.SetRuntimeValue(ctx, mgr.Resolver(db), &override))
130130
// Coalesce now returns the org-level value.
131-
structVal, err := field.Resolve(ctx, mgr.DeploymentResolver(db))
131+
structVal, err := field.Resolve(ctx, mgr.Resolver(db))
132132
require.NoError(t, err)
133133
require.Equal(t, override.Value, structVal.Value)
134134
})

coderd/runtimeconfig/manager.go

+10-33
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,28 @@
11
package runtimeconfig
22

33
import (
4-
"time"
5-
64
"github.com/google/uuid"
7-
8-
"github.com/coder/coder/v2/coderd/util/syncmap"
95
)
106

11-
// StoreManager is the shared singleton that produces resolvers for runtime configuration.
7+
// StoreManager is the singleton that produces resolvers for runtime configuration.
8+
// TODO: Implement caching layer.
129
type StoreManager struct{}
1310

1411
func NewStoreManager() Manager {
1512
return &StoreManager{}
1613
}
1714

18-
func (*StoreManager) DeploymentResolver(db Store) Resolver {
15+
// Resolver is the deployment wide namespace for runtime configuration.
16+
// If you are trying to namespace a configuration, orgs for example, use
17+
// OrganizationResolver.
18+
func (*StoreManager) Resolver(db Store) Resolver {
1919
return NewStoreResolver(db)
2020
}
2121

22+
// OrganizationResolver will namespace all runtime configuration to the provided
23+
// organization ID. Configuration values stored with a given organization ID require
24+
// that the organization ID be provided to retrieve the value.
25+
// No values set here will ever be returned by the call to 'Resolver()'.
2226
func (*StoreManager) OrganizationResolver(db Store, orgID uuid.UUID) Resolver {
2327
return OrganizationResolver(orgID, NewStoreResolver(db))
2428
}
25-
26-
type cacheEntry struct {
27-
value string
28-
lastUpdated time.Time
29-
}
30-
31-
// MemoryCacheManager is an example of how a caching layer can be added to the
32-
// resolver from the manager.
33-
// TODO: Delete MemoryCacheManager and implement it properly in 'StoreManager'.
34-
// TODO: Handle pubsub-based cache invalidation.
35-
type MemoryCacheManager struct {
36-
cache *syncmap.Map[string, cacheEntry]
37-
}
38-
39-
func NewMemoryCacheManager() *MemoryCacheManager {
40-
return &MemoryCacheManager{
41-
cache: syncmap.New[string, cacheEntry](),
42-
}
43-
}
44-
45-
func (m *MemoryCacheManager) DeploymentResolver(db Store) Resolver {
46-
return NewMemoryCachedResolver(m.cache, NewStoreResolver(db))
47-
}
48-
49-
func (m *MemoryCacheManager) OrganizationResolver(db Store, orgID uuid.UUID) Resolver {
50-
return OrganizationResolver(orgID, NewMemoryCachedResolver(m.cache, NewStoreResolver(db)))
51-
}

0 commit comments

Comments
 (0)