1use super::StageContributor;
9use crate::ir::{BuildStage, IntermediateRepresentation, StageTask};
10use cuenv_core::environment::{Env, EnvValue, EnvValueSimple};
11use cuenv_core::manifest::Project;
12use std::collections::HashMap;
13
14#[derive(Debug, Clone, Copy, Default)]
22pub struct OnePasswordContributor;
23
24impl OnePasswordContributor {
25 fn environment_has_onepassword_refs(env: &Env, environment_name: &str) -> bool {
27 let env_vars = env.for_environment(environment_name);
28 env_vars.values().any(|value| match value {
29 EnvValue::String(s) => s.starts_with("op://"),
30 EnvValue::Secret(secret) => secret.resolver == "onepassword",
31 EnvValue::WithPolicies(with_policies) => match &with_policies.value {
32 EnvValueSimple::Secret(secret) => secret.resolver == "onepassword",
33 EnvValueSimple::String(s) => s.starts_with("op://"),
34 _ => false,
35 },
36 _ => false,
37 })
38 }
39}
40
41impl StageContributor for OnePasswordContributor {
42 fn id(&self) -> &'static str {
43 "1password"
44 }
45
46 fn is_active(&self, ir: &IntermediateRepresentation, project: &Project) -> bool {
47 let Some(env_name) = &ir.pipeline.environment else {
49 return false;
50 };
51 let Some(env) = &project.env else {
52 return false;
53 };
54 Self::environment_has_onepassword_refs(env, env_name)
55 }
56
57 fn contribute(
58 &self,
59 ir: &IntermediateRepresentation,
60 _project: &Project,
61 ) -> (Vec<(BuildStage, StageTask)>, bool) {
62 if ir.stages.setup.iter().any(|t| t.id == "setup-1password") {
64 return (vec![], false);
65 }
66
67 let mut env = HashMap::new();
68 env.insert(
69 "OP_SERVICE_ACCOUNT_TOKEN".to_string(),
70 "${OP_SERVICE_ACCOUNT_TOKEN}".to_string(),
71 );
72
73 (
74 vec![(
75 BuildStage::Setup,
76 StageTask {
77 id: "setup-1password".to_string(),
78 provider: "1password".to_string(),
79 label: Some("Setup 1Password".to_string()),
80 command: vec!["cuenv secrets setup onepassword".to_string()],
81 shell: false,
82 env,
83 depends_on: vec!["setup-cuenv".to_string()],
85 priority: 20,
86 ..Default::default()
87 },
88 )],
89 true,
90 )
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::ir::{PipelineMetadata, StageConfiguration};
98 use cuenv_core::secrets::Secret;
99
100 fn make_ir_with_production_env() -> IntermediateRepresentation {
102 IntermediateRepresentation {
103 version: "1.4".to_string(),
104 pipeline: PipelineMetadata {
105 name: "test".to_string(),
106 environment: Some("production".to_string()),
107 requires_onepassword: false, project_name: None,
109 trigger: None,
110 },
111 runtimes: vec![],
112 stages: StageConfiguration::default(),
113 tasks: vec![],
114 }
115 }
116
117 fn make_ir_without_env() -> IntermediateRepresentation {
119 IntermediateRepresentation {
120 version: "1.4".to_string(),
121 pipeline: PipelineMetadata {
122 name: "test".to_string(),
123 environment: None,
124 requires_onepassword: false,
125 project_name: None,
126 trigger: None,
127 },
128 runtimes: vec![],
129 stages: StageConfiguration::default(),
130 tasks: vec![],
131 }
132 }
133
134 fn make_project_with_onepassword_env() -> Project {
136 let mut env_overrides = HashMap::new();
137 let mut production_env = HashMap::new();
138 production_env.insert(
139 "CLOUDFLARE_API_TOKEN".to_string(),
140 EnvValue::Secret(Secret::onepassword("op://vault/item/field")),
141 );
142 env_overrides.insert("production".to_string(), production_env);
143
144 Project {
145 name: "test".to_string(),
146 env: Some(Env {
147 base: HashMap::new(),
148 environment: Some(env_overrides),
149 }),
150 ..Default::default()
151 }
152 }
153
154 fn make_project_with_op_uri() -> Project {
156 let mut env_overrides = HashMap::new();
157 let mut production_env = HashMap::new();
158 production_env.insert(
159 "API_TOKEN".to_string(),
160 EnvValue::String("op://vault/item/field".to_string()),
161 );
162 env_overrides.insert("production".to_string(), production_env);
163
164 Project {
165 name: "test".to_string(),
166 env: Some(Env {
167 base: HashMap::new(),
168 environment: Some(env_overrides),
169 }),
170 ..Default::default()
171 }
172 }
173
174 fn make_project_without_onepassword() -> Project {
176 let mut env_overrides = HashMap::new();
177 let mut production_env = HashMap::new();
178 production_env.insert(
179 "SOME_VAR".to_string(),
180 EnvValue::String("value".to_string()),
181 );
182 env_overrides.insert("production".to_string(), production_env);
183
184 Project {
185 name: "test".to_string(),
186 env: Some(Env {
187 base: HashMap::new(),
188 environment: Some(env_overrides),
189 }),
190 ..Default::default()
191 }
192 }
193
194 fn make_project_without_env() -> Project {
196 Project {
197 name: "test".to_string(),
198 ..Default::default()
199 }
200 }
201
202 #[test]
203 fn test_is_active_with_onepassword_secret() {
204 let contributor = OnePasswordContributor;
205 let ir = make_ir_with_production_env();
206 let project = make_project_with_onepassword_env();
207
208 assert!(contributor.is_active(&ir, &project));
209 }
210
211 #[test]
212 fn test_is_active_with_op_uri() {
213 let contributor = OnePasswordContributor;
214 let ir = make_ir_with_production_env();
215 let project = make_project_with_op_uri();
216
217 assert!(contributor.is_active(&ir, &project));
218 }
219
220 #[test]
221 fn test_is_inactive_without_onepassword() {
222 let contributor = OnePasswordContributor;
223 let ir = make_ir_with_production_env();
224 let project = make_project_without_onepassword();
225
226 assert!(!contributor.is_active(&ir, &project));
227 }
228
229 #[test]
230 fn test_is_inactive_without_environment() {
231 let contributor = OnePasswordContributor;
232 let ir = make_ir_without_env();
233 let project = make_project_with_onepassword_env();
234
235 assert!(!contributor.is_active(&ir, &project));
237 }
238
239 #[test]
240 fn test_is_inactive_without_project_env() {
241 let contributor = OnePasswordContributor;
242 let ir = make_ir_with_production_env();
243 let project = make_project_without_env();
244
245 assert!(!contributor.is_active(&ir, &project));
247 }
248
249 #[test]
250 fn test_contribute_returns_setup_task() {
251 let contributor = OnePasswordContributor;
252 let ir = make_ir_with_production_env();
253 let project = make_project_with_onepassword_env();
254
255 let (contributions, modified) = contributor.contribute(&ir, &project);
256
257 assert!(modified);
258 assert_eq!(contributions.len(), 1);
259
260 let (stage, task) = &contributions[0];
261 assert_eq!(*stage, BuildStage::Setup);
262 assert_eq!(task.id, "setup-1password");
263 assert_eq!(task.provider, "1password");
264 assert_eq!(task.priority, 20);
265 }
266
267 #[test]
268 fn test_contribute_sets_env_var() {
269 let contributor = OnePasswordContributor;
270 let ir = make_ir_with_production_env();
271 let project = make_project_with_onepassword_env();
272
273 let (contributions, _) = contributor.contribute(&ir, &project);
274 let (_, task) = &contributions[0];
275
276 assert!(task.env.contains_key("OP_SERVICE_ACCOUNT_TOKEN"));
277 assert_eq!(
278 task.env.get("OP_SERVICE_ACCOUNT_TOKEN").unwrap(),
279 "${OP_SERVICE_ACCOUNT_TOKEN}"
280 );
281 }
282
283 #[test]
284 fn test_contribute_runs_setup_command() {
285 let contributor = OnePasswordContributor;
286 let ir = make_ir_with_production_env();
287 let project = make_project_with_onepassword_env();
288
289 let (contributions, _) = contributor.contribute(&ir, &project);
290 let (_, task) = &contributions[0];
291
292 assert_eq!(task.command.len(), 1);
293 assert_eq!(task.command[0], "cuenv secrets setup onepassword");
294 }
295
296 #[test]
297 fn test_contribute_depends_on_setup_cuenv() {
298 let contributor = OnePasswordContributor;
299 let ir = make_ir_with_production_env();
300 let project = make_project_with_onepassword_env();
301
302 let (contributions, _) = contributor.contribute(&ir, &project);
303 let (_, task) = &contributions[0];
304
305 assert!(task.depends_on.contains(&"setup-cuenv".to_string()));
306 }
307
308 #[test]
309 fn test_contribute_is_idempotent() {
310 let contributor = OnePasswordContributor;
311 let mut ir = make_ir_with_production_env();
312 let project = make_project_with_onepassword_env();
313
314 let (contributions, modified) = contributor.contribute(&ir, &project);
316 assert!(modified);
317 assert_eq!(contributions.len(), 1);
318
319 for (stage, task) in contributions {
321 ir.stages.add(stage, task);
322 }
323
324 let (contributions, modified) = contributor.contribute(&ir, &project);
326 assert!(!modified);
327 assert!(contributions.is_empty());
328 }
329}