Skip to content

Commit 2ce6c6b

Browse files
committed
refactor: [#281] encapsulate RuntimeOutputs with semantic setters
- Make all RuntimeOutputs fields private (instance_ip, provision_method, service_endpoints) - Add RuntimeOutputs::new() constructor for empty outputs - Add semantic setters that indicate when data is set: - record_provisioning(ip) - after provision command - record_registration(ip) - after register command - record_services_started(endpoints) - after run command - Add low-level setters (set_instance_ip, set_provision_method) for compatibility with existing code - Add getter methods: instance_ip(), provision_method(), service_endpoints() - Implement Default trait for RuntimeOutputs The setter names now clearly document when data is populated during the deployment lifecycle, improving code readability and traceability.
1 parent 90bc914 commit 2ce6c6b

6 files changed

Lines changed: 166 additions & 40 deletions

File tree

src/domain/environment/context.rs

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -204,11 +204,7 @@ impl EnvironmentContext {
204204
user_inputs: UserInputs::new(name, provider_config, ssh_credentials, ssh_port)
205205
.expect("UserInputs::new with defaults should never fail - default config always passes validation"),
206206
internal_config: InternalConfig::new(name),
207-
runtime_outputs: RuntimeOutputs {
208-
instance_ip: None,
209-
provision_method: None,
210-
service_endpoints: None,
211-
},
207+
runtime_outputs: RuntimeOutputs::new(),
212208
}
213209
}
214210

@@ -250,11 +246,7 @@ impl EnvironmentContext {
250246
&params.environment_name,
251247
working_dir,
252248
),
253-
runtime_outputs: RuntimeOutputs {
254-
instance_ip: None,
255-
provision_method: None,
256-
service_endpoints: None,
257-
},
249+
runtime_outputs: RuntimeOutputs::new(),
258250
})
259251
}
260252

