Skip to content

Commit 44fa8ad

Browse files
Relax configuration check when Discovery is enabled
Previously if Discovery was enabled, other features like bundle downloading and status reporting could not be configured manually. The reason for this was to prevent OPAs being deployed that could not be controlled through discovery. It's possible that the system serving the discovered config is unaware of all options locally available in OPA. Hence, we relax the configuration check when discovery is enabled so that the bootstrap configuration can contain plugin configurations. In case of conflicts, the bootstrap configuration for plugins wins. These local configuration overrides from the bootstrap configuration are included in the Status API messages so that management systems can get visibility into the local overrides. **In general, the bootstrap configuration overrides the discovered configuration.** Previously this was not the case for all configuration fields. For example, if the discovered configuration changes the `labels` section, only labels that are additional compared to the bootstrap configuration are used, all other changes are ignored. This implies labels in the bootstrap configuration override those in the discovered configuration. But for fields such as `default_decision`, `default_authorization_decision`, `nd_builtin_cache`, the discovered configuration would override the bootstrap configuration. Now the behavior is more consistent for the entire configuration and helps to avoid accidental configuration errors. Fixes: #5722 Signed-off-by: Ashutosh Narkar <[email protected]>
1 parent ef8532f commit 44fa8ad

File tree

7 files changed

+608
-29
lines changed

7 files changed

+608
-29
lines changed

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,24 @@ The `go` stanza of OPA's `go.mod` had become out of sync: Projects importing OPA
99
This was introduced with v0.63.0, but the main branch's `go.mod` still claimed to be a `go 1.20` compatible.
1010
Now, it states the true state of affairs: OPA, used as Go dependency, requires at least Go 1.21, and thus works with all officially supported Go versions (1.21.x and 1.22.x).
1111

12+
#### Breaking Change
13+
14+
##### Bootstrap configuration overrides Discovered configuration
15+
16+
Previously if Discovery was enabled, other features like bundle downloading and status reporting could not be configured manually.
17+
The reason for this was to prevent OPAs being deployed that could not be controlled through discovery. It's possible that
18+
the system serving the discovered config is unaware of all options locally available in OPA. Hence, we relax the configuration
19+
check when discovery is enabled so that the bootstrap configuration can contain plugin configurations. In case of conflicts,
20+
the bootstrap configuration for plugins wins. These local configuration overrides from the bootstrap configuration are included
21+
in the Status API messages so that management systems can get visibility into the local overrides.
22+
23+
**In general, the bootstrap configuration overrides the discovered configuration.** Previously this was not the case for all
24+
configuration fields. For example, if the discovered configuration changes the `labels` section, only labels that are
25+
additional compared to the bootstrap configuration are used, all other changes are ignored. This implies labels in the
26+
bootstrap configuration override those in the discovered configuration. But for fields such as `default_decision`, `default_authorization_decision`,
27+
`nd_builtin_cache`, the discovered configuration would override the bootstrap configuration. Now the behavior is more consistent
28+
for the entire configuration and helps to avoid accidental configuration errors.
29+
1230
## 0.63.0
1331

1432
This release contains a mix of features, performance improvements, and bugfixes.

docs/content/management-discovery.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ ways to structure the discovery bundle:
2424
> option.
2525
2626
If discovery is enabled, other features like bundle downloading and status
27-
reporting **cannot** be configured manually. Similarly, discovered configuration
28-
cannot override the original discovery settings in the configuration file that
29-
OPA was booted with.
27+
reporting **can** be configured manually. In case of conflicts, the bootstrap configuration
28+
for plugins would override the discovered configuration. **In general, the bootstrap configuration
29+
overrides the discovered configuration.**
3030

3131
See the [Configuration Reference](../configuration) for configuration details.
3232

plugins/discovery/discovery.go

