Skip to content

Commit d5d00ba

Browse files
committed
feat: [#222] configure SSH port via cloud-init during VM provisioning
- Add ssh_port field to CloudInitContext with builder pattern - Update cloud-init.yml.tera to configure SSH port via write_files - Pass ssh_port from environment to TofuProjectGenerator - Refactor CloudInitRenderer: remove unused provider field, move ssh_port to render method - Update all tests to use new CloudInitRenderer API - Update issue spec to document cloud-init approach and explain why Ansible was discarded This implementation configures SSH port during VM initialization (provision phase) rather than post-provisioning (configure phase), ensuring SSH service is listening on the configured port before any Ansible connections are attempted.
1 parent 572094c commit d5d00ba

File tree

8 files changed

+187
-678
lines changed

8 files changed

+187
-678
lines changed

docs/issues/222-configure-ssh-service-port.md

Lines changed: 104 additions & 649 deletions
Large diffs are not rendered by default.

src/application/command_handlers/provision/handler.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ impl ProvisionCommandHandler {
252252
template_manager,
253253
environment.build_dir(),
254254
environment.ssh_credentials().clone(),
255+
environment.ssh_port(),
255256
environment.instance_name().clone(),
256257
environment.provider_config().clone(),
257258
));

src/infrastructure/templating/ansible/template/renderer/project_generator.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ impl AnsibleProjectGenerator {
316316

317317
tracing::debug!(
318318
"Successfully copied {} static template files",
319-
13 // ansible.cfg + 12 playbooks
319+
14 // ansible.cfg + 13 playbooks
320320
);
321321

322322
Ok(())

src/infrastructure/templating/tofu/template/common/renderer/cloud_init.rs

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
//! # use std::path::Path;
2323
//! # use torrust_tracker_deployer_lib::infrastructure::templating::tofu::template::common::renderer::cloud_init::CloudInitRenderer;
2424
//! # use torrust_tracker_deployer_lib::domain::template::TemplateManager;
25-
//! # use torrust_tracker_deployer_lib::domain::provider::Provider;
2625
//! # use torrust_tracker_deployer_lib::shared::Username;
2726
//! use torrust_tracker_deployer_lib::adapters::ssh::SshCredentials;
2827
//! # use std::path::PathBuf;
@@ -35,7 +34,7 @@
3534
//! PathBuf::from("fixtures/testing_rsa.pub"),
3635
//! Username::new("username").unwrap()
3736
//! );
38-
//! let renderer = CloudInitRenderer::new(template_manager, Provider::Lxd);
37+
//! let renderer = CloudInitRenderer::new(template_manager);
3938
//!
4039
//! // Just demonstrate creating the renderer - actual rendering requires
4140
//! // a proper template manager setup with cloud-init templates
@@ -48,7 +47,6 @@ use std::sync::Arc;
4847
use thiserror::Error;
4948

5049
use crate::adapters::ssh::credentials::SshCredentials;
51-
use crate::domain::provider::Provider;
5250
use crate::domain::template::file::File;
5351
use crate::domain::template::{TemplateManager, TemplateManagerError};
5452

@@ -100,12 +98,10 @@ pub enum CloudInitRendererError {
10098
/// It follows the Single Responsibility Principle by focusing solely on cloud-init
10199
/// template operations, making the main `TofuProjectGenerator` simpler and more focused.
102100
///
103-
/// Note: The provider field is kept for potential future provider-specific customization,
104-
/// but currently all providers use the same common cloud-init template.
101+
/// All providers (LXD, Hetzner) use the same common cloud-init template, so no
102+
/// provider-specific logic is needed.
105103
pub struct CloudInitRenderer {
106104
template_manager: Arc<TemplateManager>,
107-
#[allow(dead_code)]
108-
provider: Provider,
109105
}
110106

111107
impl CloudInitRenderer {
@@ -125,17 +121,13 @@ impl CloudInitRenderer {
125121
/// # Arguments
126122
///
127123
/// * `template_manager` - Arc reference to the template manager for file operations
128-
/// * `provider` - The infrastructure provider (LXD, Hetzner) - kept for future customization
129124
///
130125
/// # Returns
131126
///
132127
/// * `CloudInitRenderer` - A new renderer instance
133128
#[must_use]
134-
pub fn new(template_manager: Arc<TemplateManager>, provider: Provider) -> Self {
135-
Self {
136-
template_manager,
137-
provider,
138-
}
129+
pub fn new(template_manager: Arc<TemplateManager>) -> Self {
130+
Self { template_manager }
139131
}
140132

141133
/// Renders the cloud-init.yml.tera template with SSH credentials
@@ -149,6 +141,7 @@ impl CloudInitRenderer {
149141
/// # Arguments
150142
///
151143
/// * `ssh_credentials` - SSH credentials containing public key path for cloud-init injection
144+
/// * `ssh_port` - The SSH service port to configure in cloud-init
152145
/// * `output_dir` - Directory where the rendered `cloud-init.yml` file will be written
153146
///
154147
/// # Returns
@@ -169,10 +162,11 @@ impl CloudInitRenderer {
169162
pub async fn render(
170163
&self,
171164
ssh_credentials: &SshCredentials,
165+
ssh_port: u16,
172166
output_dir: &Path,
173167
) -> Result<(), CloudInitRendererError> {
174168
tracing::debug!(
175-
provider = %self.provider,
169+
ssh_port = ssh_port,
176170
"Rendering cloud-init template with SSH public key injection"
177171
);
178172

@@ -193,14 +187,14 @@ impl CloudInitRenderer {
193187
.map_err(|_| CloudInitRendererError::FileCreationFailed)?;
194188

195189
// Render cloud-init template (shared logic for all providers)
196-
self.render_cloud_init(&template_file, ssh_credentials, output_dir)
190+
Self::render_cloud_init(&template_file, ssh_credentials, ssh_port, output_dir)
197191
}
198192

199193
/// Renders cloud-init template (shared logic for all providers)
200194
fn render_cloud_init(
201-
&self,
202195
template_file: &File,
203196
ssh_credentials: &SshCredentials,
197+
ssh_port: u16,
204198
output_dir: &Path,
205199
) -> Result<(), CloudInitRendererError> {
206200
use crate::infrastructure::templating::tofu::template::common::wrappers::cloud_init::{
@@ -214,6 +208,7 @@ impl CloudInitRenderer {
214208
.map_err(|_| CloudInitRendererError::SshKeyReadError)?
215209
.with_username(ssh_credentials.ssh_username.as_str())
216210
.map_err(|_| CloudInitRendererError::ContextCreationFailed)?
211+
.with_ssh_port(ssh_port)
217212
.build()
218213
.map_err(|_| CloudInitRendererError::ContextCreationFailed)?;
219214

@@ -228,7 +223,6 @@ impl CloudInitRenderer {
228223
.map_err(|_| CloudInitRendererError::CloudInitTemplateRenderFailed)?;
229224

230225
tracing::debug!(
231-
provider = %self.provider,
232226
"Successfully rendered cloud-init template to {}",
233227
output_path.display()
234228
);
@@ -308,9 +302,9 @@ users:
308302
}
309303

310304
#[test]
311-
fn it_should_create_cloud_init_renderer_with_template_manager_and_provider() {
305+
fn it_should_create_cloud_init_renderer_with_template_manager() {
312306
let template_manager = Arc::new(TemplateManager::new(std::env::temp_dir()));
313-
let renderer = CloudInitRenderer::new(template_manager, Provider::Lxd);
307+
let renderer = CloudInitRenderer::new(template_manager);
314308

315309
// Verify the renderer was created successfully
316310
// Just check that it contains the expected template manager reference
@@ -328,13 +322,15 @@ users:
328322
#[tokio::test]
329323
async fn it_should_render_cloud_init_template_successfully() {
330324
let template_manager = create_mock_template_manager_with_cloud_init();
331-
let renderer = CloudInitRenderer::new(template_manager, Provider::Lxd);
325+
let renderer = CloudInitRenderer::new(template_manager);
332326

333327
let temp_dir = TempDir::new().expect("Failed to create temp dir");
334328
let ssh_credentials = create_mock_ssh_credentials(temp_dir.path());
335329
let output_dir = TempDir::new().expect("Failed to create output dir");
336330

337-
let result = renderer.render(&ssh_credentials, output_dir.path()).await;
331+
let result = renderer
332+
.render(&ssh_credentials, 22, output_dir.path())
333+
.await;
338334

339335
assert!(
340336
result.is_ok(),
@@ -367,7 +363,7 @@ users:
367363
#[tokio::test]
368364
async fn it_should_fail_when_ssh_key_file_missing() {
369365
let template_manager = create_mock_template_manager_with_cloud_init();
370-
let renderer = CloudInitRenderer::new(template_manager, Provider::Lxd);
366+
let renderer = CloudInitRenderer::new(template_manager);
371367

372368
// Create SSH credentials with non-existent key file
373369
let temp_dir = TempDir::new().expect("Failed to create temp dir");
@@ -380,7 +376,9 @@ users:
380376

381377
let output_dir = TempDir::new().expect("Failed to create output dir");
382378

383-
let result = renderer.render(&ssh_credentials, output_dir.path()).await;
379+
let result = renderer
380+
.render(&ssh_credentials, 22, output_dir.path())
381+
.await;
384382

385383
assert!(result.is_err(), "Should fail when SSH key file is missing");
386384
match result.unwrap_err() {
@@ -394,7 +392,7 @@ users:
394392
#[tokio::test]
395393
async fn it_should_fail_when_output_directory_is_readonly() {
396394
let template_manager = create_mock_template_manager_with_cloud_init();
397-
let renderer = CloudInitRenderer::new(template_manager, Provider::Lxd);
395+
let renderer = CloudInitRenderer::new(template_manager);
398396

399397
let temp_dir = TempDir::new().expect("Failed to create temp dir");
400398
let ssh_credentials = create_mock_ssh_credentials(temp_dir.path());
@@ -408,7 +406,9 @@ users:
408406
fs::set_permissions(output_dir.path(), permissions)
409407
.expect("Failed to set readonly permissions");
410408

411-
let result = renderer.render(&ssh_credentials, output_dir.path()).await;
409+
let result = renderer
410+
.render(&ssh_credentials, 22, output_dir.path())
411+
.await;
412412

413413
assert!(
414414
result.is_err(),

src/infrastructure/templating/tofu/template/common/renderer/project_generator.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ pub struct TofuProjectGenerator {
143143
template_manager: Arc<TemplateManager>,
144144
build_dir: PathBuf,
145145
ssh_credentials: SshCredentials,
146+
ssh_port: u16,
146147
cloud_init_renderer: CloudInitRenderer,
147148
instance_name: InstanceName,
148149
provider: Provider,
@@ -157,6 +158,7 @@ impl TofuProjectGenerator {
157158
/// * `template_manager` - The template manager to source templates from
158159
/// * `build_dir` - The destination directory where templates will be rendered
159160
/// * `ssh_credentials` - The SSH credentials for injecting public key into cloud-init
161+
/// * `ssh_port` - The SSH service port to configure in cloud-init
160162
/// * `instance_name` - The name of the instance to be created (for template rendering)
161163
/// * `provider_config` - The provider configuration containing provider type and settings
162164
///
@@ -165,16 +167,18 @@ impl TofuProjectGenerator {
165167
template_manager: Arc<TemplateManager>,
166168
build_dir: P,
167169
ssh_credentials: SshCredentials,
170+
ssh_port: u16,
168171
instance_name: InstanceName,
169172
provider_config: ProviderConfig,
170173
) -> Self {
171174
let provider = provider_config.provider();
172-
let cloud_init_renderer = CloudInitRenderer::new(template_manager.clone(), provider);
175+
let cloud_init_renderer = CloudInitRenderer::new(template_manager.clone());
173176

174177
Self {
175178
template_manager,
176179
build_dir: build_dir.as_ref().to_path_buf(),
177180
ssh_credentials,
181+
ssh_port,
178182
cloud_init_renderer,
179183
instance_name,
180184
provider,
@@ -387,7 +391,7 @@ impl TofuProjectGenerator {
387391

388392
// Use collaborator to render cloud-init.yml.tera template
389393
self.cloud_init_renderer
390-
.render(&self.ssh_credentials, destination_dir)
394+
.render(&self.ssh_credentials, self.ssh_port, destination_dir)
391395
.await
392396
.map_err(|source| TofuProjectGeneratorError::CloudInitRenderingFailed { source })?;
393397

@@ -612,6 +616,7 @@ mod tests {
612616
template_manager,
613617
&build_path,
614618
ssh_credentials,
619+
22, // Default SSH port for tests
615620
test_instance_name(),
616621
test_lxd_provider_config(),
617622
);
@@ -631,6 +636,7 @@ mod tests {
631636
template_manager,
632637
&build_path,
633638
ssh_credentials,
639+
22, // Default SSH port for tests
634640
test_instance_name(),
635641
test_lxd_provider_config(),
636642
);
@@ -650,6 +656,7 @@ mod tests {
650656
template_manager,
651657
&build_path,
652658
ssh_credentials,
659+
22,
653660
test_instance_name(),
654661
test_lxd_provider_config(),
655662
);
@@ -669,6 +676,7 @@ mod tests {
669676
template_manager,
670677
&build_path,
671678
ssh_credentials,
679+
22,
672680
test_instance_name(),
673681
test_lxd_provider_config(),
674682
);
@@ -699,6 +707,7 @@ mod tests {
699707
template_manager,
700708
&build_path,
701709
ssh_credentials,
710+
22,
702711
test_instance_name(),
703712
test_lxd_provider_config(),
704713
);
@@ -740,6 +749,7 @@ mod tests {
740749
template_manager,
741750
&build_path,
742751
ssh_credentials,
752+
22,
743753
test_instance_name(),
744754
test_lxd_provider_config(),
745755
);
@@ -777,6 +787,7 @@ mod tests {
777787
template_manager,
778788
&build_path,
779789
ssh_credentials,
790+
22,
780791
test_instance_name(),
781792
test_lxd_provider_config(),
782793
);
@@ -838,6 +849,7 @@ mod tests {
838849
template_manager,
839850
temp_dir.path(),
840851
ssh_credentials,
852+
22,
841853
test_instance_name(),
842854
test_lxd_provider_config(),
843855
);
@@ -868,6 +880,7 @@ mod tests {
868880
template_manager,
869881
&build_path,
870882
ssh_credentials,
883+
22,
871884
test_instance_name(),
872885
test_lxd_provider_config(),
873886
);
@@ -886,6 +899,7 @@ mod tests {
886899
template_manager,
887900
&build_path,
888901
ssh_credentials,
902+
22,
889903
test_instance_name(),
890904
test_lxd_provider_config(),
891905
);
@@ -914,6 +928,7 @@ mod tests {
914928
template_manager,
915929
&build_path,
916930
ssh_credentials,
931+
22,
917932
test_instance_name(),
918933
test_lxd_provider_config(),
919934
);
@@ -942,6 +957,7 @@ mod tests {
942957
template_manager,
943958
&build_path,
944959
ssh_credentials,
960+
22,
945961
test_instance_name(),
946962
test_lxd_provider_config(),
947963
);
@@ -971,6 +987,7 @@ mod tests {
971987
template_manager,
972988
&build_path,
973989
ssh_credentials,
990+
22,
974991
test_instance_name(),
975992
test_lxd_provider_config(),
976993
);
@@ -995,6 +1012,7 @@ mod tests {
9951012
template_manager,
9961013
&build_path,
9971014
ssh_credentials,
1015+
22,
9981016
test_instance_name(),
9991017
test_lxd_provider_config(),
10001018
);
@@ -1032,6 +1050,7 @@ mod tests {
10321050
template_manager,
10331051
temp_dir.path(),
10341052
ssh_credentials,
1053+
22,
10351054
test_instance_name(),
10361055
test_lxd_provider_config(),
10371056
);
@@ -1080,6 +1099,7 @@ mod tests {
10801099
template_manager.clone(),
10811100
&build_path1,
10821101
ssh_credentials1,
1102+
22,
10831103
test_instance_name(),
10841104
test_lxd_provider_config(),
10851105
);
@@ -1088,6 +1108,7 @@ mod tests {
10881108
template_manager,
10891109
&build_path2,
10901110
ssh_credentials2,
1111+
22,
10911112
test_instance_name(),
10921113
test_lxd_provider_config(),
10931114
);
@@ -1147,6 +1168,7 @@ mod tests {
11471168
template_manager,
11481169
temp_dir.path(),
11491170
ssh_credentials,
1171+
22,
11501172
test_instance_name(),
11511173
test_lxd_provider_config(),
11521174
);
@@ -1206,6 +1228,7 @@ mod tests {
12061228
template_manager,
12071229
temp_dir.path(),
12081230
ssh_credentials,
1231+
22,
12091232
test_instance_name(),
12101233
test_lxd_provider_config(),
12111234
);

0 commit comments

Comments
 (0)