Skip to content

feat: implement runtime configuration package with multi-org support #14624

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Document usage in test
Signed-off-by: Danny Kopping <danny@coder.com>
  • Loading branch information
dannykopping authored and Emyrk committed Sep 6, 2024
commit b2b43c8c56f37d11266d68ff2c025c77c0f493b7
20 changes: 18 additions & 2 deletions coderd/runtimeconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ func (e *Entry[T]) SetKey(k string) {
e.k = k
}

func (e *Entry[T]) Set(s string) error {
return e.SetStartupValue(s)
}

func (e *Entry[T]) SetStartupValue(s string) error {
return e.val().Set(s)
}
Expand All @@ -75,6 +79,9 @@ func (e *Entry[T]) String() string {
return e.val().String()
}

// StartupValue returns the wrapped type T which represents the state as of the definition of this Entry.
// This function would've been named Value, but this conflicts with a field named Value on some implementations of T in
// the serpent library; plus it's just more clear.
func (e *Entry[T]) StartupValue() T {
return e.val()
}
Expand All @@ -85,7 +92,16 @@ func (e *Entry[T]) SetRuntimeValue(ctx context.Context, m Mutator, val T) error
return err
}

return m.MutateByKey(ctx, key, val.String())
return m.UpsertRuntimeSetting(ctx, key, val.String())
}

func (e *Entry[T]) UnsetRuntimeValue(ctx context.Context, m Mutator) error {
key, err := e.key()
if err != nil {
return err
}

return m.DeleteRuntimeSetting(ctx, key)
}

func (e *Entry[T]) Resolve(ctx context.Context, r Resolver) (T, error) {
Expand All @@ -96,7 +112,7 @@ func (e *Entry[T]) Resolve(ctx context.Context, r Resolver) (T, error) {
return zero, err
}

val, err := r.ResolveByKey(ctx, key)
val, err := r.GetRuntimeSetting(ctx, key)
if err != nil {
return zero, err
}
Expand Down
97 changes: 85 additions & 12 deletions coderd/runtimeconfig/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,77 @@ import (
"github.com/coder/coder/v2/testutil"
)

func TestUsage(t *testing.T) {
t.Run("deployment value without runtimeconfig", func(t *testing.T) {
t.Parallel()

var field serpent.StringArray
opt := serpent.Option{
Name: "my deployment value",
Description: "this mimicks an option we'd define in codersdk/deployment.go",
Env: "MY_DEPLOYMENT_VALUE",
Default: "pestle,mortar",
Value: &field,
}

set := serpent.OptionSet{opt}
require.NoError(t, set.SetDefaults())
require.Equal(t, []string{"pestle", "mortar"}, field.Value())
})

t.Run("deployment value with runtimeconfig", func(t *testing.T) {
t.Parallel()

_, altOrg := setup(t)

ctx := testutil.Context(t, testutil.WaitShort)
store := dbmem.New()
resolver := runtimeconfig.NewOrgResolver(altOrg.ID, runtimeconfig.NewStoreResolver(store))
mutator := runtimeconfig.NewOrgMutator(altOrg.ID, runtimeconfig.NewStoreMutator(store))

// NOTE: this field is now wrapped
var field runtimeconfig.Entry[*serpent.HostPort]
opt := serpent.Option{
Name: "my deployment value",
Description: "this mimicks an option we'd define in codersdk/deployment.go",
Env: "MY_DEPLOYMENT_VALUE",
Default: "localhost:1234",
Value: &field,
}

set := serpent.OptionSet{opt}
require.NoError(t, set.SetDefaults())

// The value has to now be retrieved from a StartupValue() call.
require.Equal(t, "localhost:1234", field.StartupValue().String())

// One new constraint is that we have to set the key on the runtimeconfig.Entry.
// Attempting to perform any operation which accesses the store will enforce the need for a key.
_, err := field.Resolve(ctx, resolver)
require.ErrorIs(t, err, runtimeconfig.ErrKeyNotSet)

// Let's see that key. The environment var name is likely to be the most stable.
field.SetKey(opt.Env)

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

// ...and we can retrieve the value, as well.
resolved, err := field.Resolve(ctx, resolver)
require.NoError(t, err)
require.Equal(t, newVal.String(), resolved.String())

// We can also remove the runtime config.
require.NoError(t, field.UnsetRuntimeValue(ctx, mutator))
})
}

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

vals := coderdtest.DeploymentValues(t)
vals.Experiments = []string{string(codersdk.ExperimentMultiOrganization)}
adminClient, _, _, _ := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
Options: &coderdtest.Options{DeploymentValues: vals},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureMultipleOrganizations: 1,
},
},
})
altOrg := coderdenttest.CreateOrganization(t, adminClient, coderdenttest.CreateOrganizationOptions{})
_, altOrg := setup(t)

