Skip to content

Commit f324ec4

Browse files
committed
Add support for tedge_config to prefer separate mapper config by default
Signed-off-by: James Rhodes <jarhodes314@gmail.com>
1 parent 1c97f70 commit f324ec4

File tree

3 files changed

+224
-2
lines changed

3 files changed

+224
-2
lines changed

crates/common/tedge_config/src/lib.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,20 @@ impl TEdgeConfig {
2727
config_location.load().await
2828
}
2929

30+
/// Load [TEdgeConfig], using a separate mapper config file as the default
31+
/// behaviour if no clouds are already configured
32+
///
33+
/// As of 2026-01-05, this is only used for testing how the new default will
34+
/// work before we fully adopt the new format. When we do this, we should
35+
/// probably also include `tedge-mapper config-migrate` commands in the
36+
/// relevant package postinstall scripts.
37+
#[cfg(feature = "test")]
38+
pub async fn load_prefer_separate_mapper_config(config_dir: impl AsRef<StdPath>) -> Result<Self, TEdgeConfigError> {
39+
let mut config_location = TEdgeConfigLocation::from_custom_root(config_dir.as_ref());
40+
config_location.default_to_mapper_config_dir();
41+
config_location.load().await
42+
}
43+
3044
pub async fn update_toml(
3145
self,
3246
update: &impl Fn(&mut TEdgeConfigDto, &TEdgeConfigReader) -> ConfigSettingResult<()>,

crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ pub(crate) struct TEdgeConfigLocation {
5959

6060
/// Full path to the `tedge.toml` file.
6161
tedge_config_file_path: Utf8PathBuf,
62+
63+
mapper_config_default_location: MapperConfigLocation,
64+
}
65+
66+
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
67+
pub(crate) enum MapperConfigLocation {
68+
TedgeToml,
69+
SeparateFile,
6270
}
6371

6472
impl Default for TEdgeConfigLocation {
@@ -77,9 +85,15 @@ impl TEdgeConfigLocation {
7785
tedge_config_file_path: Utf8Path::from_path(tedge_config_root_path.as_ref())
7886
.unwrap()
7987
.join(TEDGE_CONFIG_FILE),
88+
mapper_config_default_location: MapperConfigLocation::TedgeToml,
8089
}
8190
}
8291

92+
#[cfg(feature = "test")]
93+
pub(crate) fn default_to_mapper_config_dir(&mut self) {
94+
self.mapper_config_default_location = MapperConfigLocation::SeparateFile;
95+
}
96+
8397
pub(crate) fn tedge_config_root_path(&self) -> &Utf8Path {
8498
&self.tedge_config_root_path
8599
}
@@ -94,7 +108,14 @@ impl TEdgeConfigLocation {
94108
/// 1. New format (`mappers/[cloud].toml` or `mappers/[cloud].d/[profile].toml`) takes precedence
95109
/// 2. If new format exists for some profiles but not the requested one, returns NotFound
96110
/// 3. If the config directory is inaccessible due to a permissions error, return Error
97-
/// 4. If no new format exists at all, fall back to legacy tedge.toml format
111+
///
112+
/// If no new format exists, we then look at the
113+
/// `mapper_config_default_location` field of [TEdgeConfigLocation]:
114+
/// 1. If this is set to [MapperConfigLocation::TedgeToml], we fall back to tedge.toml
115+
/// 2. If this is set to [MapperConfigLocation::SeparateFile] and no cloud configurations
116+
/// exist in tedge.toml, we use the new mapper config format
117+
/// 3. If cloud configurations (for any cloud) do already exist in tedge.toml, we use
118+
/// tedge.toml until the configuration is explicitly migrated
98119
pub async fn decide_config_source<T>(&self, profile: Option<&ProfileName>) -> ConfigDecision
99120
where
100121
T: ExpectedCloudType,
@@ -122,7 +143,20 @@ impl TEdgeConfigLocation {
122143
(Ok(true), _, _) | (_, Ok(true), _) => ConfigDecision::NotFound { path },
123144

124145
// No new format configs exist for this cloud, use legacy
125-
(Ok(false), Ok(false), _) => ConfigDecision::LoadLegacy,
146+
(Ok(false), Ok(false), _) => {
147+
// TODO tidy this up
148+
let toml = tokio::fs::read_to_string(self.toml_path()).await.unwrap_or_default();
149+
let tedge_toml: toml::Table = toml::from_str(&toml).unwrap();
150+
let non_migrated_cloud_exists = ["c8y", "az", "aws"].iter().any(|key| tedge_toml.contains_key(*key));
151+
152+
if self.mapper_config_default_location == MapperConfigLocation::SeparateFile
153+
&& !non_migrated_cloud_exists
154+
{
155+
ConfigDecision::LoadNew { path }
156+
} else {
157+
ConfigDecision::LoadLegacy
158+
}
159+
}
126160

127161
// Permission error accessing mapper config directory
128162
(Err(err), _, _) | (_, Err(err), _) => ConfigDecision::PermissionError {

crates/common/tedge_config/tests/mapper_config.rs

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,177 @@ async fn empty_new_config_uses_tedge_toml_defaults() {
3636
"Device ID should come from tedge.toml defaults"
3737
);
3838
}
39+
40+
mod default_location_mode {
41+
use super::*;
42+
43+
mod prefers_tedge_toml {
44+
use tedge_config::models::CloudType;
45+
46+
use super::*;
47+
48+
#[tokio::test]
49+
async fn mapper_config_is_not_created_if_tedge_toml_does_not_exist() {
50+
let ttd = TempTedgeDir::new();
51+
52+
let tedge_config = TEdgeConfig::load(ttd.path()).await.unwrap();
53+
tedge_config
54+
.update_toml(&|dto, _rdr| {
55+
dto.c8y.try_get_mut(None, "c8y").unwrap().url =
56+
Some("example.com".parse().unwrap());
57+
Ok(())
58+
})
59+
.await
60+
.unwrap();
61+
62+
assert!(
63+
!ttd.path().join("mappers").exists(),
64+
"mappers dir should not have been created"
65+
);
66+
}
67+
68+
#[tokio::test]
69+
async fn mapper_config_is_created_for_new_profile_of_existing_separate_config_cloud() {
70+
let ttd = TempTedgeDir::new();
71+
72+
let tedge_config = TEdgeConfig::load(ttd.path())
73+
.await
74+
.unwrap();
75+
tedge_config
76+
.update_toml(&|dto, _rdr| {
77+
dto.c8y.try_get_mut(None, "c8y").unwrap().url =
78+
Some("example.com".parse().unwrap());
79+
Ok(())
80+
})
81+
.await
82+
.unwrap();
83+
84+
let tedge_config = TEdgeConfig::load(ttd.path())
85+
.await
86+
.unwrap();
87+
tedge_config.migrate_mapper_config(CloudType::C8y).await.unwrap();
88+
89+
let tedge_config = TEdgeConfig::load(ttd.path())
90+
.await
91+
.unwrap();
92+
tedge_config
93+
.update_toml(&|dto, _rdr| {
94+
dto.c8y.try_get_mut(Some("new-profile"), "c8y").unwrap().url =
95+
Some("new.example.com".parse().unwrap());
96+
Ok(())
97+
})
98+
.await
99+
.unwrap();
100+
101+
assert!(
102+
ttd.path().join("mappers").exists(),
103+
"mappers dir should exist"
104+
);
105+
let c8y_toml = tokio::fs::read_to_string(ttd.path().join("mappers/c8y.toml"))
106+
.await
107+
.unwrap();
108+
assert_eq!(c8y_toml.trim(), "url = \"example.com\"");
109+
}
110+
}
111+
112+
mod prefers_separate_config {
113+
use super::*;
114+
115+
#[tokio::test]
116+
async fn mapper_config_is_created_if_tedge_toml_does_not_exist() {
117+
let ttd = TempTedgeDir::new();
118+
119+
let tedge_config = TEdgeConfig::load_prefer_separate_mapper_config(ttd.path())
120+
.await
121+
.unwrap();
122+
tedge_config
123+
.update_toml(&|dto, _rdr| {
124+
dto.c8y.try_get_mut(None, "c8y").unwrap().url =
125+
Some("example.com".parse().unwrap());
126+
Ok(())
127+
})
128+
.await
129+
.unwrap();
130+
131+
assert!(
132+
ttd.path().join("mappers").exists(),
133+
"mappers dir should have been created"
134+
);
135+
136+
let tedge_toml = tokio::fs::read_to_string(ttd.path().join("tedge.toml"))
137+
.await
138+
.unwrap();
139+
let c8y_toml = tokio::fs::read_to_string(ttd.path().join("mappers/c8y.toml"))
140+
.await
141+
.unwrap();
142+
assert_eq!(tedge_toml, "");
143+
assert_eq!(c8y_toml.trim(), "url = \"example.com\"");
144+
}
145+
146+
#[tokio::test]
147+
async fn mapper_config_is_created_if_tedge_toml_has_no_cloud_configs() {
148+
let ttd = TempTedgeDir::new();
149+
ttd.file("tedge.toml").with_toml_content(toml::toml!(
150+
device.type = "my-fancy-device"
151+
));
152+
153+
let tedge_config = TEdgeConfig::load_prefer_separate_mapper_config(ttd.path())
154+
.await
155+
.unwrap();
156+
tedge_config
157+
.update_toml(&|dto, _rdr| {
158+
dto.c8y.try_get_mut(None, "c8y").unwrap().url =
159+
Some("example.com".parse().unwrap());
160+
Ok(())
161+
})
162+
.await
163+
.unwrap();
164+
165+
assert!(
166+
ttd.path().join("mappers").exists(),
167+
"mappers dir should have been created"
168+
);
169+
170+
let tedge_toml = tokio::fs::read_to_string(ttd.path().join("tedge.toml"))
171+
.await
172+
.unwrap();
173+
let c8y_toml = tokio::fs::read_to_string(ttd.path().join("mappers/c8y.toml"))
174+
.await
175+
.unwrap();
176+
assert!(!tedge_toml.contains("c8y"));
177+
assert_eq!(c8y_toml.trim(), "url = \"example.com\"");
178+
}
179+
180+
#[tokio::test]
181+
async fn mapper_config_is_not_created_for_new_profile_of_existing_tedge_toml_cloud() {
182+
let ttd = TempTedgeDir::new();
183+
ttd.file("tedge.toml")
184+
.with_toml_content(toml::toml!(c8y.url = "example.com"));
185+
186+
let tedge_config = TEdgeConfig::load_prefer_separate_mapper_config(ttd.path())
187+
.await
188+
.unwrap();
189+
tedge_config
190+
.update_toml(&|dto, _rdr| {
191+
dto.c8y.try_get_mut(Some("new-profile"), "c8y").unwrap().url =
192+
Some("new.example.com".parse().unwrap());
193+
Ok(())
194+
})
195+
.await
196+
.unwrap();
197+
198+
assert!(
199+
!ttd.path().join("mappers").exists(),
200+
"mappers dir should not have been created"
201+
);
202+
}
203+
}
204+
}
205+
206+
// TEST PLAN
207+
// no tedge.toml, old mode -> no migration
208+
// tedge.toml with c8y, old mode -> no migration
209+
// tedge.toml with az, old mode -> no migration
210+
// no tedge.toml, new mode -> migrate
211+
// tedge.toml without c8y, new mode -> migrate
212+
// tedge.toml cannot be read -> do nothing

0 commit comments

Comments
 (0)