Skip to content

Commit 279a7e2

Browse files
NR-179188 report invalid Sub Agent remote config
NR-179188 store config state with hash
2 parents 255f3db + 3dfe4d2 commit 279a7e2

File tree

3 files changed

+205
-31
lines changed

3 files changed

+205
-31
lines changed

src/opamp/remote_config_hash.rs

Lines changed: 104 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,20 @@ use crate::super_agent::defaults::{REMOTE_AGENT_DATA_DIR, SUPER_AGENT_DATA_DIR};
1818
#[cfg(target_family = "unix")]
1919
const DIRECTORY_PERMISSIONS: u32 = 0o700;
2020

21+
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Hash, Eq)]
22+
#[serde(rename_all = "snake_case")]
23+
#[serde(tag = "state")]
24+
enum ConfigState {
25+
Applying,
26+
Applied,
27+
Failed { error_message: String },
28+
}
29+
2130
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Hash, Eq)]
2231
pub struct Hash {
2332
hash: String,
24-
applied: bool,
33+
#[serde(flatten)]
34+
state: ConfigState,
2535
}
2636

2737
#[derive(Error, Debug)]
@@ -43,22 +53,44 @@ pub enum HashRepositoryError {
4353
}
4454

4555
impl Hash {
46-
pub fn new(hash: String) -> Self {
47-
Self {
48-
hash,
49-
applied: false,
50-
}
51-
}
5256
pub fn get(&self) -> String {
5357
self.hash.clone()
5458
}
55-
5659
pub fn is_applied(&self) -> bool {
57-
self.applied
60+
self.state == ConfigState::Applied
61+
}
62+
63+
pub fn is_applying(&self) -> bool {
64+
self.state == ConfigState::Applying
65+
}
66+
67+
pub fn is_failed(&self) -> bool {
68+
// if let self.state = ConfigState::Failed(msg)
69+
matches!(&self.state, ConfigState::Failed { .. })
5870
}
5971

72+
pub fn error_message(&self) -> Option<String> {
73+
match &self.state {
74+
ConfigState::Failed { error_message: msg } => Some(msg.clone()),
75+
_ => None,
76+
}
77+
}
78+
}
79+
80+
impl Hash {
81+
pub fn new(hash: String) -> Self {
82+
Self {
83+
hash,
84+
state: ConfigState::Applying,
85+
}
86+
}
6087
pub fn apply(&mut self) {
61-
self.applied = true
88+
self.state = ConfigState::Applied;
89+
}
90+
91+
// It is mandatory for a failed hash to have the error
92+
pub fn fail(&mut self, error_message: String) {
93+
self.state = ConfigState::Failed { error_message };
6294
}
6395
}
6496