@@ -407,13 +399,13 @@ impl EnvironmentContext {
407399
/// Returns the instance IP address if available
408400
#[must_use]
409401
pub fn instance_ip(&self) -> Option<std::net::IpAddr> {
410-
self.runtime_outputs.instance_ip
402+
self.runtime_outputs.instance_ip()
411403
}
412404

413405
/// Returns the provision method
414406
#[must_use]
415407
pub fn provision_method(&self) -> Option<crate::domain::environment::ProvisionMethod> {
416-
self.runtime_outputs.provision_method
408+
self.runtime_outputs.provision_method()
417409
}
418410

419411
/// Returns the creation timestamp

src/domain/environment/mod.rs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -694,7 +694,7 @@ impl<S> Environment<S> {
694694
/// ```
695695
#[must_use]
696696
pub fn with_instance_ip(mut self, ip: IpAddr) -> Self {
697-
self.context_mut().runtime_outputs.instance_ip = Some(ip);
697+
self.context_mut().runtime_outputs.set_instance_ip(ip);
698698
self
699699
}
700700

@@ -713,7 +713,9 @@ impl<S> Environment<S> {
713713
/// Returns the environment with the provision method set.
714714
#[must_use]
715715
pub fn with_provision_method(mut self, method: runtime_outputs::ProvisionMethod) -> Self {
716-
self.context_mut().runtime_outputs.provision_method = Some(method);
716+
self.context_mut()
717+
.runtime_outputs
718+
.set_provision_method(method);
717719
self
718720
}
719721

@@ -1125,11 +1127,7 @@ mod tests {
11251127
data_dir: data_dir.clone(),
11261128
build_dir: build_dir.clone(),
11271129
},
1128-
runtime_outputs: RuntimeOutputs {
1129-
instance_ip: None,
1130-
provision_method: None,
1131-
service_endpoints: None,
1132-
},
1130+
runtime_outputs: RuntimeOutputs::new(),
11331131
created_at: chrono::Utc::now(),
11341132
};
11351133

@@ -1569,7 +1567,7 @@ mod tests {
15691567
.build();
15701568

15711569
// Runtime outputs start empty
1572-
assert_eq!(env.context.runtime_outputs.instance_ip, None);
1570+
assert_eq!(env.context.runtime_outputs.instance_ip(), None);
15731571
}
15741572

15751573
#[test]
@@ -1582,7 +1580,7 @@ mod tests {
15821580
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
15831581
let env = env.with_instance_ip(ip);
15841582

1585-
assert_eq!(env.context.runtime_outputs.instance_ip, Some(ip));
1583+
assert_eq!(env.context.runtime_outputs.instance_ip(), Some(ip));
15861584
}
15871585

15881586
#[test]

src/domain/environment/runtime_outputs.rs

Lines changed: 148 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,17 @@ impl ServiceEndpoints {
218218
/// Runtime outputs generated during deployment operations
219219
///
220220
/// This struct contains fields that are generated during deployment operations
221-
/// and represent the runtime state of deployed infrastructure. These fields
222-
/// are mutable as operations progress.
221+
/// and represent the runtime state of deployed infrastructure. Fields are
222+
/// private to protect invariants and provide semantic clarity through setters.
223+
///
224+
/// # Lifecycle
225+
///
226+
/// Fields are populated at different stages of the deployment lifecycle:
227+
/// - **Creation**: All fields are `None` (use `RuntimeOutputs::new()`)
228+
/// - **After Provisioning**: `instance_ip` and `provision_method` are set
229+
/// (use `record_provisioning()` or `record_registration()`)
230+
/// - **After Run Command**: `service_endpoints` is set
231+
/// (use `record_services_started()`)
223232
///
224233
/// # Future Fields
225234
///
@@ -233,19 +242,23 @@ impl ServiceEndpoints {
233242
/// use torrust_tracker_deployer_lib::domain::environment::runtime_outputs::{RuntimeOutputs, ProvisionMethod};
234243
/// use std::net::{IpAddr, Ipv4Addr};
235244
///
236-
/// let runtime_outputs = RuntimeOutputs {
237-
/// instance_ip: Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100))),
238-
/// provision_method: Some(ProvisionMethod::Provisioned),
239-
/// service_endpoints: None,
240-
/// };
245+
/// // Create empty runtime outputs
246+
/// let mut runtime_outputs = RuntimeOutputs::new();
247+
/// assert!(runtime_outputs.instance_ip().is_none());
248+
///
249+
/// // After provisioning, record the IP and method
250+
/// let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100));
251+
/// runtime_outputs.record_provisioning(ip);
252+
/// assert_eq!(runtime_outputs.instance_ip(), Some(ip));
253+
/// assert_eq!(runtime_outputs.provision_method(), Some(ProvisionMethod::Provisioned));
241254
/// ```
242255
#[derive(Debug, Clone, Serialize, Deserialize)]
243256
pub struct RuntimeOutputs {
244257
/// Instance IP address (populated after provisioning)
245258
///
246259
/// This field stores the IP address of the provisioned instance and is
247260
/// `None` until the environment has been successfully provisioned.
248-
pub instance_ip: Option<IpAddr>,
261+
instance_ip: Option<IpAddr>,
249262

250263
/// How the instance was provisioned
251264
///
@@ -257,7 +270,7 @@ pub struct RuntimeOutputs {
257270
/// - `Some(Provisioned)`: Instance was created via `provision` command
258271
/// - `Some(Registered)`: Instance was connected via `register` command
259272
#[serde(default)]
260-
pub provision_method: Option<ProvisionMethod>,
273+
provision_method: Option<ProvisionMethod>,
261274

262275
/// Service endpoints populated after services are started
263276
///
@@ -267,5 +280,130 @@ pub struct RuntimeOutputs {
267280
/// - `None`: Services not yet started or legacy state
268281
/// - `Some(endpoints)`: URLs for all running services
269282
#[serde(default)]
270-
pub service_endpoints: Option<ServiceEndpoints>,
283+
service_endpoints: Option<ServiceEndpoints>,
284+
}
285+
286+
impl RuntimeOutputs {
287+
/// Creates new empty runtime outputs
288+
///
289+
/// All fields are initialized to `None`, representing an environment
290+
/// that has not yet been provisioned or run.
291+
///
292+
/// # Examples
293+
///
294+
/// ```rust
295+
/// use torrust_tracker_deployer_lib::domain::environment::runtime_outputs::RuntimeOutputs;
296+
///
297+
/// let outputs = RuntimeOutputs::new();
298+
/// assert!(outputs.instance_ip().is_none());
299+
/// assert!(outputs.provision_method().is_none());
300+
/// assert!(outputs.service_endpoints().is_none());
301+
/// ```
302+
#[must_use]
303+
pub fn new() -> Self {
304+
Self {
305+
instance_ip: None,
306+
provision_method: None,
307+
service_endpoints: None,
308+
}
309+
}
310+
311+
// =========================================================================
312+
// Getters - Access runtime output values
313+
// =========================================================================
314+
315+
/// Returns the instance IP address if available
316+
///
317+
/// This is `None` until the environment has been provisioned or registered.
318+
#[must_use]
319+
pub fn instance_ip(&self) -> Option<IpAddr> {
320+
self.instance_ip
321+
}
322+
323+
/// Returns how the instance was provisioned
324+
///
325+
/// - `None`: Unknown or legacy state
326+
/// - `Some(Provisioned)`: Created via `provision` command
327+
/// - `Some(Registered)`: Connected via `register` command
328+
#[must_use]
329+
pub fn provision_method(&self) -> Option<ProvisionMethod> {
330+
self.provision_method
331+
}
332+
333+
/// Returns the service endpoints if available
334+
///
335+
/// This is `None` until the `run` command has started services successfully.
336+
#[must_use]
337+
pub fn service_endpoints(&self) -> Option<&ServiceEndpoints> {
338+
self.service_endpoints.as_ref()
339+
}
340+
341+
// =========================================================================
342+
// Semantic Setters - Record deployment lifecycle events
343+
// =========================================================================
344+
345+
/// Records that provisioning has completed with the given instance IP
346+
///
347+
/// Call this after the `provision` command successfully creates infrastructure.
348+
/// Sets both `instance_ip` and `provision_method` to `Provisioned`.
349+
///
350+
/// # Arguments
351+
///
352+
/// * `ip` - The IP address of the newly provisioned instance
353+
pub fn record_provisioning(&mut self, ip: IpAddr) {
354+
self.instance_ip = Some(ip);
355+
self.provision_method = Some(ProvisionMethod::Provisioned);
356+
}
357+
358+
/// Records that an existing instance has been registered
359+
///
360+
/// Call this after the `register` command connects to existing infrastructure.
361+
/// Sets both `instance_ip` and `provision_method` to `Registered`.
362+
///
363+
/// # Arguments
364+
///
365+
/// * `ip` - The IP address of the registered instance
366+
pub fn record_registration(&mut self, ip: IpAddr) {
367+
self.instance_ip = Some(ip);
368+
self.provision_method = Some(ProvisionMethod::Registered);
369+
}
370+
371+
/// Records that services have been started with the given endpoints
372+
///
373+
/// Call this after the `run` command successfully starts all services.
374+
/// The endpoints can then be displayed to users or used for health checks.
375+
///
376+
/// # Arguments
377+
///
378+
/// * `endpoints` - The URLs for all running services
379+
pub fn record_services_started(&mut self, endpoints: ServiceEndpoints) {
380+
self.service_endpoints = Some(endpoints);
381+
}
382+
383+
// =========================================================================
384+
// Low-level setters - For backward compatibility and state restoration
385+
// =========================================================================
386+
387+
/// Sets the instance IP directly
388+
///
389+
/// Prefer `record_provisioning()` or `record_registration()` which also
390+
/// set the provision method. This method is provided for cases where
391+
/// only the IP needs to be updated (e.g., deserialization workarounds).
392+
pub fn set_instance_ip(&mut self, ip: IpAddr) {
393+
self.instance_ip = Some(ip);
394+
}
395+
396+
/// Sets the provision method directly
397+
///
398+
/// Prefer `record_provisioning()` or `record_registration()` which also
399+
/// set the instance IP. This method is provided for backward compatibility.
400+
pub fn set_provision_method(&mut self, method: ProvisionMethod) {
401+
self.provision_method = Some(method);
402+
}
403+
}
404+
405+
impl Default for RuntimeOutputs {
406+
fn default() -> Self {
407+
Self::new()
408+
}
271409
}

src/domain/environment/state/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,7 @@ impl AnyEnvironmentState {
510510
/// - `None` if the environment hasn't been provisioned yet
511511
#[must_use]
512512
pub fn instance_ip(&self) -> Option<std::net::IpAddr> {
513-
self.context().runtime_outputs.instance_ip
513+
self.context().runtime_outputs.instance_ip()
514514
}
515515

516516
/// Get when the environment was created
@@ -538,7 +538,7 @@ impl AnyEnvironmentState {
538538
/// - `None` if the provision method hasn't been set yet (legacy or pre-provisioned state)
539539
#[must_use]
540540
pub fn provision_method(&self) -> Option<ProvisionMethod> {
541-
self.context().runtime_outputs.provision_method
541+
self.context().runtime_outputs.provision_method()
542542
}
543543

544544
/// Get the service endpoints if available, regardless of current state
@@ -552,7 +552,7 @@ impl AnyEnvironmentState {
552552
/// - `None` if services haven't been started yet or URLs weren't recorded
553553
#[must_use]
554554
pub fn service_endpoints(&self) -> Option<&ServiceEndpoints> {
555-
self.context().runtime_outputs.service_endpoints.as_ref()
555+
self.context().runtime_outputs.service_endpoints()
556556
}
557557

558558
/// Get the Prometheus configuration if enabled, regardless of current state

src/domain/environment/state/released.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ impl Environment<Released> {
3737
mut self,
3838
service_endpoints: ServiceEndpoints,
3939
) -> Environment<Running> {
40-
self.context.runtime_outputs.service_endpoints = Some(service_endpoints);
40+
self.context
41+
.runtime_outputs
42+
.record_services_started(service_endpoints);
4143
self.with_state(Running)
4244
}
4345

src/domain/environment/testing.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -170,11 +170,7 @@ impl EnvironmentTestBuilder {
170170
data_dir: data_dir.clone(),
171171
build_dir: build_dir.clone(),
172172
},
173-
runtime_outputs: crate::domain::environment::RuntimeOutputs {
174-
instance_ip: None,
175-
provision_method: None,
176-
service_endpoints: None,
177-
},
173+
runtime_outputs: crate::domain::environment::RuntimeOutputs::new(),
178174
};
179175

180176
let environment = Environment {

0 commit comments

Comments
 (0)