Skip to content

Commit 5c92688

Browse files
dannykoppingEmyrk
authored andcommitted
Implementation
Signed-off-by: Danny Kopping <danny@coder.com>
1 parent 8b1c46f commit 5c92688

File tree

7 files changed

+418
-0
lines changed

7 files changed

+418
-0
lines changed

coderd/runtimeconfig/config.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package runtimeconfig
2+
3+
import (
4+
"context"
5+
"errors"
6+
"reflect"
7+
8+
"github.com/spf13/pflag"
9+
"golang.org/x/xerrors"
10+
)
11+
12+
// TODO: comment
13+
type Value pflag.Value
14+
15+
type Entry[T Value] struct {
16+
val T
17+
key string
18+
}
19+
20+
func New[T Value](key, val string) (out Entry[T], err error) {
21+
out.Init(key)
22+
23+
if err = out.Set(val); err != nil {
24+
return out, err
25+
}
26+
27+
return out, nil
28+
}
29+
30+
func MustNew[T Value](key, val string) Entry[T] {
31+
out, err := New[T](key, val)
32+
if err != nil {
33+
panic(err)
34+
}
35+
return out
36+
}
37+
38+
func (o *Entry[T]) Init(key string) {
39+
o.val = create[T]()
40+
o.key = key
41+
}
42+
43+
func (o *Entry[T]) Set(s string) error {
44+
if reflect.ValueOf(o.val).IsNil() {
45+
return xerrors.Errorf("instance of %T is uninitialized", o.val)
46+
}
47+
return o.val.Set(s)
48+
}
49+
50+
func (o *Entry[T]) Type() string {
51+
return o.val.Type()
52+
}
53+
54+
func (o *Entry[T]) String() string {
55+
return o.val.String()
56+
}
57+
58+
func (o *Entry[T]) StartupValue() T {
59+
return o.val
60+
}
61+
62+
func (o *Entry[T]) Resolve(ctx context.Context, r Resolver) (T, error) {
63+
return o.resolve(ctx, r)
64+
}
65+
66+
func (o *Entry[T]) resolve(ctx context.Context, r Resolver) (T, error) {
67+
var zero T
68+
69+
val, err := r.ResolveByKey(ctx, o.key)
70+
if err != nil {
71+
return zero, err
72+
}
73+
74+
inst := create[T]()
75+
if err = inst.Set(val); err != nil {
76+
return zero, xerrors.Errorf("instantiate new %T: %w", inst, err)
77+
}
78+
return inst, nil
79+
}
80+
81+
func (o *Entry[T]) Save(ctx context.Context, m Mutator, val T) error {
82+
return m.MutateByKey(ctx, o.key, val.String())
83+
}
84+
85+
func (o *Entry[T]) Coalesce(ctx context.Context, r Resolver) (T, error) {
86+
var zero T
87+
88+
resolved, err := o.resolve(ctx, r)
89+
if err != nil {
90+
if errors.Is(err, EntryNotFound) {
91+
return o.StartupValue(), nil
92+
}
93+
return zero, err
94+
}
95+
96+
return resolved, nil
97+
}

