dagger-cuddle-please 0.2.0

A set of components for running cuddle-please in dagger
Documentation
use std::sync::Arc;

use models::{CuddlePleaseArgs, CuddlePleaseSrcArgs};
use traits::CuddlePlease;

use crate::models::{LogLevel, Server, SrcServer};

pub mod models {
    #[derive(Clone, Debug)]
    pub struct CuddlePleaseArgs {
        /// Which repository, example: "dagger-components"
        pub repository: String,

        /// Whom owns the repository, example: "kjuulh"
        pub owner: String,

        /// which branch to fetch, example: "main"
        pub branch: String,

        /// Which image to use for cuddle-please, example: "docker.io/kasperhermansen/cuddle-please:latest"
        pub cuddle_image: String,

        /// Which server to use, see inner field for more docs
        pub server: Server,

        /// Which log level to output for cuddle-please. Defaults to INFO
        pub log_level: Option<LogLevel>,

        /// Whether to include an ssh socket, if not set will default to the token set in server and http/https depending on the server
        pub use_ssh_socket: bool,
    }
    #[derive(Clone, Debug)]
    pub struct CuddlePleaseSrcArgs {
        /// Which image to use for cuddle-please, example: "docker.io/kasperhermansen/cuddle-please:latest"
        pub cuddle_image: String,

        /// Which log level to output for cuddle-please. Defaults to INFO
        pub log_level: Option<LogLevel>,

        /// Which server to use, see inner field for more docs
        pub server: SrcServer,
    }

    #[derive(Clone, Debug)]
    pub enum LogLevel {
        Trace,
        Debug,
        Info,
        Warn,
        Error,
    }

    #[derive(Clone, Debug)]
    pub enum Server {
        Gitea {
            url: String,
            user: String,
            token: String,
            insecure: Option<bool>,
        },
        GitHub {
            token: String,
        },
    }

    #[derive(Clone, Debug)]
    pub enum SrcServer {
        Gitea { token: String },
        GitHub { token: String },
    }
}

pub mod traits {
    use crate::models::{CuddlePleaseArgs, CuddlePleaseSrcArgs};

    #[async_trait::async_trait]
    pub trait CuddlePlease {
        async fn execute(&self, args: &CuddlePleaseArgs) -> eyre::Result<()>;
        async fn execute_src(&self, args: &CuddlePleaseSrcArgs) -> eyre::Result<()>;
    }
}

