1use crate::instance_spec::{InstanceSpec, ReviewChangeSet, TemplateUploader};
2use crate::types::{ParameterKey, ParameterMap, ParameterValue};
3use sha2::Digest;
4
5pub struct BinaryName(pub String);
6pub struct BuildTarget(pub String);
7pub struct S3ObjectKey(pub String);
8
9impl From<&S3ObjectKey> for String {
10 fn from(value: &S3ObjectKey) -> String {
11 value.0.clone()
12 }
13}
14
15#[derive(Clone)]
16pub struct S3BucketName(pub String);
17
18impl From<&S3BucketName> for String {
19 fn from(value: &S3BucketName) -> String {
20 value.0.clone()
21 }
22}
23
24pub enum BuildType {
25 Debug,
26 Release,
27}
28
29impl BuildType {
30 fn path(&self) -> &str {
31 match self {
32 Self::Debug => "debug",
33 Self::Release => "release",
34 }
35 }
36
37 fn args(&self) -> &[&str] {
38 match self {
39 Self::Debug => &[],
40 Self::Release => &["--release"],
41 }
42 }
43}
44
45pub enum S3BucketSource {
46 Static(S3BucketName),
47 StackOutput {
48 stack_name: crate::types::StackName,
49 output_key: crate::types::OutputKey,
50 },
51}
52
53pub struct ZipFile {
54 body: aws_sdk_s3::primitives::ByteStream,
55 s3_object_key: S3ObjectKey,
56}
57
58impl ZipFile {
59 pub async fn upload(
60 self,
61 s3: &aws_sdk_s3::client::Client,
62 s3_bucket_name: &S3BucketName,
63 ) -> ParameterValue {
64 if !self.object_exists(s3, s3_bucket_name).await {
65 s3.put_object()
66 .bucket(s3_bucket_name)
67 .key(&self.s3_object_key)
68 .body(self.body)
69 .send()
70 .await
71 .unwrap();
72 }
73
74 ParameterValue(self.s3_object_key.0.clone())
75 }
76
77 async fn object_exists(
78 &self,
79 s3: &aws_sdk_s3::client::Client,
80 s3_bucket_name: &S3BucketName,
81 ) -> bool {
82 let result = s3
83 .head_object()
84 .bucket(s3_bucket_name)
85 .key(&self.s3_object_key.0)
86 .send()
87 .await;
88
89 match result {
90 Err(error) => match error.into_service_error() {
91 aws_sdk_s3::operation::head_object::HeadObjectError::NotFound { .. } => false,
92 other => panic!("Unexpected head object error: {other:#?}"),
93 },
94 Ok(_) => true,
95 }
96 }
97}
98
99pub struct Target {
100 pub binary_name: BinaryName,
101 pub build_target: BuildTarget,
102 pub build_type: BuildType,
103 pub extra_files: std::collections::BTreeMap<std::path::PathBuf, std::path::PathBuf>,
104}
105
106impl Target {
107 #[must_use]
126 pub fn path(&self) -> std::path::PathBuf {
127 std::path::Path::new("./target")
128 .join(&self.build_target.0)
129 .join(self.build_type.path())
130 .join(&self.binary_name.0)
131 }
132
133 pub async fn build(&self) {
135 log::info!("Building lambda target");
136 cmd_proc::Command::new("cargo")
137 .arguments(["build", "--target", &self.build_target.0])
138 .arguments(self.build_type.args())
139 .status()
140 .await
141 .unwrap_or_else(|error| panic!("Failed to build lambda target: {error}"));
142 }
143
144 pub async fn build_zip(&self) -> ZipFile {
146 self.build().await;
147 self.generate_zip()
148 }
149
150 fn generate_zip(&self) -> ZipFile {
152 let path = self.path();
153 log::info!("Reading binary from: {}", path.display());
154
155 let binary = std::fs::read(path).unwrap();
156
157 log::info!("Compressing binary into zip");
158
159 let mut cursor: std::io::Cursor<Vec<u8>> = std::io::Cursor::new(vec![]);
160 let mut zip = zip::write::ZipWriter::new(&mut cursor);
161 zip.start_file::<&str, ()>("bootstrap", Self::zip_file_options(0o555))
162 .unwrap();
163 std::io::Write::write_all(&mut zip, binary.as_ref()).unwrap();
164
165 self.write_extra_files(&mut zip);
166
167 zip.finish().unwrap();
168
169 log::info!("Computing zip hash");
170 let body = cursor.into_inner();
171 let hash = hex::encode(sha2::Sha256::digest(&body).as_slice());
172 log::info!("Content hash: {hash}");
173
174 ZipFile {
175 body: aws_sdk_s3::primitives::ByteStream::from(body),
176 s3_object_key: S3ObjectKey(format!("{hash}.zip")),
177 }
178 }
179
180 fn write_extra_files<W: std::io::Write + std::io::Seek>(
181 &self,
182 zip: &mut zip::write::ZipWriter<W>,
183 ) {
184 for (host, target) in &self.extra_files {
185 zip.start_file_from_path(target, Self::zip_file_options(0o444))
186 .unwrap();
187
188 std::io::copy(&mut std::fs::File::open(host).unwrap(), zip).unwrap();
189 }
190 }
191
192 fn zip_file_options(unix_permissions: u32) -> zip::write::FileOptions<'static, ()> {
193 zip::write::FileOptions::default()
194 .unix_permissions(unix_permissions)
195 .last_modified_time(zip::DateTime::default())
199 }
200
201 pub async fn deploy_parameter_update(
202 s3: &aws_sdk_s3::client::Client,
203 cloudformation: &aws_sdk_cloudformation::Client,
204 s3_bucket_name: &S3BucketName,
205 instance_spec: &InstanceSpec,
206 parameter_key: &ParameterKey,
207 template_uploader: &Option<TemplateUploader<'_>>,
208 zip_file: ZipFile,
209 ) {
210 let parameter_value = zip_file.upload(s3, s3_bucket_name).await;
211
212 instance_spec
213 .context(cloudformation, template_uploader.as_ref())
214 .parameter_update(&ParameterMap(std::collections::BTreeMap::from([(
215 parameter_key.clone(),
216 parameter_value,
217 )])))
218 .await;
219 }
220
221 #[allow(clippy::too_many_arguments)]
222 pub async fn deploy_template_update(
223 s3: &aws_sdk_s3::client::Client,
224 cloudformation: &aws_sdk_cloudformation::Client,
225 s3_bucket_name: &S3BucketName,
226 instance_spec: &InstanceSpec,
227 review_change_set: &ReviewChangeSet,
228 parameter_key: &ParameterKey,
229 template_uploader: &Option<TemplateUploader<'_>>,
230 zip_file: ZipFile,
231 ) {
232 let parameter_value = zip_file.upload(s3, s3_bucket_name).await;
233
234 instance_spec
235 .context(cloudformation, template_uploader.as_ref())
236 .update(
237 review_change_set,
238 &ParameterMap(std::collections::BTreeMap::from([(
239 parameter_key.clone(),
240 parameter_value,
241 )])),
242 )
243 .await;
244 }
245}
246
247pub mod cli {
248 use crate::instance_spec::{Registry, ReviewChangeSet, TemplateUploader};
249 use crate::lambda::deploy::S3BucketSource;
250 use crate::types::{ParameterKey, ParameterMap, ParameterValue, StackName};
251
252 #[derive(Clone, Debug, Eq, PartialEq, clap::Parser)]
253 pub struct App {
254 #[clap(subcommand)]
255 command: Command,
256 }
257
258 impl App {
259 pub async fn run(&self, config: &'_ Config<'_>) {
260 self.command.run(config).await
261 }
262 }
263
264 pub struct Config<'a> {
265 pub cloudformation: &'a aws_sdk_cloudformation::client::Client,
266 pub parameter_key: ParameterKey,
267 pub registry: Registry,
268 pub s3: &'a aws_sdk_s3::client::Client,
269 pub s3_bucket_source: S3BucketSource,
270 pub target: crate::lambda::deploy::Target,
271 pub template_uploader: Option<&'a TemplateUploader<'a>>,
272 }
273
274 impl Config<'_> {
275 pub(crate) async fn build(&self) {
276 self.target.build().await
277 }
278
279 pub(crate) async fn upload(&self) -> ParameterValue {
280 let s3_bucket_name = self.load_s3_bucket_name().await;
281
282 let parameter_value = self
283 .target
284 .build_zip()
285 .await
286 .upload(self.s3, &s3_bucket_name)
287 .await;
288
289 log::info!("Lambda object key: {}", parameter_value.0);
290
291 parameter_value
292 }
293
294 pub(crate) async fn deploy_template(
295 &self,
296 stack_name: &StackName,
297 review_change_set: &ReviewChangeSet,
298 ) {
299 let instance_spec = self
300 .registry
301 .find(stack_name)
302 .expect("instance spec not registered");
303
304 let parameter_value = self.upload().await;
305
306 instance_spec
307 .context(self.cloudformation, self.template_uploader)
308 .sync(
309 review_change_set,
310 &ParameterMap(std::collections::BTreeMap::from([(
311 self.parameter_key.clone(),
312 parameter_value,
313 )])),
314 )
315 .await
316 }
317
318 pub(crate) async fn deploy_parameter(&self, stack_name: &StackName) {
319 let instance_spec = self
320 .registry
321 .find(stack_name)
322 .expect("instance spec not registered");
323
324 let parameter_value = self.upload().await;
325
326 instance_spec
327 .context(self.cloudformation, self.template_uploader)
328 .parameter_update(&ParameterMap(std::collections::BTreeMap::from([(
329 self.parameter_key.clone(),
330 parameter_value,
331 )])))
332 .await
333 }
334
335 async fn load_s3_bucket_name(&self) -> crate::lambda::deploy::S3BucketName {
336 match &self.s3_bucket_source {
337 S3BucketSource::StackOutput {
338 stack_name,
339 output_key,
340 } => crate::lambda::deploy::S3BucketName(
341 crate::stack::read_stack_output(self.cloudformation, stack_name, output_key)
342 .await,
343 ),
344 S3BucketSource::Static(s3_bucket_name) => s3_bucket_name.clone(),
345 }
346 }
347 }
348
349 #[derive(Clone, Debug, Eq, PartialEq, clap::Subcommand)]
350 pub enum Command {
351 DeployTemplate {
353 #[arg(long = "stack-name")]
355 name: StackName,
356 #[arg(long, default_value = "interactive")]
357 review_change_set: ReviewChangeSet,
358 },
359 DeployParameter {
361 #[arg(long = "stack-name")]
363 name: StackName,
364 },
365 Build,
367 Upload,
369 }
370
371 impl Command {
372 pub async fn run(&self, config: &'_ Config<'_>) {
373 match self {
374 Self::Build => config.build().await,
375 Self::Upload => {
376 config.upload().await;
377 }
378 Self::DeployTemplate {
379 name,
380 review_change_set,
381 } => config.deploy_template(name, review_change_set).await,
382 Self::DeployParameter { name } => config.deploy_parameter(name).await,
383 }
384 }
385 }
386}