Skip to main content

terraform_wrapper/commands/
init.rs

1use crate::Terraform;
2use crate::command::TerraformCommand;
3use crate::error::Result;
4use crate::exec::{self, CommandOutput};
5
6/// Command for initializing a Terraform working directory.
7///
8/// Downloads providers, initializes backend, and prepares the directory
9/// for other commands.
10///
11/// ```no_run
12/// # async fn example() -> terraform_wrapper::error::Result<()> {
13/// use terraform_wrapper::{Terraform, TerraformCommand};
14/// use terraform_wrapper::commands::init::InitCommand;
15///
16/// let tf = Terraform::builder().working_dir("/tmp/infra").build()?;
17/// InitCommand::new().execute(&tf).await?;
18/// # Ok(())
19/// # }
20/// ```
21#[derive(Debug, Clone, Default)]
22pub struct InitCommand {
23    backend_configs: Vec<(String, String)>,
24    backend_config_files: Vec<String>,
25    upgrade: bool,
26    reconfigure: bool,
27    migrate_state: bool,
28    plugin_dir: Option<String>,
29    lock: Option<bool>,
30    lock_timeout: Option<String>,
31    raw_args: Vec<String>,
32}
33
34impl InitCommand {
35    /// Create a new init command with default options.
36    #[must_use]
37    pub fn new() -> Self {
38        Self::default()
39    }
40
41    /// Add a backend configuration key-value pair (`-backend-config=key=value`).
42    #[must_use]
43    pub fn backend_config(mut self, key: &str, value: &str) -> Self {
44        self.backend_configs
45            .push((key.to_string(), value.to_string()));
46        self
47    }
48
49    /// Add a backend configuration file (`-backend-config=<path>`).
50    #[must_use]
51    pub fn backend_config_file(mut self, path: &str) -> Self {
52        self.backend_config_files.push(path.to_string());
53        self
54    }
55
56    /// Update modules and plugins to the latest allowed versions (`-upgrade`).
57    #[must_use]
58    pub fn upgrade(mut self) -> Self {
59        self.upgrade = true;
60        self
61    }
62
63    /// Reconfigure backend, ignoring any saved configuration (`-reconfigure`).
64    #[must_use]
65    pub fn reconfigure(mut self) -> Self {
66        self.reconfigure = true;
67        self
68    }
69
70    /// Reconfigure backend and attempt to migrate state (`-migrate-state`).
71    #[must_use]
72    pub fn migrate_state(mut self) -> Self {
73        self.migrate_state = true;
74        self
75    }
76
77    /// Directory to search for provider plugins (`-plugin-dir`).
78    #[must_use]
79    pub fn plugin_dir(mut self, path: &str) -> Self {
80        self.plugin_dir = Some(path.to_string());
81        self
82    }
83
84    /// Enable or disable state locking (`-lock`).
85    #[must_use]
86    pub fn lock(mut self, enabled: bool) -> Self {
87        self.lock = Some(enabled);
88        self
89    }
90
91    /// Duration to wait for state lock (`-lock-timeout`).
92    #[must_use]
93    pub fn lock_timeout(mut self, timeout: &str) -> Self {
94        self.lock_timeout = Some(timeout.to_string());
95        self
96    }
97
98    /// Add a raw argument (escape hatch for unsupported options).
99    #[must_use]
100    pub fn arg(mut self, arg: impl Into<String>) -> Self {
101        self.raw_args.push(arg.into());
102        self
103    }
104}
105
106impl TerraformCommand for InitCommand {
107    type Output = CommandOutput;
108
109    fn args(&self) -> Vec<String> {
110        let mut args = vec!["init".to_string()];
111
112        for (key, value) in &self.backend_configs {
113            args.push(format!("-backend-config={key}={value}"));
114        }
115        for file in &self.backend_config_files {
116            args.push(format!("-backend-config={file}"));
117        }
118        if self.upgrade {
119            args.push("-upgrade".to_string());
120        }
121        if self.reconfigure {
122            args.push("-reconfigure".to_string());
123        }
124        if self.migrate_state {
125            args.push("-migrate-state".to_string());
126        }
127        if let Some(ref dir) = self.plugin_dir {
128            args.push(format!("-plugin-dir={dir}"));
129        }
130        if let Some(lock) = self.lock {
131            args.push(format!("-lock={lock}"));
132        }
133        if let Some(ref timeout) = self.lock_timeout {
134            args.push(format!("-lock-timeout={timeout}"));
135        }
136        args.extend(self.raw_args.clone());
137        args
138    }
139
140    async fn execute(&self, tf: &Terraform) -> Result<CommandOutput> {
141        let mut args = self.args();
142        if tf.no_input {
143            args.insert(1, "-input=false".to_string());
144        }
145        exec::run_terraform(tf, args).await
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn default_args() {
155        let cmd = InitCommand::new();
156        assert_eq!(cmd.args(), vec!["init"]);
157    }
158
159    #[test]
160    fn all_options() {
161        let cmd = InitCommand::new()
162            .backend_config("key", "value")
163            .backend_config_file("backend.hcl")
164            .upgrade()
165            .reconfigure()
166            .plugin_dir("/plugins")
167            .lock(false)
168            .lock_timeout("10s");
169        let args = cmd.args();
170        assert!(args.contains(&"-backend-config=key=value".to_string()));
171        assert!(args.contains(&"-backend-config=backend.hcl".to_string()));
172        assert!(args.contains(&"-upgrade".to_string()));
173        assert!(args.contains(&"-reconfigure".to_string()));
174        assert!(args.contains(&"-plugin-dir=/plugins".to_string()));
175        assert!(args.contains(&"-lock=false".to_string()));
176        assert!(args.contains(&"-lock-timeout=10s".to_string()));
177    }
178
179    #[test]
180    fn raw_arg_escape_hatch() {
181        let cmd = InitCommand::new().arg("-from-module=./staging");
182        let args = cmd.args();
183        assert!(args.contains(&"-from-module=./staging".to_string()));
184    }
185}