@@ -168,8 +200,8 @@ pub mod test {
168200
use std::os::unix::fs::PermissionsExt;
169201

170202
use super::{
171-
Hash, HashRepository, HashRepositoryError, HashRepositoryFile, DIRECTORY_PERMISSIONS,
172-
HASH_FILE_EXTENSION,
203+
ConfigState, Hash, HashRepository, HashRepositoryError, HashRepositoryFile,
204+
DIRECTORY_PERMISSIONS, HASH_FILE_EXTENSION,
173205
};
174206
use crate::config::persister::config_persister_file::FILE_PERMISSIONS;
175207
use crate::config::persister::config_writer_file::test::MockFileWriterMock;
@@ -186,8 +218,15 @@ pub mod test {
186218
impl Hash {
187219
pub fn applied(hash: String) -> Self {
188220
Self {
189-
applied: true,
190221
hash,
222+
state: ConfigState::Applied,
223+
}
224+
}
225+
226+
pub fn failed(hash: String, error_message: String) -> Self {
227+
Self {
228+
hash,
229+
state: ConfigState::Failed { error_message },
191230
}
192231
}
193232
}
@@ -206,15 +245,11 @@ pub mod test {
206245
}
207246

208247
impl MockHashRepositoryMock {
209-
pub fn should_get_applied_hash(&mut self, agent_id: &AgentID, hash: Hash) {
248+
pub fn should_get_hash(&mut self, agent_id: &AgentID, hash: Hash) {
210249
self.expect_get()
211250
.with(predicate::eq(agent_id.clone()))
212251
.once()
213-
.returning(move |_| {
214-
let mut hash = hash.clone();
215-
hash.apply();
216-
Ok(hash)
217-
});
252+
.returning(move |_| Ok(hash.clone()));
218253
}
219254
pub fn should_save_hash(&mut self, agent_id: &AgentID, hash: &Hash) {
220255
self.expect_save()
@@ -257,7 +292,7 @@ pub mod test {
257292

258293
// This indentation and the single quotes is to match serde yaml
259294
let content = r#"hash: '123456789'
260-
applied: true
295+
state: applied
261296
"#;
262297

263298
let mut expected_path = some_path.clone();
@@ -294,4 +329,53 @@ applied: true
294329
let result = hash_repository.get(&agent_id);
295330
assert_eq!(hash, result.unwrap());
296331
}
332+
333+
#[test]
334+
fn test_config_state_default_status() {
335+
//default status for a hash should be applying
336+
let hash = Hash::new("some-hash".into());
337+
assert!(hash.is_applying())
338+
}
339+
340+
#[test]
341+
fn test_config_state_transition() {
342+
// hash can change state. This is not ideal, as an applied hash should not go to failed
343+
let mut hash = Hash::new("some-hash".into());
344+
assert!(hash.is_applying());
345+
hash.apply();
346+
assert!(hash.is_applied());
347+
hash.fail("this is an error message".to_string());
348+
assert!(hash.is_failed());
349+
}
350+
351+
#[test]
352+
fn test_hash_serialization() {
353+
let mut hash = Hash::new("123456789".to_string());
354+
let expected = "hash: '123456789'\nstate: applying\n";
355+
assert_eq!(expected, serde_yaml::to_string(&hash).unwrap());
356+
357+
hash.apply();
358+
let expected = "hash: '123456789'\nstate: applied\n";
359+
assert_eq!(expected, serde_yaml::to_string(&hash).unwrap());
360+
361+
hash.fail("this is an error message".to_string());
362+
let expected =
363+
"hash: '123456789'\nstate: failed\nerror_message: this is an error message\n";
364+
assert_eq!(expected, serde_yaml::to_string(&hash).unwrap());
365+
}
366+
367+
#[test]
368+
fn test_hash_deserialization() {
369+
let mut hash = Hash::new("123456789".to_string());
370+
let content = "hash: '123456789'\nstate: applying\n";
371+
assert_eq!(hash, serde_yaml::from_str::<Hash>(&content).unwrap());
372+
373+
hash.apply();
374+
let content = "hash: '123456789'\nstate: applied\n";
375+
assert_eq!(hash, serde_yaml::from_str::<Hash>(&content).unwrap());
376+
377+
hash.fail("this is an error message".to_string());
378+
let content = "hash: '123456789'\nstate: failed\nerror_message: this is an error message\n";
379+
assert_eq!(hash, serde_yaml::from_str::<Hash>(&content).unwrap());
380+
}
297381
}

src/sub_agent/on_host/builder.rs

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,18 @@ where
117117
Vec::default(),
118118
maybe_opamp_client,
119119
)?);
120-
} else if !hash.is_applied() {
120+
} else if hash.is_applying() {
121121
report_remote_config_status_applied(opamp_client, &hash)?;
122122
hash.apply();
123123
self.hash_repository.save(&agent_id, &hash)?;
124+
} else if hash.is_failed() {
125+
// failed hash always has the error message
126+
let error_message = hash.error_message().unwrap();
127+
report_remote_config_status_error(
128+
opamp_client,
129+
&hash,
130+
error_message.to_string(),
131+
)?;
124132
}
125133
}
126134
}
@@ -170,6 +178,8 @@ mod test {
170178
use std::sync::mpsc::channel;
171179

172180
use nix::unistd::gethostname;
181+
use opamp_client::opamp::proto::RemoteConfigStatus;
182+
use opamp_client::opamp::proto::RemoteConfigStatuses::Failed;
173183
use opamp_client::operation::{
174184
capabilities::Capabilities,
175185
settings::{AgentDescription, DescriptionValueType, StartSettings},
@@ -256,6 +266,71 @@ mod test {
256266
.is_ok())
257267
}
258268

269+
#[test]
270+
fn test_builder_should_report_failed_config() {
271+
let ctx = Context::new();
272+
let (tx, _rx) = channel();
273+
// Mocks
274+
let mut opamp_builder = MockOpAMPClientBuilderMock::new();
275+
let mut hash_repository_mock = MockHashRepositoryMock::new();
276+
let mut instance_id_getter = MockInstanceIDGetterMock::new();
277+
let mut effective_agent_assembler = MockEffectiveAgentAssemblerMock::new();
278+
279+
// Structures
280+
let hostname = gethostname().unwrap_or_default().into_string().unwrap();
281+
let start_settings_infra = infra_agent_default_start_settings(&hostname);
282+
let final_agent = on_host_final_agent();
283+
let sub_agent_id = AgentID::new("infra_agent").unwrap();
284+
let sub_agent_config = SubAgentConfig {
285+
agent_type: final_agent.agent_type().clone(),
286+
};
287+
288+
// Expectations
289+
// Infra Agent OpAMP no final stop nor health, just after stopping on reload
290+
instance_id_getter.should_get(
291+
sub_agent_id.to_string(),
292+
"infra_agent_instance_id".to_string(),
293+
);
294+
295+
opamp_builder.should_build_and_start(
296+
sub_agent_id.clone(),
297+
start_settings_infra,
298+
|_, _, _| {
299+
let mut started_client = MockOpAMPClientMock::new();
300+
// failed conf should be reported
301+
started_client.should_set_remote_config_status(RemoteConfigStatus {
302+
error_message: "this is an error message".to_string(),
303+
status: Failed as i32,
304+
last_remote_config_hash: "a-hash".as_bytes().to_vec(),
305+
});
306+
Ok(started_client)
307+
},
308+
);
309+
310+
effective_agent_assembler.should_assemble_agent(
311+
&sub_agent_id,
312+
&sub_agent_config,
313+
final_agent,
314+
);
315+
316+
// return a failed hash
317+
let failed_hash =
318+
Hash::failed("a-hash".to_string(), "this is an error message".to_string());
319+
hash_repository_mock.should_get_hash(&sub_agent_id, failed_hash);
320+
321+
// Sub Agent Builder
322+
let on_host_builder = OnHostSubAgentBuilder::new(
323+
Some(&opamp_builder),
324+
&instance_id_getter,
325+
&hash_repository_mock,
326+
&effective_agent_assembler,
327+
);
328+
329+
assert!(on_host_builder
330+
.build(sub_agent_id, &sub_agent_config, tx, ctx)
331+
.is_ok());
332+
}
333+
259334
// HELPERS
260335
fn on_host_final_agent() -> FinalAgent {
261336
let mut final_agent: FinalAgent = FinalAgent::default();

src/super_agent/super_agent.rs

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ where
385385
// Sub Agent on remote config
386386
fn process_sub_agent_remote_config(
387387
&self,
388-
remote_config: RemoteConfig,
388+
mut remote_config: RemoteConfig,
389389
sub_agents: &mut StartedSubAgents<
390390
<S::NotStartedSubAgent as NotStartedSubAgent>::StartedSubAgent,
391391
>,
@@ -397,13 +397,28 @@ where
397397
self.sub_agent_remote_config_hash_repository
398398
.save(&remote_config.agent_id, &remote_config.hash)?;
399399
let remote_config_value = remote_config.get_unique()?;
400+
// If remote config is empty, we delete the persisted remote config so later the store
401+
// will load the local config
400402
if remote_config_value.is_empty() {
401403
self.remote_values_repo
402404
.delete_remote(&remote_config.agent_id)?;
403405
} else {
404-
let agent_values = AgentValues::try_from(remote_config_value.to_string())?;
405-
self.remote_values_repo
406-
.store_remote(&remote_config.agent_id, &agent_values)?;
406+
// If the config is not valid log we cannot report it to OpAMP as
407+
// we don't have access to the Sub Agent OpAMP Client here (yet) so
408+
// for now we mark the remote config as failed and we don't persist it.
409+
// When the Sub Agent is "recreated" it will report the remote config
410+
// as failed.
411+
match AgentValues::try_from(remote_config_value.to_string()) {
412+
Err(e) => {
413+
error!("Error applying Sub Agent remote config: {}", e);
414+
remote_config.hash.fail(e.to_string());
415+
self.sub_agent_remote_config_hash_repository
416+
.save(&remote_config.agent_id, &remote_config.hash)?;
417+
}
418+
Ok(agent_values) => self
419+
.remote_values_repo
420+
.store_remote(&remote_config.agent_id, &agent_values)?,
421+
}
407422
}
408423

409424
let config = self.sub_agents_config_store.load()?;
@@ -451,7 +466,7 @@ where
451466
if let Err(err) =
452467
self.apply_remote_config(remote_config.clone(), tx, running_sub_agents, ctx)
453468
{
454-
let error_message = format!("Error applying remote config: {}", err);
469+
let error_message = format!("Error applying Super Agent remote config: {}", err);
455470
error!(error_message);
456471
Ok(report_remote_config_status_error(
457472
opamp_client,
@@ -1219,9 +1234,9 @@ config_file: /some/path/newrelic-infra.yml
12191234
.times(2)
12201235
.returning(move || Ok(sub_agents_config.clone()));
12211236

1222-
hash_repository_mock.should_get_applied_hash(
1237+
hash_repository_mock.should_get_hash(
12231238
&AgentID::new_super_agent_id(),
1224-
Hash::new("a-hash".to_string()),
1239+
Hash::applied("a-hash".to_string()),
12251240
);
12261241

12271242
// two agents in the supervisor group
@@ -1293,9 +1308,9 @@ config_file: /some/path/newrelic-infra.yml
12931308
sub_agent_builder.should_build(3);
12941309

12951310
let mut hash_repository_mock = MockHashRepositoryMock::new();
1296-
hash_repository_mock.should_get_applied_hash(
1311+
hash_repository_mock.should_get_hash(
12971312
&AgentID::new_super_agent_id(),
1298-
Hash::new("a-hash".to_string()),
1313+
Hash::applied("a-hash".to_string()),
12991314
);
13001315

13011316
let mut sub_agents_config_store = MockSubAgentsConfigStore::new();
@@ -1506,7 +1521,7 @@ agents:
15061521
let status = RemoteConfigStatus {
15071522
status: Failed as i32,
15081523
last_remote_config_hash: remote_config.hash.get().into_bytes(),
1509-
error_message: "Error applying remote config: could not resolve config: `configuration is not valid YAML: `invalid type: string \"invalid_yaml_content:{}\", expected struct SubAgentsConfig``".to_string(),
1524+
error_message: "Error applying Super Agent remote config: could not resolve config: `configuration is not valid YAML: `invalid type: string \"invalid_yaml_content:{}\", expected struct SubAgentsConfig``".to_string(),
15101525
};
15111526
started_client.should_set_remote_config_status(status);
15121527

0 commit comments

Comments
 (0)