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: Option<bool>,
24    backend_configs: Vec<(String, String)>,
25    backend_config_files: Vec<String>,
26    from_module: Option<String>,
27    get: Option<bool>,
28    upgrade: bool,
29    reconfigure: bool,
30    migrate_state: bool,
31    plugin_dir: Option<String>,
32    lockfile: Option<String>,
33    lock: Option<bool>,
34    lock_timeout: Option<String>,
35    json: bool,
36    raw_args: Vec<String>,
37}
38
39impl InitCommand {
40    /// Create a new init command with default options.
41    #[must_use]
42    pub fn new() -> Self {
43        Self::default()
44    }
45
46    /// Enable or disable backend initialization (`-backend`).
47    #[must_use]
48    pub fn backend(mut self, enabled: bool) -> Self {
49        self.backend = Some(enabled);
50        self
51    }
52
53    /// Add a backend configuration key-value pair (`-backend-config=key=value`).
54    #[must_use]
55    pub fn backend_config(mut self, key: &str, value: &str) -> Self {
56        self.backend_configs
57            .push((key.to_string(), value.to_string()));
58        self
59    }
60
61    /// Add a backend configuration file (`-backend-config=<path>`).
62    #[must_use]
63    pub fn backend_config_file(mut self, path: &str) -> Self {
64        self.backend_config_files.push(path.to_string());
65        self
66    }
67
68    /// Copy the contents of the given module into the target directory (`-from-module=SOURCE`).
69    #[must_use]
70    pub fn from_module(mut self, source: &str) -> Self {
71        self.from_module = Some(source.to_string());
72        self
73    }
74
75    /// Enable or disable downloading modules for this configuration (`-get`).
76    #[must_use]
77    pub fn get(mut self, enabled: bool) -> Self {
78        self.get = Some(enabled);
79        self
80    }
81
82    /// Update modules and plugins to the latest allowed versions (`-upgrade`).
83    #[must_use]
84    pub fn upgrade(mut self) -> Self {
85        self.upgrade = true;
86        self
87    }
88
89    /// Reconfigure backend, ignoring any saved configuration (`-reconfigure`).
90    #[must_use]
91    pub fn reconfigure(mut self) -> Self {
92        self.reconfigure = true;
93        self
94    }
95
96    /// Reconfigure backend and attempt to migrate state (`-migrate-state`).
97    #[must_use]
98    pub fn migrate_state(mut self) -> Self {
99        self.migrate_state = true;
100        self
101    }
102
103    /// Directory to search for provider plugins (`-plugin-dir`).
104    #[must_use]
105    pub fn plugin_dir(mut self, path: &str) -> Self {
106        self.plugin_dir = Some(path.to_string());
107        self
108    }
109
110    /// Set the lockfile mode (`-lockfile=MODE`), e.g. `"readonly"`.
111    #[must_use]
112    pub fn lockfile(mut self, mode: &str) -> Self {
113        self.lockfile = Some(mode.to_string());
114        self
115    }
116
117    /// Enable or disable state locking (`-lock`).
118    #[must_use]
119    pub fn lock(mut self, enabled: bool) -> Self {
120        self.lock = Some(enabled);
121        self
122    }
123
124    /// Duration to wait for state lock (`-lock-timeout`).
125    #[must_use]
126    pub fn lock_timeout(mut self, timeout: &str) -> Self {
127        self.lock_timeout = Some(timeout.to_string());
128        self
129    }
130
131    /// Produce output in a machine-readable JSON format (`-json`).
132    #[must_use]
133    pub fn json(mut self) -> Self {
134        self.json = true;
135        self
136    }
137
138    /// Add a raw argument (escape hatch for unsupported options).
139    #[must_use]
140    pub fn arg(mut self, arg: impl Into<String>) -> Self {
141        self.raw_args.push(arg.into());
142        self
143    }
144}
145
146impl TerraformCommand for InitCommand {
147    type Output = CommandOutput;
148
149    fn args(&self) -> Vec<String> {
150        let mut args = vec!["init".to_string()];
151
152        if let Some(backend) = self.backend {
153            args.push(format!("-backend={backend}"));
154        }
155        for (key, value) in &self.backend_configs {
156            args.push(format!("-backend-config={key}={value}"));
157        }
158        for file in &self.backend_config_files {
159            args.push(format!("-backend-config={file}"));
160        }
161        if let Some(ref source) = self.from_module {
162            args.push(format!("-from-module={source}"));
163        }
164        if let Some(get) = self.get {
165            args.push(format!("-get={get}"));
166        }
167        if self.upgrade {
168            args.push("-upgrade".to_string());
169        }
170        if self.reconfigure {
171            args.push("-reconfigure".to_string());
172        }
173        if self.migrate_state {
174            args.push("-migrate-state".to_string());
175        }
176        if let Some(ref dir) = self.plugin_dir {
177            args.push(format!("-plugin-dir={dir}"));
178        }
179        if let Some(ref mode) = self.lockfile {
180            args.push(format!("-lockfile={mode}"));
181        }
182        if let Some(lock) = self.lock {
183            args.push(format!("-lock={lock}"));
184        }
185        if let Some(ref timeout) = self.lock_timeout {
186            args.push(format!("-lock-timeout={timeout}"));
187        }
188        if self.json {
189            args.push("-json".to_string());
190        }
191        args.extend(self.raw_args.clone());
192        args
193    }
194
195    fn supports_input(&self) -> bool {
196        true
197    }
198
199    async fn execute(&self, tf: &Terraform) -> Result<CommandOutput> {
200        exec::run_terraform(tf, self.prepare_args(tf)).await
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn default_args() {
210        let cmd = InitCommand::new();
211        assert_eq!(cmd.args(), vec!["init"]);
212    }
213
214    #[test]
215    fn all_options() {
216        let cmd = InitCommand::new()
217            .backend(false)
218            .backend_config("key", "value")
219            .backend_config_file("backend.hcl")
220            .from_module("./staging")
221            .get(false)
222            .upgrade()
223            .reconfigure()
224            .plugin_dir("/plugins")
225            .lockfile("readonly")
226            .lock(false)
227            .lock_timeout("10s")
228            .json();
229        let args = cmd.args();
230        assert!(args.contains(&"-backend=false".to_string()));
231        assert!(args.contains(&"-backend-config=key=value".to_string()));
232        assert!(args.contains(&"-backend-config=backend.hcl".to_string()));
233        assert!(args.contains(&"-from-module=./staging".to_string()));
234        assert!(args.contains(&"-get=false".to_string()));
235        assert!(args.contains(&"-upgrade".to_string()));
236        assert!(args.contains(&"-reconfigure".to_string()));
237        assert!(args.contains(&"-plugin-dir=/plugins".to_string()));
238        assert!(args.contains(&"-lockfile=readonly".to_string()));
239        assert!(args.contains(&"-lock=false".to_string()));
240        assert!(args.contains(&"-lock-timeout=10s".to_string()));
241        assert!(args.contains(&"-json".to_string()));
242    }
243
244    #[test]
245    fn backend_disabled() {
246        let cmd = InitCommand::new().backend(false);
247        assert!(cmd.args().contains(&"-backend=false".to_string()));
248    }
249
250    #[test]
251    fn from_module_source() {
252        let cmd = InitCommand::new().from_module("./staging");
253        assert!(cmd.args().contains(&"-from-module=./staging".to_string()));
254    }
255
256    #[test]
257    fn get_disabled() {
258        let cmd = InitCommand::new().get(false);
259        assert!(cmd.args().contains(&"-get=false".to_string()));
260    }
261
262    #[test]
263    fn lockfile_readonly() {
264        let cmd = InitCommand::new().lockfile("readonly");
265        assert!(cmd.args().contains(&"-lockfile=readonly".to_string()));
266    }
267
268    #[test]
269    fn json_output() {
270        let cmd = InitCommand::new().json();
271        assert!(cmd.args().contains(&"-json".to_string()));
272    }
273
274    #[test]
275    fn raw_arg_escape_hatch() {
276        let cmd = InitCommand::new().arg("-no-color");
277        let args = cmd.args();
278        assert!(args.contains(&"-no-color".to_string()));
279    }
280}