coderd/runtimeconfig/config_test.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package runtimeconfig_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/coder/serpent"
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/coder/coder/v2/coderd/coderdtest"
10+
"github.com/coder/coder/v2/coderd/runtimeconfig"
11+
"github.com/coder/coder/v2/codersdk"
12+
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
13+
"github.com/coder/coder/v2/enterprise/coderd/license"
14+
"github.com/coder/coder/v2/testutil"
15+
)
16+
17+
// TestConfig demonstrates creating org-level overrides for deployment-level settings.
18+
func TestConfig(t *testing.T) {
19+
t.Parallel()
20+
21+
vals := coderdtest.DeploymentValues(t)
22+
vals.Experiments = []string{string(codersdk.ExperimentMultiOrganization)}
23+
adminClient, _, _, _ := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
24+
Options: &coderdtest.Options{DeploymentValues: vals},
25+
LicenseOptions: &coderdenttest.LicenseOptions{
26+
Features: license.Features{
27+
codersdk.FeatureMultipleOrganizations: 1,
28+
},
29+
},
30+
})
31+
altOrg := coderdenttest.CreateOrganization(t, adminClient, coderdenttest.CreateOrganizationOptions{})
32+
33+
t.Run("panics unless initialized", func(t *testing.T) {
34+
t.Parallel()
35+
36+
field := runtimeconfig.Entry[*serpent.String]{}
37+
require.Panics(t, func() {
38+
field.StartupValue().String()
39+
})
40+
41+
field.Init("my-field")
42+
require.NotPanics(t, func() {
43+
field.StartupValue().String()
44+
})
45+
})
46+
47+
t.Run("simple", func(t *testing.T) {
48+
t.Parallel()
49+
50+
ctx := testutil.Context(t, testutil.WaitShort)
51+
store := runtimeconfig.NewInMemoryStore()
52+
resolver := runtimeconfig.NewOrgResolver(altOrg.ID, runtimeconfig.NewStoreResolver(store))
53+
mutator := runtimeconfig.NewOrgMutator(altOrg.ID, runtimeconfig.NewStoreMutator(store))
54+
55+
var (
56+
base = serpent.String("system@dev.coder.com")
57+
override = serpent.String("dogfood@dev.coder.com")
58+
)
59+
60+
field := runtimeconfig.Entry[*serpent.String]{}
61+
field.Init("my-field")
62+
// Check that no default has been set.
63+
require.Empty(t, field.StartupValue().String())
64+
// Initialize the value.
65+
require.NoError(t, field.Set(base.String()))
66+
// Validate that it returns that value.
67+
require.Equal(t, base.String(), field.String())
68+
// Validate that there is no org-level override right now.
69+
_, err := field.Resolve(ctx, resolver)
70+
require.ErrorIs(t, err, runtimeconfig.EntryNotFound)
71+
// Coalesce returns the deployment-wide value.
72+
val, err := field.Coalesce(ctx, resolver)
73+
require.NoError(t, err)
74+
require.Equal(t, base.String(), val.String())
75+
// Set an org-level override.
76+
require.NoError(t, field.Save(ctx, mutator, &override))
77+
// Coalesce now returns the org-level value.
78+
val, err = field.Coalesce(ctx, resolver)
79+
require.NoError(t, err)
80+
require.Equal(t, override.String(), val.String())
81+
})
82+
83+
t.Run("complex", func(t *testing.T) {
84+
t.Parallel()
85+
86+
ctx := testutil.Context(t, testutil.WaitShort)
87+
store := runtimeconfig.NewInMemoryStore()
88+
resolver := runtimeconfig.NewOrgResolver(altOrg.ID, runtimeconfig.NewStoreResolver(store))
89+
mutator := runtimeconfig.NewOrgMutator(altOrg.ID, runtimeconfig.NewStoreMutator(store))
90+
91+
field := runtimeconfig.Entry[*serpent.Struct[map[string]string]]{}
92+
field.Init("my-field")
93+
var (
94+
base = serpent.Struct[map[string]string]{
95+
Value: map[string]string{"access_type": "offline"},
96+
}
97+
override = serpent.Struct[map[string]string]{
98+
Value: map[string]string{
99+
"a": "b",
100+
"c": "d",
101+
},
102+
}
103+
)
104+
105+
// Check that no default has been set.
106+
require.Empty(t, field.StartupValue().Value)
107+
// Initialize the value.
108+
require.NoError(t, field.Set(base.String()))
109+
// Validate that there is no org-level override right now.
110+
_, err := field.Resolve(ctx, resolver)
111+
require.ErrorIs(t, err, runtimeconfig.EntryNotFound)
112+
// Coalesce returns the deployment-wide value.
113+
val, err := field.Coalesce(ctx, resolver)
114+
require.NoError(t, err)
115+
require.Equal(t, base.Value, val.Value)
116+
// Set an org-level override.
117+
require.NoError(t, field.Save(ctx, mutator, &override))
118+
// Coalesce now returns the org-level value.
119+
structVal, err := field.Resolve(ctx, resolver)
120+
require.NoError(t, err)
121+
require.Equal(t, override.Value, structVal.Value)
122+
})
123+
}