pub struct DaggerCuddlePleaseAction(Arc<dyn CuddlePlease + Send + Sync + 'static>);

impl DaggerCuddlePleaseAction {
    /// Create a [`traits::CuddlePlease`] client based on dagger
    pub fn dagger(client: Arc<dagger_sdk::Query>) -> Self {
        Self(Arc::new(DaggerCuddlePlease::new(client)))
    }

    /// Executes the cuddle-please action. This is a slow operation, but async, so make sure to not spawn blocking
    /// This relies exclively on arguments, as such this pull the repository directly, instead of using local src
    pub async fn execute(&self, args: &CuddlePleaseArgs) -> eyre::Result<()> {
        self.0.execute(args).await
    }

    /// Executes the cuddle-please action. This is a slow operation, but async, so make sure to not spawn blocking
    /// This function relies exclusively on cuddle.yaml and cuddle.please.yaml
    /// For private repository access, make sure to use [`CuddlePleaseSrcArgs::use_ssh_agent`] otherwise we may not able to push to your repository
    pub async fn execute_src(&self, args: &CuddlePleaseSrcArgs) -> eyre::Result<()> {
        self.0.execute_src(args).await
    }
}

#[derive(Clone)]
struct DaggerCuddlePlease {
    client: Arc<dagger_sdk::Query>,
}

#[async_trait::async_trait]
impl CuddlePlease for DaggerCuddlePlease {
    async fn execute(&self, args: &CuddlePleaseArgs) -> eyre::Result<()> {
        self.cuddle_please(self.client.clone(), args).await
    }

    async fn execute_src(&self, args: &CuddlePleaseSrcArgs) -> eyre::Result<()> {
        self.cuddle_please_src(self.client.clone(), args).await
    }
}

impl DaggerCuddlePlease {
    pub fn new(client: Arc<dagger_sdk::Query>) -> Self {
        Self { client }
    }

    pub async fn cuddle_please(
        &self,
        client: Arc<dagger_sdk::Query>,
        args: &CuddlePleaseArgs,
    ) -> eyre::Result<()> {
        let build_image = client.container().from(&args.cuddle_image);

        let repo_url = match &args.server {
            Server::Gitea {
                url,
                user,
                token,
                insecure,
            } => {
                if args.use_ssh_socket {
                    format!(
                        "ssh://{}:{}@{}/{}/{}",
                        user, token, url, &args.owner, &args.repository
                    )
                } else {
                    format!(
                        "{}://{}:{}@{}/{}/{}",
                        if insecure.unwrap_or(false) {
                            "http"
                        } else {
                            "https"
                        },
                        user,
                        token,
                        url,
                        &args.owner,
                        &args.repository
                    )
                }
            }
            Server::GitHub { .. } => {
                if args.use_ssh_socket {
                    format!("git@github.com:{}/{}", &args.owner, &args.repository)
                } else {
                    format!("https://github.com/{}/{}", &args.owner, &args.repository)
                }
            }
        };
        let api_url = match &args.server {
            Server::Gitea { url, insecure, .. } => {
                format!(
                    "{}://{}",
                    if insecure.unwrap_or(false) {
                        "http"
                    } else {
                        "https"
                    },
                    url,
                )
            }
            Server::GitHub { .. } => "https://github.com".into(),
        };

        let src = if args.use_ssh_socket {
            let socket = client
                .host()
                .unix_socket(std::env::var("SSH_AGENT").expect("SSH_AGENT to be set"));

            client
                .git_opts(
                    &repo_url,
                    dagger_sdk::QueryGitOpts {
                        experimental_service_host: None,
                        keep_git_dir: Some(true),
                    },
                )
                .branch("main")
                .tree_opts(dagger_sdk::GitRefTreeOpts {
                    ssh_auth_socket: Some(socket.id().await?),
                    ssh_known_hosts: None,
                })
        } else {
            client
                .git_opts(
                    &repo_url,
                    dagger_sdk::QueryGitOpts {
                        experimental_service_host: None,
                        keep_git_dir: Some(true),
                    },
                )
                .branch("main")
                .tree()
        };

        let res = build_image
            .with_secret_variable(
                "CUDDLE_PLEASE_TOKEN",
                client
                    .set_secret(
                        "CUDDLE_PLEASE_TOKEN",
                        match &args.server {
                            Server::Gitea { token, .. } => token,
                            Server::GitHub { token } => token,
                        },
                    )
                    .id()
                    .await?,
            )
            .with_workdir("/mnt/app")
            .with_directory(".", src.id().await?)
            .with_exec(vec!["git", "remote", "set-url", "origin", &repo_url])
            .with_exec(vec![
                "cuddle-please",
                "release",
                &format!(
                    "--engine={}",
                    match &args.server {
                        Server::Gitea { .. } => "gitea",
                        Server::GitHub { .. } => "github",
                    }
                ),
                "--owner",
                &args.owner,
                "--repo",
                &args.repository,
                "--branch",
                &args.branch,
                "--api-url",
                &api_url,
                "--log-level",
                match args.log_level.as_ref().unwrap_or(&LogLevel::Info) {
                    LogLevel::Trace => "trace",
                    LogLevel::Debug => "debug",
                    LogLevel::Info => "info",
                    LogLevel::Warn => "warn",
                    LogLevel::Error => "error",
                },
            ]);

        let exit_code = res.exit_code().await?;
        if exit_code != 0 {
            eyre::bail!("failed to run cuddle-please");
        }

        let please_out = res.stdout().await?;
        println!("{please_out}");
        let please_out = res.stderr().await?;
        println!("{please_out}");

        Ok(())
    }
    pub async fn cuddle_please_src(
        &self,
        client: Arc<dagger_sdk::Query>,
        args: &CuddlePleaseSrcArgs,
    ) -> eyre::Result<()> {
        let build_image = client.container().from(&args.cuddle_image);
        let res = build_image
            .with_secret_variable(
                "CUDDLE_PLEASE_TOKEN",
                client
                    .set_secret(
                        "CUDDLE_PLEASE_TOKEN",
                        match &args.server {
                            SrcServer::Gitea { token, .. } => token,
                            SrcServer::GitHub { token } => token,
                        },
                    )
                    .id()
                    .await?,
            )
            .with_workdir("/mnt/app")
            .with_directory(".", client.host().directory(".").id().await?)
            .with_unix_socket(
                "/tmp/ssh.sock",
                client
                    .host()
                    .unix_socket(
                        std::env::var("SSH_AUTH_SOCK").expect("expect SSH_AUTH_SOCK to be present"),
                    )
                    .id()
                    .await?,
            )
            .with_env_variable("SSH_AUTH_SOCK", "/tmp/ssh.sock")
            .with_new_file_opts(
                "/root/.ssh/config",
                dagger_sdk::ContainerWithNewFileOpts {
                    contents: Some(
                        "
Host *
    User git
    StrictHostKeyChecking no
    UserKnownHostsFile /dev/null
                ",
                    ),
                    owner: Some("root"),
                    permissions: Some(700),
                },
            );

        let remote_url = res
            .with_exec(vec!["git", "config", "--get", "remote.origin.url"])
            .stdout()
            .await?;

        let res = if remote_url.starts_with("http") {
            let new_remote_url = format!(
                "ssh://git@{}",
                remote_url
                    .trim()
                    .trim_start_matches("https://")
                    .trim_start_matches("http://")
            );
            println!("new remote_url: {}", new_remote_url);
            res.with_exec(vec!["git", "remote", "set-url", "origin", &new_remote_url])
        } else {
            res
        };

        let res = res.with_exec(vec![
            "cuddle-please",
            "release",
            &format!(
                "--engine={}",
                match &args.server {
                    SrcServer::Gitea { .. } => "gitea",
                    SrcServer::GitHub { .. } => "github",
                }
            ),
            "--log-level",
            match args.log_level.as_ref().unwrap_or(&LogLevel::Info) {
                LogLevel::Trace => "trace",
                LogLevel::Debug => "debug",
                LogLevel::Info => "info",
                LogLevel::Warn => "warn",
                LogLevel::Error => "error",
            },
        ]);

        let exit_code = res.exit_code().await?;
        if exit_code != 0 {
            eyre::bail!("failed to run cuddle-please");
        }

        let please_out = res.stdout().await?;
        println!("{please_out}");
        let please_out = res.stderr().await?;
        println!("{please_out}");

        Ok(())
    }
}