Lines changed: 113 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
"github.com/open-policy-agent/opa/plugins/status"
3333
"github.com/open-policy-agent/opa/rego"
3434
"github.com/open-policy-agent/opa/storage/inmem"
35+
"github.com/open-policy-agent/opa/util"
3536
)
3637

3738
const (
@@ -49,19 +50,21 @@ const (
4950
// started it will periodically download a configuration bundle and try to
5051
// reconfigure the OPA.
5152
type Discovery struct {
52-
manager *plugins.Manager
53-
config *Config
54-
factories map[string]plugins.Factory
55-
downloader bundle.Loader // discovery bundle downloader
56-
status *bundle.Status // discovery status
57-
listenersMtx sync.Mutex // lock for listener map
58-
listeners map[interface{}]func(bundle.Status) // listeners for discovery update events
59-
etag string // discovery bundle etag for caching purposes
60-
metrics metrics.Metrics
61-
readyOnce sync.Once
62-
logger logging.Logger
63-
bundlePersistPath string
64-
hooks hooks.Hooks
53+
manager *plugins.Manager
54+
config *Config
55+
factories map[string]plugins.Factory
56+
downloader bundle.Loader // discovery bundle downloader
57+
status *bundle.Status // discovery status
58+
listenersMtx sync.Mutex // lock for listener map
59+
listeners map[interface{}]func(bundle.Status) // listeners for discovery update events
60+
etag string // discovery bundle etag for caching purposes
61+
metrics metrics.Metrics
62+
readyOnce sync.Once
63+
logger logging.Logger
64+
bundlePersistPath string
65+
hooks hooks.Hooks
66+
bootConfig map[string]interface{}
67+
overriddenConfigKeys []string
6568
}
6669

6770
// Factories provides a set of factory functions to use for
@@ -85,6 +88,12 @@ func Hooks(hs hooks.Hooks) func(*Discovery) {
8588
}
8689
}
8790

91+
func BootConfig(bootConfig map[string]interface{}) func(*Discovery) {
92+
return func(d *Discovery) {
93+
d.bootConfig = bootConfig
94+
}
95+
}
96+
8897
// New returns a new discovery plugin.
8998
func New(manager *plugins.Manager, opts ...func(*Discovery)) (*Discovery, error) {
9099
result := &Discovery{
@@ -107,10 +116,6 @@ func New(manager *plugins.Manager, opts ...func(*Discovery)) (*Discovery, error)
107116
return result, nil
108117
}
109118

110-
if names := manager.Config.PluginNames(); len(names) > 0 {
111-
return nil, fmt.Errorf("discovery prohibits manual configuration of %v", strings.Join(names, " and "))
112-
}
113-
114119
result.config = config
115120
restClient := manager.Client(config.service)
116121
if strings.ToLower(restClient.Config().Type) == "oci" {
@@ -353,6 +358,14 @@ func (c *Discovery) processUpdate(ctx context.Context, u download.Update) {
353358
c.status.SetError(nil)
354359
c.status.SetActivateSuccess(u.Bundle.Manifest.Revision)
355360

361+
// include the local overrides in the status update
362+
if len(c.overriddenConfigKeys) != 0 {
363+
msg := fmt.Sprintf("Keys in the discovered configuration overridden by boot configuration: %v", strings.Join(c.overriddenConfigKeys, ", "))
364+
c.logger.Debug(msg)
365+
c.status.Message = msg
366+
}
367+
c.overriddenConfigKeys = nil
368+
356369
// On the first activation success mark the plugin as being in OK state
357370
c.readyOnce.Do(func() {
358371
c.manager.UpdatePluginStatus(Name, &plugins.Status{State: plugins.StateOK})
@@ -394,6 +407,33 @@ func (c *Discovery) reconfigure(ctx context.Context, u download.Update) error {
394407
return nil
395408
}
396409

410+
func (c *Discovery) applyLocalPluginConfigOverride(conf *config.Config) (*config.Config, []string, error) {
411+
raw, err := json.Marshal(conf)
412+
if err != nil {
413+
return nil, nil, err
414+
}
415+
416+
var newConfig map[string]interface{}
417+
err = util.Unmarshal(raw, &newConfig)
418+
if err != nil {
419+
return nil, nil, err
420+
}
421+
422+
_, overriddenKeys := mergeValuesAndListOverrides(newConfig, c.bootConfig, "")
423+
424+
bs, err := json.Marshal(newConfig)
425+
if err != nil {
426+
return nil, nil, err
427+
}
428+
429+
parsedConf, err := config.ParseConfig(bs, c.manager.ID)
430+
if err != nil {
431+
return nil, nil, err
432+
}
433+
434+
return parsedConf, overriddenKeys, nil
435+
}
436+
397437
func (c *Discovery) processBundle(ctx context.Context, b *bundleApi.Bundle) (*pluginSet, error) {
398438

399439
config, err := evaluateBundle(ctx, c.manager.ID, c.manager.Info, b, c.config.query)
@@ -454,11 +494,23 @@ func (c *Discovery) processBundle(ctx context.Context, b *bundleApi.Bundle) (*pl
454494
}
455495
}
456496

457-
if err := c.manager.Reconfigure(config); err != nil {
497+
overriddenConfig, overriddenKeys, err := c.applyLocalPluginConfigOverride(config)
498+
if err != nil {
499+
return nil, err
500+
}
501+
502+
if err := c.manager.Reconfigure(overriddenConfig); err != nil {
458503
return nil, err
459504
}
460505

461-
return getPluginSet(c.factories, c.manager, config, c.metrics, c.config.Trigger)
506+
ps, err := getPluginSet(c.factories, c.manager, overriddenConfig, c.metrics, c.config.Trigger)
507+
if err != nil {
508+
return nil, err
509+
}
510+
511+
c.overriddenConfigKeys = overriddenKeys
512+
513+
return ps, nil
462514
}
463515

464516
// discoveryBundleDirName returns the name of the directory where the discovery bundle will be persisted.
@@ -678,3 +730,45 @@ func registerBundleStatusUpdates(m *plugins.Manager) {
678730
bp.RegisterBulkListener(pluginlistener(status.Name), sp.BulkUpdateBundleStatus)
679731
}
680732
}
733+
734+
// mergeValuesAndListOverrides will merge source and destination map, preferring values from the source map.
735+
// It will also return a list of keys in the destination map which were overridden by those in the source map
736+
func mergeValuesAndListOverrides(dest map[string]interface{}, src map[string]interface{}, prefix string) (map[string]interface{}, []string) {
737+
overriddenKeys := []string{}
738+
739+
for k, v := range src {
740+
// If the key doesn't exist already, then just set the key to that value
741+
if _, exists := dest[k]; !exists {
742+
dest[k] = v
743+
continue
744+
}
745+
746+
fullKey := k
747+
if prefix != "" {
748+
fullKey = fmt.Sprintf("%v.%v", prefix, k)
749+
}
750+
751+
nextMap, ok := v.(map[string]interface{})
752+
// If it isn't another map, overwrite the value
753+
if !ok {
754+
if dest[k] != v {
755+
overriddenKeys = append(overriddenKeys, fullKey)
756+
}
757+
dest[k] = v
758+
continue
759+
}
760+
// Edge case: If the key exists in the destination, but isn't a map
761+
destMap, isMap := dest[k].(map[string]interface{})
762+
// If the source map has a map for this key, prefer it
763+
if !isMap {
764+
dest[k] = v
765+
overriddenKeys = append(overriddenKeys, fullKey)
766+
continue
767+
}
768+
// If we got to this point, it is a map in both, so merge them
769+
merged, overridden := mergeValuesAndListOverrides(destMap, nextMap, fullKey)
770+
dest[k] = merged
771+
overriddenKeys = append(overriddenKeys, overridden...)
772+
}
773+
return dest, overriddenKeys
774+
}

0 commit comments

Comments
 (0)