coderd/runtimeconfig/mutator.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package runtimeconfig
2+
3+
import (
4+
"context"
5+
6+
"github.com/google/uuid"
7+
"golang.org/x/xerrors"
8+
)
9+
10+
type StoreMutator struct {
11+
store Store
12+
}
13+
14+
func NewStoreMutator(store Store) *StoreMutator {
15+
if store == nil {
16+
panic("developer error: store is nil")
17+
}
18+
return &StoreMutator{store}
19+
}
20+
21+
func (s *StoreMutator) MutateByKey(ctx context.Context, key, val string) error {
22+
err := s.store.UpsertRuntimeSetting(ctx, key, val)
23+
if err != nil {
24+
return xerrors.Errorf("update %q: %w", err)
25+
}
26+
return nil
27+
}
28+
29+
type OrgMutator struct {
30+
inner Mutator
31+
orgID uuid.UUID
32+
}
33+
34+
func NewOrgMutator(orgID uuid.UUID, inner Mutator) *OrgMutator {
35+
return &OrgMutator{inner: inner, orgID: orgID}
36+
}
37+
38+
func (m OrgMutator) MutateByKey(ctx context.Context, key, val string) error {
39+
return m.inner.MutateByKey(ctx, orgKey(m.orgID, key), val)
40+
}

coderd/runtimeconfig/resolver.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package runtimeconfig
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"errors"
7+
8+
"github.com/google/uuid"
9+
"golang.org/x/xerrors"
10+
)
11+
12+
type StoreResolver struct {
13+
store Store
14+
}
15+
16+
func NewStoreResolver(store Store) *StoreResolver {
17+
return &StoreResolver{store}
18+
}
19+
20+
func (s StoreResolver) ResolveByKey(ctx context.Context, key string) (string, error) {
21+
if s.store == nil {
22+
panic("developer error: store must be set")
23+
}
24+
25+
val, err := s.store.GetRuntimeSetting(ctx, key)
26+
if err != nil {
27+
if errors.Is(err, sql.ErrNoRows) {
28+
return "", xerrors.Errorf("%q: %w", key, EntryNotFound)
29+
}
30+
return "", xerrors.Errorf("fetch %q: %w", key, err)
31+
}
32+
33+
return val, nil
34+
}
35+
36+
type OrgResolver struct {
37+
inner Resolver
38+
orgID uuid.UUID
39+
}
40+
41+
func NewOrgResolver(orgID uuid.UUID, inner Resolver) *OrgResolver {
42+
if inner == nil {
43+
panic("developer error: resolver is nil")
44+
}
45+
46+
return &OrgResolver{inner: inner, orgID: orgID}
47+
}
48+
49+
func (r OrgResolver) ResolveByKey(ctx context.Context, key string) (string, error) {
50+
return r.inner.ResolveByKey(ctx, orgKey(r.orgID, key))
51+
}
52+
53+
// NoopResolver will always fail to resolve the given key.
54+
// Useful in tests where you just want to look up the startup value of configs, and are not concerned with runtime config.
55+
type NoopResolver struct {}
56+
57+
func NewNoopResolver() *NoopResolver {
58+
return &NoopResolver{}
59+
}
60+
61+
func (n NoopResolver) ResolveByKey(context.Context, string) (string, error) {
62+
return "", EntryNotFound
63+
}

coderd/runtimeconfig/spec.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package runtimeconfig
2+
3+
import "context"
4+
5+
type Initializer interface {
6+
Init(key string)
7+
}
8+
9+
// type RuntimeConfigResolver[T Value] interface {
10+
// StartupValue() T
11+
// Resolve(r Resolver) (T, error)
12+
// Coalesce(r Resolver) (T, error)
13+
// }
14+
//
15+
// type RuntimeConfigMutator[T Value] interface {
16+
// Save(m Mutator, val T) error
17+
// }
18+
19+
type Resolver interface {
20+
ResolveByKey(ctx context.Context, key string) (string, error)
21+
}
22+
23+
type Mutator interface {
24+
MutateByKey(ctx context.Context, key, val string) error
25+
}

0 commit comments

Comments
 (0)