Skip to content

Commit 202f57a

Browse files
fix: preserve env var references in manager.yaml during OpAMP config updates (#3205)
When the server pushes config updates (e.g. measurements_interval), the managerReload function was marshaling the in-memory config (which has resolved env var values) back to YAML, replacing ${env:...} references with plaintext secrets. This changes to a read-modify-write approach: read the raw on-disk file (which preserves env var strings), selectively update only the updatable fields, and write back. Co-authored-by: Ian Adams <ian.sam.adams@gmail.com>
1 parent 0febd29 commit 202f57a

2 files changed

Lines changed: 130 additions & 5 deletions

File tree

opamp/observiq/reload_funcs.go

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,59 @@ func managerReload(client *Client, managerConfigPath string) opamp.ReloadFunc {
8888
client.ident.agentName = newConfig.AgentName
8989
client.ident.labels = newConfig.Labels
9090

91-
// Write out new config file
92-
// Marshal back into bytes
93-
newContents, err := yaml.Marshal(client.currentConfig)
91+
// Read the raw on-disk file (preserves ${env:...} references)
92+
rawContents, err := os.ReadFile(filepath.Clean(managerConfigPath))
93+
if err != nil {
94+
// Rollback file
95+
if rollbackErr := rollbackFunc(); rollbackErr != nil {
96+
client.logger.Error("Rollback failed for manager config", zap.Error(rollbackErr))
97+
}
98+
client.ident = rollbackIdent
99+
client.currentConfig = *rollBackCfg
100+
return false, fmt.Errorf("failed to read manager config for update: %w", err)
101+
}
102+
103+
// Unmarshal into a generic map (yaml.v3 does NOT resolve env vars)
104+
var rawMap map[string]any
105+
if err := yaml.Unmarshal(rawContents, &rawMap); err != nil {
106+
// Rollback file
107+
if rollbackErr := rollbackFunc(); rollbackErr != nil {
108+
client.logger.Error("Rollback failed for manager config", zap.Error(rollbackErr))
109+
}
110+
client.ident = rollbackIdent
111+
client.currentConfig = *rollBackCfg
112+
return false, fmt.Errorf("failed to parse manager config for update: %w", err)
113+
}
114+
115+
// Update only the fields that are allowed to change via OpAMP
116+
if client.currentConfig.AgentName != nil {
117+
rawMap["agent_name"] = *client.currentConfig.AgentName
118+
} else {
119+
delete(rawMap, "agent_name")
120+
}
121+
if client.currentConfig.Labels != nil {
122+
rawMap["labels"] = *client.currentConfig.Labels
123+
} else {
124+
delete(rawMap, "labels")
125+
}
126+
if client.currentConfig.MeasurementsInterval != 0 {
127+
rawMap["measurements_interval"] = client.currentConfig.MeasurementsInterval
128+
} else {
129+
delete(rawMap, "measurements_interval")
130+
}
131+
if client.currentConfig.ExtraMeasurementsAttributes != nil {
132+
rawMap["extra_measurements_attributes"] = client.currentConfig.ExtraMeasurementsAttributes
133+
} else {
134+
delete(rawMap, "extra_measurements_attributes")
135+
}
136+
if client.currentConfig.TopologyInterval != nil {
137+
rawMap["topology_interval"] = *client.currentConfig.TopologyInterval
138+
} else {
139+
delete(rawMap, "topology_interval")
140+
}
141+
142+
// Marshal the map back (env var references in untouched fields are preserved)
143+
newContents, err := yaml.Marshal(rawMap)
94144
if err != nil {
95145
// Rollback file
96146
if rollbackErr := rollbackFunc(); rollbackErr != nil {

opamp/observiq/reload_funcs_test.go

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,15 @@ func Test_managerReload(t *testing.T) {
132132
assert.Equal(t, newConfig.AgentName, client.ident.agentName)
133133
assert.Equal(t, newConfig.AgentName, client.currentConfig.AgentName)
134134

135-
// Verify new file was written
135+
// Verify new file was written with correct values
136136
data, err := os.ReadFile(managerFilePath)
137137
assert.NoError(t, err)
138-
assert.Equal(t, newContents, data)
138+
139+
var writtenConfig opamp.Config
140+
assert.NoError(t, yaml.Unmarshal(data, &writtenConfig))
141+
assert.Equal(t, newConfig.Endpoint, writtenConfig.Endpoint)
142+
assert.Equal(t, newConfig.AgentID.String(), writtenConfig.AgentID.String())
143+
assert.Equal(t, *newConfig.AgentName, *writtenConfig.AgentName)
139144
},
140145
},
141146
{
@@ -196,6 +201,76 @@ func Test_managerReload(t *testing.T) {
196201
assert.Equal(t, currContents, data)
197202
},
198203
},
204+
{
205+
desc: "Env var references preserved in non-updatable fields",
206+
testFunc: func(*testing.T) {
207+
tmpDir := t.TempDir()
208+
209+
managerFilePath := filepath.Join(tmpDir, ManagerConfigName)
210+
211+
// Write a manager.yaml with env var references in non-updatable fields
212+
rawYAML := []byte(`endpoint: wss://example.com
213+
secret_key: ${env:OPAMP_SECRET_KEY}
214+
agent_id: ` + testAgentID.String() + `
215+
labels: env=prod
216+
`)
217+
err := os.WriteFile(managerFilePath, rawYAML, 0600)
218+
require.NoError(t, err)
219+
220+
labels := "env=prod"
221+
currConfig := &opamp.Config{
222+
Endpoint: "wss://example.com",
223+
SecretKey: func() *string {
224+
s := "resolved-secret-value"
225+
return &s
226+
}(),
227+
AgentID: testAgentID,
228+
Labels: &labels,
229+
}
230+
231+
mockOpAmpClient := mocks.NewMockOpAMPClient(t)
232+
mockOpAmpClient.On("SetAgentDescription", mock.Anything).Return(nil)
233+
234+
client := &Client{
235+
logger: zap.NewNop(),
236+
opampClient: mockOpAmpClient,
237+
ident: newIdentity(zap.NewNop(), *currConfig, "0.0.0"),
238+
currentConfig: *currConfig,
239+
measurementsSender: newMeasurementsSender(zap.NewNop(), nil, mockOpAmpClient, 0, nil),
240+
topologySender: newTopologySender(zap.NewNop(), nil, mockOpAmpClient, nil),
241+
}
242+
reloadFunc := managerReload(client, managerFilePath)
243+
244+
// Create a new config with changed measurements_interval
245+
agentName := "new-name"
246+
newConfig := &opamp.Config{
247+
Endpoint: "wss://example.com",
248+
AgentID: testAgentID,
249+
Labels: &labels,
250+
AgentName: &agentName,
251+
MeasurementsInterval: 30 * time.Second,
252+
}
253+
254+
newContents, err := yaml.Marshal(newConfig)
255+
require.NoError(t, err)
256+
257+
changed, err := reloadFunc(newContents)
258+
assert.NoError(t, err)
259+
assert.True(t, changed)
260+
261+
// Read back the file and verify env var reference is preserved
262+
data, err := os.ReadFile(managerFilePath)
263+
require.NoError(t, err)
264+
265+
dataStr := string(data)
266+
assert.Contains(t, dataStr, "${env:OPAMP_SECRET_KEY}", "env var reference should be preserved")
267+
assert.NotContains(t, dataStr, "resolved-secret-value", "resolved secret should not appear in file")
268+
269+
// Verify the updatable fields were updated
270+
assert.Contains(t, dataStr, "new-name")
271+
assert.Contains(t, dataStr, "30s")
272+
},
273+
},
199274
}
200275

201276
for _, tc := range testCases {

0 commit comments

Comments
 (0)