diff --git a/CODEOWNERS b/CODEOWNERS index 05bf49d479c62..d1f8a2cbd7206 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -507,6 +507,7 @@ Makefile* @cilium/build /pkg/defaults @cilium/sig-agent /pkg/debug @cilium/sig-agent /pkg/dial @cilium/sig-agent +/pkg/driftchecker @cilium/sig-foundations /pkg/dynamicconfig @cilium/sig-foundations /pkg/ebpf @cilium/sig-datapath /pkg/egressgateway/ @cilium/egress-gateway diff --git a/Documentation/cmdref/cilium-agent.md b/Documentation/cmdref/cilium-agent.md index ea96e27183e25..9273bc1466a0c 100644 --- a/Documentation/cmdref/cilium-agent.md +++ b/Documentation/cmdref/cilium-agent.md @@ -112,6 +112,7 @@ cilium-agent [flags] --enable-cilium-endpoint-slice Enable the CiliumEndpointSlice watcher in place of the CiliumEndpoint watcher (beta) --enable-cilium-health-api-server-access strings List of cilium health API APIs which are administratively enabled. Supports '*'. (default [*]) --enable-custom-calls Enable tail call hooks for custom eBPF programs + --enable-drift-checker Enables support for config drift checker --enable-dynamic-config Enables support for dynamic agent config --enable-encryption-strict-mode Enable encryption strict mode --enable-endpoint-health-checking Enable connectivity health checking between virtual endpoints (default true) @@ -237,6 +238,7 @@ cilium-agent [flags] --identity-allocation-mode string Method to use for identity allocation (default "kvstore") --identity-change-grace-period duration Time to wait before using new identity on endpoint identity change (default 5s) --identity-restore-grace-period duration Time to wait before releasing unused restored CIDR identities during agent restart (default 30s) + --ignore-flags-drift-checker strings Ignores specified flags during drift checking --ingress-secrets-namespace string IngressSecretsNamespace is the namespace having tls secrets used by CEC, originating from Ingress controller --install-no-conntrack-iptables-rules Install Iptables rules to skip netfilter connection tracking on all pod traffic. This option is only effective when Cilium is running in direct routing and full KPR mode. Moreover, this option cannot be enabled when Cilium is running in a managed Kubernetes environment or in a chained CNI setup. --ip-masq-agent-config-path string ip-masq-agent configuration file path (default "/etc/config/ip-masq-agent") diff --git a/Documentation/cmdref/cilium-agent_hive.md b/Documentation/cmdref/cilium-agent_hive.md index eb8356c1d4464..a8809c5115681 100644 --- a/Documentation/cmdref/cilium-agent_hive.md +++ b/Documentation/cmdref/cilium-agent_hive.md @@ -36,6 +36,7 @@ cilium-agent hive [flags] --enable-bbr Enable BBR for the bandwidth manager --enable-cilium-api-server-access strings List of cilium API APIs which are administratively enabled. Supports '*'. (default [*]) --enable-cilium-health-api-server-access strings List of cilium health API APIs which are administratively enabled. Supports '*'. (default [*]) + --enable-drift-checker Enables support for config drift checker --enable-dynamic-config Enables support for dynamic agent config --enable-gateway-api Enables Envoy secret sync for Gateway API related TLS secrets --enable-ingress-controller Enables Envoy secret sync for Ingress controller related TLS secrets @@ -66,6 +67,7 @@ cilium-agent hive [flags] --http-request-timeout uint Time after which a forwarded HTTP request is considered failed unless completed (in seconds); Use 0 for unlimited (default 3600) --http-retry-count uint Number of retries performed after a forwarded request attempt fails (default 3) --http-retry-timeout uint Time after which a forwarded but uncompleted request is retried (connection failures are retried immediately); defaults to 0 (never) + --ignore-flags-drift-checker strings Ignores specified flags during drift checking --ingress-secrets-namespace string IngressSecretsNamespace is the namespace having tls secrets used by CEC, originating from Ingress controller --iptables-lock-timeout duration Time to pass to each iptables invocation to wait for xtables lock acquisition (default 5s) --iptables-random-fully Set iptables flag random-fully on masquerading rules diff --git a/Documentation/cmdref/cilium-agent_hive_dot-graph.md b/Documentation/cmdref/cilium-agent_hive_dot-graph.md index e04c4e2177ae2..b3779ac39cab4 100644 --- a/Documentation/cmdref/cilium-agent_hive_dot-graph.md +++ b/Documentation/cmdref/cilium-agent_hive_dot-graph.md @@ -42,6 +42,7 @@ cilium-agent hive dot-graph [flags] --enable-bbr Enable BBR for the bandwidth manager --enable-cilium-api-server-access strings List of cilium API APIs which are administratively enabled. Supports '*'. (default [*]) --enable-cilium-health-api-server-access strings List of cilium health API APIs which are administratively enabled. Supports '*'. (default [*]) + --enable-drift-checker Enables support for config drift checker --enable-dynamic-config Enables support for dynamic agent config --enable-gateway-api Enables Envoy secret sync for Gateway API related TLS secrets --enable-ingress-controller Enables Envoy secret sync for Ingress controller related TLS secrets @@ -71,6 +72,7 @@ cilium-agent hive dot-graph [flags] --http-request-timeout uint Time after which a forwarded HTTP request is considered failed unless completed (in seconds); Use 0 for unlimited (default 3600) --http-retry-count uint Number of retries performed after a forwarded request attempt fails (default 3) --http-retry-timeout uint Time after which a forwarded but uncompleted request is retried (connection failures are retried immediately); defaults to 0 (never) + --ignore-flags-drift-checker strings Ignores specified flags during drift checking --ingress-secrets-namespace string IngressSecretsNamespace is the namespace having tls secrets used by CEC, originating from Ingress controller --iptables-lock-timeout duration Time to pass to each iptables invocation to wait for xtables lock acquisition (default 5s) --iptables-random-fully Set iptables flag random-fully on masquerading rules diff --git a/daemon/cmd/cells.go b/daemon/cmd/cells.go index f2d01a38c3b0e..d7b97f0ad2ccd 100644 --- a/daemon/cmd/cells.go +++ b/daemon/cmd/cells.go @@ -28,6 +28,7 @@ import ( "github.com/cilium/cilium/pkg/datapath" "github.com/cilium/cilium/pkg/defaults" "github.com/cilium/cilium/pkg/dial" + "github.com/cilium/cilium/pkg/driftchecker" "github.com/cilium/cilium/pkg/dynamicconfig" "github.com/cilium/cilium/pkg/egressgateway" "github.com/cilium/cilium/pkg/endpoint" @@ -287,6 +288,9 @@ var ( // Provides a wrapper of the cilium config that can be watched dynamically dynamicconfig.Cell, + + // Allows agent to monitor the configuration drift and publish drift metric + driftchecker.Cell, ) ) diff --git a/pkg/driftchecker/cell.go b/pkg/driftchecker/cell.go new file mode 100644 index 0000000000000..4d2bb0e5cd440 --- /dev/null +++ b/pkg/driftchecker/cell.go @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package driftchecker + +import ( + "github.com/cilium/hive/cell" + "github.com/spf13/pflag" + + "github.com/cilium/cilium/pkg/metrics" +) + +// Cell will monitor the configuration drift from DynamicConfig table. +// It allows agent to monitor the configuration drift and publish +// `drift_checker_config_delta` metric reporting the diff delta. +var Cell = cell.Module( + "config-drift-checker", + "Monitor configuration cilium configuration drift from DynamicConfig table", + cell.Invoke(Register), + metrics.Metric(MetricsProvider), + cell.Config(defaultConfig), +) + +var defaultConfig = config{ + EnableDriftChecker: false, + IgnoreFlagsDriftChecker: []string{}, +} + +type config struct { + EnableDriftChecker bool + IgnoreFlagsDriftChecker []string +} + +func (c config) Flags(flags *pflag.FlagSet) { + flags.Bool("enable-drift-checker", c.EnableDriftChecker, "Enables support for config drift checker") + flags.StringSlice("ignore-flags-drift-checker", c.IgnoreFlagsDriftChecker, "Ignores specified flags during drift checking") +} diff --git a/pkg/driftchecker/checker.go b/pkg/driftchecker/checker.go new file mode 100644 index 0000000000000..2f2eb18c4c702 --- /dev/null +++ b/pkg/driftchecker/checker.go @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package driftchecker + +import ( + "context" + "fmt" + "log/slog" + "slices" + + "github.com/cilium/hive/cell" + "github.com/cilium/hive/job" + "github.com/cilium/statedb" + "github.com/spf13/cast" + "k8s.io/apimachinery/pkg/util/sets" + + "github.com/cilium/cilium/pkg/dynamicconfig" +) + +type checkerParams struct { + cell.In + + Lifecycle cell.Lifecycle + CellAllSettings cell.AllSettings + Health cell.Health + DB *statedb.DB + JobGroup job.Group + DynamicConfigTable statedb.Table[dynamicconfig.DynamicConfig] + Logger *slog.Logger + CheckerConfig config + DynamicConfigCellConfig dynamicconfig.Config + Metrics Metrics +} + +type checker struct { + cla cell.AllSettings + db *statedb.DB + dct statedb.Table[dynamicconfig.DynamicConfig] + l *slog.Logger + m Metrics + ignoredFlags sets.Set[string] +} + +func Register(params checkerParams) { + if !params.CheckerConfig.EnableDriftChecker || !params.DynamicConfigCellConfig.EnableDynamicConfig { + return + } + + c := checker{ + cla: params.CellAllSettings, + db: params.DB, + dct: params.DynamicConfigTable, + l: params.Logger, + m: params.Metrics, + ignoredFlags: sets.New[string](params.CheckerConfig.IgnoreFlagsDriftChecker...), + } + + params.JobGroup.Add(job.OneShot("drift-checker", func(ctx context.Context, health cell.Health) error { + return c.watchTableChanges(ctx) + })) +} + +func (c checker) watchTableChanges(ctx context.Context) error { + for { + tableKeys, channel := dynamicconfig.WatchAllKeys(c.db.ReadTxn(), c.dct) + // Wait for table initialization + if len(tableKeys) == 0 { + <-channel + continue + } + + deltas := c.computeDelta(tableKeys, c.cla) + c.publishMetrics(deltas) + + select { + case <-ctx.Done(): + return ctx.Err() + case <-channel: + continue + } + } +} + +func (c checker) computeDelta(desired map[string]dynamicconfig.DynamicConfig, actual cell.AllSettings) []string { + var deltas []string + + for key, value := range desired { + if c.ignoredFlags.Has(key) { + continue + } + + if actualValue, ok := actual[key]; ok { + actualValueString := cast.ToString(actualValue) + if value.Value != actualValueString { + deltas = append(deltas, fmt.Sprintf("Mismatch for key [%s]: expecting %q but got %q", key, value.Value, actualValueString)) + c.l.Warn("Mismatch found", "key", key, "actual", actualValueString, "expectedValue", value.Value, "expectedSource", value.Key.String()) + } + } else { + deltas = append(deltas, fmt.Sprintf("No entry found for key: [%s]", key)) + c.l.Warn("No local entry found", "key", key, "expectedValue", value.Value, "expectedSource", value.Key.String()) + } + } + slices.Sort(deltas) + return deltas +} + +func (c checker) publishMetrics(deltas []string) { + c.m.DriftCheckerConfigDelta.Set(float64(len(deltas))) +} diff --git a/pkg/driftchecker/checker_test.go b/pkg/driftchecker/checker_test.go new file mode 100644 index 0000000000000..51256c8ba1f91 --- /dev/null +++ b/pkg/driftchecker/checker_test.go @@ -0,0 +1,235 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package driftchecker + +import ( + "context" + "log/slog" + "reflect" + "testing" + "time" + + "github.com/cilium/hive/cell" + "github.com/cilium/hive/hivetest" + "github.com/cilium/hive/job" + "github.com/cilium/statedb" + prometheustestutil "github.com/prometheus/client_golang/prometheus/testutil" + "k8s.io/apimachinery/pkg/util/sets" + + "github.com/cilium/cilium/pkg/dynamicconfig" + "github.com/cilium/cilium/pkg/hive" + "github.com/cilium/cilium/pkg/hive/health/types" + k8sClient "github.com/cilium/cilium/pkg/k8s/client" + "github.com/cilium/cilium/pkg/metrics" + "github.com/cilium/cilium/pkg/testutils" +) + +func TestWatchTableChanges(t *testing.T) { + tests := []struct { + name string + cellSettings map[string]any + table map[string]string + expectedCount int + expectedValue float64 + }{ + { + name: "No mismatches", + cellSettings: map[string]any{"key": "value"}, + table: map[string]string{"key": "value"}, + expectedCount: 1, + expectedValue: 0, + }, + { + name: "Missing Key", + table: map[string]string{"key2": "value"}, + expectedCount: 1, + expectedValue: 1, + }, + { + name: "Value missmatch", + cellSettings: map[string]any{"key": "other_value"}, + table: map[string]string{"key": "value"}, + expectedCount: 1, + expectedValue: 1, + }, + { + name: "Key missmatch", + cellSettings: map[string]any{"key2": "value"}, + table: map[string]string{"key": "value"}, + expectedCount: 1, + expectedValue: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, db, s, m := fixture(t, tt.cellSettings) + for k, v := range tt.table { + upsertEntry(db, s, k, v) + } + + if err := testutils.WaitUntil(func() bool { + return prometheustestutil.CollectAndCount(m.DriftCheckerConfigDelta) > 0 + }, 5*time.Second); err != nil { + t.Errorf("expected DriftCheckerConfigDelta to be collected, but got error: %v", err) + } + + if err := testutils.WaitUntil(func() bool { + return prometheustestutil.ToFloat64(m.DriftCheckerConfigDelta) == tt.expectedValue + }, 5*time.Second); err != nil { + t.Errorf("expected DriftCheckerConfigDelta to be %f, but got error: %v", tt.expectedValue, err) + } + + }) + } + +} + +func TestComputeDelta(t *testing.T) { + tests := []struct { + name string + desired map[string]dynamicconfig.DynamicConfig + actual cell.AllSettings + ignored []string + expected []string + }{ + { + name: "No mismatches", + desired: map[string]dynamicconfig.DynamicConfig{ + "key1": newDynamicConfig("key1", "value1"), + "key2": newDynamicConfig("key2", "123"), + }, + actual: cell.AllSettings{ + "key1": "value1", + "key2": 123, + }, + ignored: []string{}, + expected: nil, + }, + { + name: "Mismatches present", + desired: map[string]dynamicconfig.DynamicConfig{ + "key1": newDynamicConfig("key1", "value1"), + "key2": newDynamicConfig("key2", "123"), + "key3": newDynamicConfig("key3", "true"), + }, + actual: cell.AllSettings{ + "key1": "value1", + "key2": 456, + }, + ignored: []string{}, + expected: []string{ + "Mismatch for key [key2]: expecting \"123\" but got \"456\"", + "No entry found for key: [key3]", + }, + }, + { + name: "Ignored flags", + desired: map[string]dynamicconfig.DynamicConfig{ + "key1": newDynamicConfig("key1", "value1"), + "key2": newDynamicConfig("key2", "123"), + }, + actual: cell.AllSettings{ + "key1": "different_value", + }, + ignored: []string{"key1"}, + expected: []string{"No entry found for key: [key2]"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := checker{ + l: slog.Default(), + ignoredFlags: sets.New[string](tt.ignored...), + } + result := c.computeDelta(tt.desired, tt.actual) + if !reflect.DeepEqual(tt.expected, result) { + t.Errorf("Expected %v, but got %v", tt.expected, result) + } + }) + } +} + +func newDynamicConfig(key string, value string) dynamicconfig.DynamicConfig { + return dynamicconfig.DynamicConfig{ + Key: dynamicconfig.Key{ + Name: key, + Source: "kube-system", + }, + Value: value, + Priority: 0, + } +} + +func upsertEntry(db *statedb.DB, table statedb.RWTable[dynamicconfig.DynamicConfig], k string, v string) { + txn := db.WriteTxn(table) + defer txn.Commit() + + entry := dynamicconfig.DynamicConfig{Key: dynamicconfig.Key{Name: k, Source: "kube-system"}, Value: v, Priority: 0} + _, _, _ = table.Insert(txn, entry) +} + +func fixture(t *testing.T, cellAllSettings map[string]any) (*hive.Hive, *statedb.DB, statedb.RWTable[dynamicconfig.DynamicConfig], Metrics) { + var ( + db *statedb.DB + table statedb.RWTable[dynamicconfig.DynamicConfig] + m Metrics + ) + + h := hive.New( + k8sClient.FakeClientCell, + metrics.Metric(MetricsProvider), + cell.Provide( + dynamicconfig.NewConfigTable, + func(table statedb.RWTable[dynamicconfig.DynamicConfig]) statedb.Table[dynamicconfig.DynamicConfig] { + return table + }, + func(lc cell.Lifecycle, p types.Provider, jr job.Registry) job.Group { + h := p.ForModule(cell.FullModuleID{"test"}) + jg := jr.NewGroup(h) + lc.Append(jg) + return jg + }, + func() config { + return config{ + EnableDriftChecker: true, + IgnoreFlagsDriftChecker: nil, + } + }, + func() dynamicconfig.Config { + return dynamicconfig.Config{ + EnableDynamicConfig: true, + ConfigSources: "", + ConfigSourcesOverrides: "", + } + }, + ), + cell.Invoke( + func(params checkerParams) { + params.CellAllSettings = cellAllSettings + Register(params) + }, + func(t statedb.RWTable[dynamicconfig.DynamicConfig], db_ *statedb.DB, c *k8sClient.FakeClientset, metrics_ Metrics) error { + table = t + db = db_ + m = metrics_ + return nil + }, + ), + ) + + ctx := context.Background() + tLog := hivetest.Logger(t) + if err := h.Start(tLog, ctx); err != nil { + t.Fatalf("starting hive encountered: %s", err) + } + t.Cleanup(func() { + if err := h.Stop(tLog, ctx); err != nil { + t.Fatalf("stopping hive encountered: %s", err) + } + }) + + return h, db, table, m +} diff --git a/pkg/driftchecker/metrics.go b/pkg/driftchecker/metrics.go new file mode 100644 index 0000000000000..6be7cc91b03e1 --- /dev/null +++ b/pkg/driftchecker/metrics.go @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package driftchecker + +import ( + "github.com/cilium/cilium/pkg/metrics" + "github.com/cilium/cilium/pkg/metrics/metric" +) + +type Metrics struct { + DriftCheckerConfigDelta metric.Gauge +} + +func MetricsProvider() Metrics { + return Metrics{ + DriftCheckerConfigDelta: metric.NewGauge(metric.GaugeOpts{ + Name: "drift_checker_config_delta", + Namespace: metrics.Namespace, + Help: "Total number of deltas found to mismatch between agent settings and remote sources.", + }), + } +} diff --git a/pkg/dynamicconfig/cell.go b/pkg/dynamicconfig/cell.go index e1a00692647bd..543d2b98949e5 100644 --- a/pkg/dynamicconfig/cell.go +++ b/pkg/dynamicconfig/cell.go @@ -44,19 +44,19 @@ var Cell = cell.Module( cell.Config(defaultConfig), ) -var defaultConfig = config{ +var defaultConfig = Config{ EnableDynamicConfig: false, ConfigSources: `[{"kind":"config-map","namespace":"kube-system","name":"cilium-config"}]`, // See pkg/option/resolver.go for the JSON definition ConfigSourcesOverrides: `{"allowConfigKeys":null,"denyConfigKeys":null}`, } -type config struct { +type Config struct { EnableDynamicConfig bool ConfigSources string ConfigSourcesOverrides string } -func (c config) Flags(flags *pflag.FlagSet) { +func (c Config) Flags(flags *pflag.FlagSet) { flags.Bool("enable-dynamic-config", c.EnableDynamicConfig, "Enables support for dynamic agent config") flags.String(resolver.ConfigSources, c.ConfigSources, "Ordered list of configuration sources") flags.MarkHidden(resolver.ConfigSources) diff --git a/pkg/dynamicconfig/reflectors.go b/pkg/dynamicconfig/reflectors.go index b192682407b26..0879540cc497f 100644 --- a/pkg/dynamicconfig/reflectors.go +++ b/pkg/dynamicconfig/reflectors.go @@ -31,7 +31,7 @@ const ( metadataName = "metadata.name=" ) -func NewConfigMapReflector(cs k8sClient.Clientset, t statedb.RWTable[DynamicConfig], c config, l *slog.Logger) []k8s.ReflectorConfig[DynamicConfig] { +func NewConfigMapReflector(cs k8sClient.Clientset, t statedb.RWTable[DynamicConfig], c Config, l *slog.Logger) []k8s.ReflectorConfig[DynamicConfig] { if !cs.IsEnabled() || !c.EnableDynamicConfig { return []k8s.ReflectorConfig[DynamicConfig]{} } @@ -61,7 +61,7 @@ func NewConfigMapReflector(cs k8sClient.Clientset, t statedb.RWTable[DynamicConf return reflectors } -func parseConfigs(c config) ([]resolver.ConfigSource, resolver.ConfigOverride, error) { +func parseConfigs(c Config) ([]resolver.ConfigSource, resolver.ConfigOverride, error) { var sources []resolver.ConfigSource if err := json.Unmarshal([]byte(c.ConfigSources), &sources); err != nil { return nil, resolver.ConfigOverride{}, fmt.Errorf("error during unmarshall config-sources: %w", err) diff --git a/pkg/dynamicconfig/table.go b/pkg/dynamicconfig/table.go index 3409487ac11ce..ce166db408089 100644 --- a/pkg/dynamicconfig/table.go +++ b/pkg/dynamicconfig/table.go @@ -85,7 +85,7 @@ func NewConfigTable(db *statedb.DB) (statedb.RWTable[DynamicConfig], error) { return tbl, db.RegisterTable(tbl) } -func RegisterConfigMapReflector(jobGroup job.Group, db *statedb.DB, rcs []k8s.ReflectorConfig[DynamicConfig], c config) error { +func RegisterConfigMapReflector(jobGroup job.Group, db *statedb.DB, rcs []k8s.ReflectorConfig[DynamicConfig], c Config) error { if !c.EnableDynamicConfig { return nil } @@ -125,6 +125,24 @@ func WatchKey(txn statedb.ReadTxn, table statedb.Table[DynamicConfig], key strin return entries[0], true, w } +// WatchAllKeys retrieves all DynamicConfig values accounting for priority when the +// key is present in multiple config sources. +func WatchAllKeys(txn statedb.ReadTxn, table statedb.Table[DynamicConfig]) (map[string]DynamicConfig, <-chan struct{}) { + keyValue := map[string]DynamicConfig{} + keyPriority := map[string]int{} + + iter, w := table.AllWatch(txn) + for obj := range iter { + priority, found := keyPriority[obj.Key.Name] + if !found || priority > obj.Priority { + keyValue[obj.Key.Name] = obj + keyPriority[obj.Key.Name] = obj.Priority + } + } + + return keyValue, w +} + func sortByPriority(entries []DynamicConfig) { sort.Slice(entries, func(i, j int) bool { return entries[i].Priority < entries[j].Priority diff --git a/pkg/dynamicconfig/table_test.go b/pkg/dynamicconfig/table_test.go index 0123b9f60943d..a5c7eb876972e 100644 --- a/pkg/dynamicconfig/table_test.go +++ b/pkg/dynamicconfig/table_test.go @@ -30,6 +30,40 @@ var ( dummyConfigMap = map[string]string{"key1": "value1", "key2": "value2"} ) +func TestWatchAllKeys(t *testing.T) { + cSource := `[{"kind":"config-map","namespace":"kube-system","name":"aLowPriority"},{"kind":"config-map","namespace":"kube-system","name":"cilium-config"}]` + expected := map[string]DynamicConfig{ + "key": { + Key: Key{ + Name: "key", + Source: "cilium-config", + }, + Value: "newValue", + Priority: 0, + }, + } + _, db, dct, _ := fixture(t, cSource) + + key := "key" + lowPrioritySource := "aLowPriority" // leading 'a' to test that sources that are not in priority map have lower priority + value := "value" + newValue := "newValue" + + upsertDummyEntry(db, dct, key, lowPrioritySource, value, 1) + _, w := WatchAllKeys(db.ReadTxn(), dct) + upsertDummyEntry(db, dct, key, cmName, newValue, 0) + select { + case <-w: + case <-time.After(2 * time.Second): + t.Fatal("WatchKey() failed to detect changes") + } + + keys, _ := WatchAllKeys(db.ReadTxn(), dct) + if !reflect.DeepEqual(keys, expected) { + t.Errorf("WatchAllKeys returned unexpected result. Got: %v, Expected: %v", keys, expected) + } +} + func TestWatchKey(t *testing.T) { cSource := `[{"kind":"config-map","namespace":"kube-system","name":"aLowPriority"},{"kind":"config-map","namespace":"kube-system","name":"cilium-config"}]` _, db, dct, _ := fixture(t, cSource) @@ -192,8 +226,8 @@ func fixture(t *testing.T, sources string) (*hive.Hive, *statedb.DB, statedb.RWT lc.Append(jg) return jg }, - func() config { - return config{ + func() Config { + return Config{ EnableDynamicConfig: true, ConfigSources: sources, ConfigSourcesOverrides: "{\"allowConfigKeys\":null,\"denyConfigKeys\":null}",