t.Run("new", func(t *testing.T) {
t.Parallel()
Expand Down Expand Up @@ -119,7 +175,7 @@ func TestConfig(t *testing.T) {
}
)

field := runtimeconfig.MustNew[*serpent.Struct[map[string]string]]("my-field", base.String())
field := runtimeconfig.MustNew[*serpent.Struct[map[string]string]]("my-field", base.String())

// Check that default has been set.
require.Equal(t, base.String(), field.StartupValue().String())
Expand All @@ -138,3 +194,20 @@ func TestConfig(t *testing.T) {
require.Equal(t, override.Value, structVal.Value)
})
}

// setup creates a new API, enabled notifications + multi-org experiments, and returns the API client and a new org.
func setup(t *testing.T) (*codersdk.Client, codersdk.Organization) {
t.Helper()

vals := coderdtest.DeploymentValues(t)
vals.Experiments = []string{string(codersdk.ExperimentMultiOrganization)}
adminClient, _, _, _ := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
Options: &coderdtest.Options{DeploymentValues: vals},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureMultipleOrganizations: 1,
},
},
})
return adminClient, coderdenttest.CreateOrganization(t, adminClient, coderdenttest.CreateOrganizationOptions{})
}
24 changes: 18 additions & 6 deletions coderd/runtimeconfig/mutator.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,18 @@ func NewStoreMutator(store Store) *StoreMutator {
return &StoreMutator{store}
}

func (s *StoreMutator) MutateByKey(ctx context.Context, key, val string) error {
func (s StoreMutator) UpsertRuntimeSetting(ctx context.Context, key, val string) error {
err := s.store.UpsertRuntimeConfig(ctx, database.UpsertRuntimeConfigParams{Key: key, Value: val})
if err != nil {
return xerrors.Errorf("update %q: %w", err)
}
return nil
}

func (s StoreMutator) DeleteRuntimeSetting(ctx context.Context, key string) error {
return s.store.DeleteRuntimeConfig(ctx, key)
}

type OrgMutator struct {
inner Mutator
orgID uuid.UUID
Expand All @@ -37,16 +41,24 @@ func NewOrgMutator(orgID uuid.UUID, inner Mutator) *OrgMutator {
return &OrgMutator{inner: inner, orgID: orgID}
}

func (m OrgMutator) MutateByKey(ctx context.Context, key, val string) error {
return m.inner.MutateByKey(ctx, orgKey(m.orgID, key), val)
func (m OrgMutator) UpsertRuntimeSetting(ctx context.Context, key, val string) error {
return m.inner.UpsertRuntimeSetting(ctx, orgKey(m.orgID, key), val)
}

func (m OrgMutator) DeleteRuntimeSetting(ctx context.Context, key string) error {
return m.inner.DeleteRuntimeSetting(ctx, key)
}

type NoopMutator struct{}

func (n *NoopMutator) MutateByKey(ctx context.Context, key, val string) error {
func NewNoopMutator() *NoopMutator {
return &NoopMutator{}
}

func (n NoopMutator) UpsertRuntimeSetting(context.Context, string, string) error {
return nil
}

func NewNoopMutator() *NoopMutator {
return &NoopMutator{}
func (n NoopMutator) DeleteRuntimeSetting(context.Context, string) error {
return nil
}
8 changes: 4 additions & 4 deletions coderd/runtimeconfig/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func NewStoreResolver(store Store) *StoreResolver {
return &StoreResolver{store}
}

func (s StoreResolver) ResolveByKey(ctx context.Context, key string) (string, error) {
func (s StoreResolver) GetRuntimeSetting(ctx context.Context, key string) (string, error) {
if s.store == nil {
panic("developer error: store must be set")
}
Expand Down Expand Up @@ -46,8 +46,8 @@ func NewOrgResolver(orgID uuid.UUID, inner Resolver) *OrgResolver {
return &OrgResolver{inner: inner, orgID: orgID}
}

func (r OrgResolver) ResolveByKey(ctx context.Context, key string) (string, error) {
return r.inner.ResolveByKey(ctx, orgKey(r.orgID, key))
func (r OrgResolver) GetRuntimeSetting(ctx context.Context, key string) (string, error) {
return r.inner.GetRuntimeSetting(ctx, orgKey(r.orgID, key))
}

// NoopResolver will always fail to resolve the given key.
Expand All @@ -58,6 +58,6 @@ func NewNoopResolver() *NoopResolver {
return &NoopResolver{}
}

func (n NoopResolver) ResolveByKey(context.Context, string) (string, error) {
func (n NoopResolver) GetRuntimeSetting(context.Context, string) (string, error) {
return "", EntryNotFound
}
7 changes: 4 additions & 3 deletions coderd/runtimeconfig/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ type Initializer interface {
}

type Resolver interface {
ResolveByKey(ctx context.Context, key string) (string, error)
GetRuntimeSetting(ctx context.Context, key string) (string, error)
}

type Mutator interface {
MutateByKey(ctx context.Context, key, val string) error
}
UpsertRuntimeSetting(ctx context.Context, key, val string) error
DeleteRuntimeSetting(ctx context.Context, key string) error
}