feat(opamp): compute and store effective_config_hash for collector pipelines#6872
feat(opamp): compute and store effective_config_hash for collector pipelines#6872juliaElastic wants to merge 8 commits intoelastic:mainfrom
Conversation
…pelines Computes a SHA-256 hash of the pipeline topology fields (receivers, processors, exporters, connectors, service.pipelines, service.extensions) from the OpAMP effective config and stores it as effective_config_hash in the .fleet-agents index. Non-topology fields (extensions config, service.telemetry, etc.) are excluded so the hash reflects only what the pipeline does, not how it is observed. Keys are canonicalized via yaml.v3 Marshal (which sorts alphabetically) before hashing to ensure identical topologies always produce the same hash regardless of key order. Closes elastic/ingest-dev#7064 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…h_test Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
This pull request does not have a backport label. Could you fix it @juliaElastic? 🙏
|
…onfig hash Adds an adjective-noun label (e.g. "swift-hawk") stored alongside effective_config_hash in .fleet-agents. The label is derived from the first two bytes of the SHA-256 hash using two fixed 256-entry wordlists embedded in source, giving 65,536 possible combinations. Because the wordlists are frozen in the codebase the mapping is stable across deployments and dependency updates. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
CI fails due to an environmental issue, not related to changes in this PR: The job, Package x86_64, has been canceled as it failed to get an agent after 5 tries. The job, Package x86_64 FIPS, has been canceled as it failed to get an agent after 5 tries. |
#6881 should address the CI issues. |
| if effectiveConfig.ConfigMap == nil || effectiveConfig.ConfigMap.ConfigMap[""] == nil { | ||
| return "", nil | ||
| } | ||
| body := effectiveConfig.ConfigMap.ConfigMap[""].Body |
There was a problem hiding this comment.
In theory we should hash everything in the configmap, not just the default config
There was a problem hiding this comment.
Added support for multiple config files: 9ac32c8
Though I'm not sure if two separate files should hash differently from the same content in one file.
| } | ||
|
|
||
| topology := make(map[string]any) | ||
| for _, k := range []string{"receivers", "processors", "exporters", "connectors"} { |
There was a problem hiding this comment.
Why do we need to only use these keys when computing the sha?
How do we ensure that a change to another key is emitted to a collector?
There was a problem hiding this comment.
Why do we need to only use these keys when computing the sha?
The issue scope is deliberate: the hash is meant to answer "are two collectors running the same pipeline wiring?", not "are all their configs byte-for-byte identical." The topology keys describe what data flows where. Non-topology fields like extensions.* configs or service.telemetry describe how the pipeline operates (scrape intervals, log levels, endpoints) but don't change the pipeline graph.
The use case is: two collectors that differ only in their health_check endpoint or telemetry verbosity should be considered "the same topology" and produce the same hash, so you can cluster/query them together.
How do we ensure that a change to another key is emitted to a collector?
Changes to other keys are not surfaced via the hash, but they are captured in full in effective_config (the redacted JSON blob already stored in .fleet-agents). If a non-topology field changes, effective_config changes, but effective_config_hash stays the same.
There was a problem hiding this comment.
Can you add that to the doc string or in schema.json? It's important to note how this hash should differ from the RemoteConfig hash
| hash := "aabbccdd" + strings.Repeat("00", 28) // 64-char hex | ||
| assert.Equal(t, LabelFromHash(hash), LabelFromHash(hash)) |
There was a problem hiding this comment.
What's the point of this test?
There was a problem hiding this comment.
it tests that calling the LabelFromHash is deterministic, gives the same response when called twice
Co-authored-by: Michel Laterman <82832767+michel-laterman@users.noreply.github.com>
Co-authored-by: Michel Laterman <82832767+michel-laterman@users.noreply.github.com>
…entry
HashEffectiveConfig now iterates all named config files in the OpAMP
ConfigMap in sorted key order, feeding each file's topology fields and
its key name into a single SHA-256. Previously only the default ("") file
was considered. Topology extraction is refactored into extractTopologyFields
for per-file reuse.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| keys := make([]string, 0, len(effectiveConfig.ConfigMap.ConfigMap)) | ||
| for k := range effectiveConfig.ConfigMap.ConfigMap { | ||
| keys = append(keys, k) | ||
| } | ||
| sort.Strings(keys) |
There was a problem hiding this comment.
| keys := make([]string, 0, len(effectiveConfig.ConfigMap.ConfigMap)) | |
| for k := range effectiveConfig.ConfigMap.ConfigMap { | |
| keys = append(keys, k) | |
| } | |
| sort.Strings(keys) | |
| keys := slices.Sorted(maps.Keys(effectiveConfig.ConfigMap.ConfigMap)) |
| } | ||
|
|
||
| topology := make(map[string]any) | ||
| for _, k := range []string{"receivers", "processors", "exporters", "connectors"} { |
There was a problem hiding this comment.
Can you add that to the doc string or in schema.json? It's important to note how this hash should differ from the RemoteConfig hash
Summary
effective_config_hashin the.fleet-agentsindex.receivers,processors,exporters,connectors,service.pipelines,service.extensions. Non-topology fields (extensions.*config,service.telemetry, etc.) are excluded.yaml.v3Marshal (sorts alphabetically) before hashing, so key order never affects the output.effective_config).effective_config_label— a human-readableadjective-nounlabel (e.g.swift-hawk) derived from the first two bytes of the hash using two fixed 256-entry wordlists embedded in source. 65,536 possible combinations; stable across deployments and dependency updates since the wordlists are frozen in the codebase.Closes: https://github.com/elastic/ingest-dev/issues/7064
To verify:
effective_config_hashandeffective_config_labelis stored in.fleet-agents.Changed files
model/schema.jsoneffective_config_hashandeffective_config_labelstring properties to agent definitioninternal/pkg/model/schema.goEffectiveConfigHashandEffectiveConfigLabelfields toAgentstructinternal/pkg/dl/constants.goFieldEffectiveConfigHashandFieldEffectiveConfigLabelinternal/pkg/api/configHash.goHashEffectiveConfig+hashConfigBodyimplementationinternal/pkg/api/configLabel.goLabelFromHash+ 256-entry adjective/noun wordlistsinternal/pkg/api/handleOpAMP.goHashEffectiveConfigandLabelFromHashinupdateAgent()internal/pkg/checkin/bulk.goeffectiveConfigHash/effectiveConfigLabelonextraT,WithEffectiveConfigHash/WithEffectiveConfigLabeloptions,toUpdateBodywrites both fieldsinternal/pkg/api/configHash_test.gointernal/pkg/api/configLabel_test.goTest plan
Hash (
configHash_test.go)TestHashEffectiveConfig_NilConfig— nil/empty config returns""with no errorTestHashEffectiveConfig_EmptyBody— empty body returns""with no errorTestHashEffectiveConfig_Determinism— same topology + differentservice.telemetry→ same hashTestHashEffectiveConfig_KeyOrderInvariant— shuffled key order → same hashTestHashEffectiveConfig_TopologyChange— changed receiver → different hashTestHashEffectiveConfig_AllowlistEnforcement— addedextensions.*config → hash unchangedTestHashEffectiveConfig_ServiceExtensionsIncluded—service.extensionslist change → different hashTestHashEffectiveConfig_HexEncoded— output is a 64-char lowercase hex stringLabel (
configLabel_test.go)TestLabelFromHash_Format— producesadjective-nounformatTestLabelFromHash_Deterministic— same hash always gives same labelTestLabelFromHash_DifferentBytesGiveDifferentLabels— different first bytes → different labelTestLabelFromHash_SameLabelForSameTopology— same topology → same labelTestLabelFromHash_EmptyInput/TestLabelFromHash_InvalidHex— graceful empty returnTestWordlistsHave256UniqueEntries— both wordlists have exactly 256 unique entries🤖 Generated with Claude Code