bular 0.0.2

CLI for managing Bular deployments
use std::{
    collections::HashMap,
    fs::File,
    io::{Cursor, Write},
    path::PathBuf,
    time::Duration,
};

use aws_sdk_iam::primitives::Blob;
use aws_sdk_lambda::types::{Environment, FunctionCode, FunctionUrlAuthType, InvokeMode};
use rand::{distributions::Alphanumeric, Rng};
use zip::{write::SimpleFileOptions, ZipWriter};

use crate::manifest::Entrypoint;

// TODO: Really build and deploy needs to be intertwined due to how the compute platform dictates the build process (Eg. target)

pub async fn deploy(
    name: &str,
    entrypoint: &Entrypoint,
    binary_path: PathBuf,
    iam: &aws_sdk_iam::Client,
    lambda: &aws_sdk_lambda::Client,
    computed_env: HashMap<String, String>,
) -> (String, String) {
    // TODO: Reusing the same Lambda by using aliases
    let hash: String = rand::thread_rng()
        .sample_iter(&Alphanumeric)
        .take(7)
        .map(char::from)
        .collect();
    let fn_name = format!("{}-{}", name, hash);

    let mut zip_file = Cursor::new(vec![]);
    {
        let mut zip = ZipWriter::new(&mut zip_file); // TODO: Configure compression???

        zip.start_file("bootstrap", SimpleFileOptions::default())
            .unwrap();
        let mut binary = File::open(&binary_path).unwrap();
        std::io::copy(&mut binary, &mut zip).unwrap();

        zip.finish().unwrap();
        zip_file.flush().unwrap();
    }

    let code = FunctionCode::builder()
        .zip_file(Blob::new(zip_file.into_inner()))
        .build();

    let role = iam
        .create_role()
        // TODO: Name must stay below 64 characters
        .role_name(format!("{fn_name}-role"))
        .assume_role_policy_document(ROLE_POLICY_DOCUMENT)
        .send()
        .await
        .unwrap();

    // TODO: This is mega cringe
    println!("Created iam role, waiting 5s for it to become active");
    tokio::time::sleep(Duration::from_secs(5)).await;

    let role = role.role().unwrap();

    let function_resp = lambda
        .create_function()
        .function_name(&fn_name)
        .description("Specta Start demo")
        .runtime(aws_sdk_lambda::types::Runtime::Providedal2023)
        .code(code)
        .role(role.arn())
        .handler("bootstrap")
        .environment(
            Environment::builder()
                .set_variables(Some(computed_env))
                .build(),
        )
        // .publish(input)
        // .tags(k, v)
        // .memory_size(input)
        // .ephemeral_storage(input)
        // .architectures(input)
        // TODO: Set RAM and storage, timeout, etc
        .send()
        .await
        .unwrap();

    println!(
        "Created function {fn_name} {} in {}",
        function_resp.function_arn().unwrap(),
        "us-east-1"
    );

    // {
    //     info!(
    //         ?environment,
    //         "Updating environment for {}", self.lambda_name
    //     );
    //     let updated = self
    //         .lambda_client
    //         .update_function_configuration()
    //         .function_name(self.lambda_name.clone())
    //         .environment(environment)
    //         .send()
    //         .await
    //         .map_err(anyhow::Error::from)?;
    // }

    // TODO: Can this have race conditions with multiple clients modifying it at once???
    // self.wait_for_function_ready().await?;

    // self.lambda_client
    //     .publish_version()
    //     .function_name(self.lambda_name.clone())
    //     .send()
    //     .await?;

    // lambda.create_alias().function_name(&fn_name).name("latest").send().await.unwrap();

    // TODO: Turn this off by default
    {
        // TODO: What happens in this runs multiple times, does it just add more or override the existing statement???
        lambda
            .add_permission()
            .function_name(&fn_name)
            .action("lambda:InvokeFunctionUrl")
            // TODO: Secure by default
            .function_url_auth_type(FunctionUrlAuthType::None)
            .principal("*")
            .statement_id("FunctionURLAllowPublicAccess")
            .send()
            .await
            .unwrap();

        let resp = lambda
            .create_function_url_config()
            .function_name(&fn_name)
            // TODO: Secure by default
            .auth_type(FunctionUrlAuthType::None)
            .invoke_mode(InvokeMode::Buffered)
            .send()
            .await
            .unwrap();

        println!("Created function URL for {fn_name}: {}", resp.function_url);
    }

    (function_resp.function_arn().unwrap().into(), "todo".into())
}

/** A policy document allowing Lambda to execute this function on the account's behalf. */
const ROLE_POLICY_DOCUMENT: &str = r#"{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": { "Service": "lambda.amazonaws.com" },
            "Action": "sts:AssumeRole"
        }
    ]
}"#;