Compare commits

..

No commits in common. "main" and "v0.1.0" have entirely different histories.
main ... v0.1.0

99 changed files with 3100 additions and 10610 deletions

View File

@ -1,2 +1,170 @@
kind: template kind: pipeline
load: cuddle-rust-lib-plan.yaml name: default
type: docker
steps:
- name: build ci
image: rustlang/rust:nightly
volumes:
- name: ci
path: /mnt/ci
environment:
PKG_CONFIG_SYSROOT_DIR: "/"
CI_PREFIX: "/mnt/ci"
commands:
- set -e
- apt update
- apt install musl-tools pkg-config libssl-dev openssl build-essential musl-dev -y
- rustup target add x86_64-unknown-linux-musl
- cargo build --target=x86_64-unknown-linux-musl -p ci
#- cargo build -p ci
- mv target/x86_64-unknown-linux-musl/debug/ci "$CI_PREFIX/ci"
#- mv target/debug/ci $CI_PREFIX/ci
- name: load_secret
image: debian:buster-slim
volumes:
- name: ssh
path: /root/.ssh/
environment:
SSH_KEY:
from_secret: gitea_id_ed25519
commands:
- mkdir -p $HOME/.ssh/
- echo "$SSH_KEY" | base64 -d > $HOME/.ssh/id_ed25519
- name: build pr
image: kasperhermansen/cuddle:latest
pull: always
volumes:
- name: ssh
path: /root/.ssh/
- name: dockersock
path: /var/run
- name: ci
path: /mnt/ci
commands:
- eval `ssh-agent`
- chmod -R 600 ~/.ssh
- ssh-add
- echo "$DOCKER_PASSWORD" | docker login --password-stdin --username="$DOCKER_USERNAME" docker.io
- ldd $CI_PREFIX
- apk add git
- cuddle x ci:pr
environment:
DOCKER_BUILDKIT: 1
DOCKER_PASSWORD:
from_secret: docker_password
DOCKER_USERNAME:
from_secret: docker_username
CUDDLE_SECRETS_PROVIDER: 1password
CUDDLE_ONE_PASSWORD_DOT_ENV: ".env.ci"
CUDDLE_SSH_AGENT: "true"
CI_PREFIX: "/mnt/ci/ci"
CUDDLE_PLEASE_TOKEN:
from_secret: cuddle_please_token
OP_SERVICE_ACCOUNT_TOKEN:
from_secret: op_service_account_token
when:
event:
- pull_request
exclude:
- main
- master
depends_on:
- "load_secret"
- "build ci"
- name: build main
image: kasperhermansen/cuddle:latest
pull: always
volumes:
- name: ssh
path: /root/.ssh/
- name: dockersock
path: /var/run
- name: ci
path: /mnt/ci
commands:
- eval `ssh-agent`
- chmod -R 600 ~/.ssh
- ssh-add
- echo "$DOCKER_PASSWORD" | docker login --password-stdin --username="$DOCKER_USERNAME" docker.io
- ldd $CI_PREFIX
- apk add git
- cuddle x ci:main
environment:
DOCKER_BUILDKIT: 1
DOCKER_PASSWORD:
from_secret: docker_password
DOCKER_USERNAME:
from_secret: docker_username
CUDDLE_SECRETS_PROVIDER: 1password
CUDDLE_ONE_PASSWORD_DOT_ENV: ".env.ci"
CUDDLE_SSH_AGENT: "true"
CI_PREFIX: "/mnt/ci/ci"
CUDDLE_PLEASE_TOKEN:
from_secret: cuddle_please_token
OP_SERVICE_ACCOUNT_TOKEN:
from_secret: op_service_account_token
when:
event:
- push
branch:
- main
- master
exclude:
- pull_request
depends_on:
- "load_secret"
- "build ci"
- name: deploy release
image: kasperhermansen/cuddle:latest
pull: always
volumes:
- name: ssh
path: /root/.ssh/
- name: dockersock
path: /var/run
commands:
- eval `ssh-agent`
- chmod -R 600 ~/.ssh
- ssh-add
- cuddle x build:release:all
- cuddle x deploy:docs:preview
environment:
DOCKER_BUILDKIT: 1
CUDDLE_SECRETS_PROVIDER: 1password
CUDDLE_ONE_PASSWORD_DOT_ENV: ".env.ci"
CUDDLE_SSH_AGENT: "true"
CUDDLE_CI: "true"
CUDDLE_PLEASE_TOKEN:
from_secret: cuddle_please_token
OP_SERVICE_ACCOUNT_TOKEN:
from_secret: op_service_account_token
when:
event:
- tag
ref:
include:
- refs/tags/v*
depends_on:
- "load_secret"
- "build ci"
services:
- name: docker
image: docker:dind
privileged: true
volumes:
- name: dockersock
path: /var/run
volumes:
- name: ssh
temp: {}
- name: dockersock
temp: {}
- name: ci
temp: {}

1
.gitignore vendored
View File

@ -1,4 +1,3 @@
/target /target
.env .env
.cuddle/ .cuddle/
target/

View File

@ -6,261 +6,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.3.0] - 2024-11-16
### Added
- with lib drone
- with rust something
- fix errors
- update dagger
- update
- without extra packages
- wrong exclude
- also exclude tests
- update dagger 0.11.10
- update dagger 0.11.7
- add empty cuddle please for now
- update
- update a lot
- update components on prs to also build release
- update version
- update image
- move to user local bin
- with alpine instead
- add permissions
- use version as well
- install curl
- trying again
- add dagger bin actually6 compiles
- add dagger bin
- add file
- match values first
- pretty print
- with debug
- return with sqlx
- add debug logs
- with single migrations
- handle for cuddle_file
- with cuddle release clone
- use stuff
- with as ref as well
- implement clone
- now with to owned as well
- use generics
- use cuddle please
- with rust workspace members
- update dagger
- fix cuddle_releaser
- make cloneable
- remove extra fluff
- add trace
- add image tag
- make sure to add values property
- update image
- add system time
- update drone templater
- include pipeline
- include sync
- use self.client
- with aborting
- fix errors
- fix errors
- create drone templater action
- make cuddle_releaser great again
- upgrade services to bookworm
- upgrade to bookworm
- with apt
- with logging
- with logging
- update leptos service
- rerun blabla
- with export
- with fix
- cargo update
- with ignore sub source as well
- add helm to kubectl
- add main
- add timestamp
- use tag instead
- use tag instead
- revert
- from cuddle file up
- remember to split output string
- with cuddle x render args
- with context
- with kubeslice
- with kubeslice
- add kubectl command
- trying spawn
- trying std::process instead
- stderr pipes
- with nonzero exit code
- with more logs
- update releaser
- disable ci for now
- add cuddle_cli
- add cuddle_cli
- with docker
- with apt before package
- all the logs
- all the logs
- all the logs
- all the logs
- all the logs
- all the logs
- with new image
- with new image
- with new image
- set rust log error
- add time str
- new image
- without logs
- try again
- with updated releaser
- with trace
- with output
- use proper releaser
- add auth sock
- with cuddle x render
- add deployment take 2
- add deployment
- add releaser
- conditionally disable deployment
- without home
- run before base
- set user
- with empty string
- with ingored host key checking
- with migrations as well
- with opinionated ssh auth sock fetch
- set env variable as well
- with ssh agent
- extract cuddle_please
- update image
- with cuddle please
- without new async
- with rust_lib
- with main as well
- with test with leptos
- add initial leptos
- add postgresql-dev
- without nodemodules
- with entry point
- with actual pr
- with pub fn new
- add node service
- trying again with opts
- without opts
- set registry
- move to after package
- with docker cache
- update assets
- with assets
- with package as well
- without deps
- with ca certificates
- with working ssh
- update with ssh
- with ssh sock dep
- can use ssh sock
- with username
- with git name
- with sync
- with update deployment
- with registry
- with before test
- with impl into
- with arc
- with &mut service
- with mutex
- with rust service impl
- with src
- with sqlx
- forgot async_trait
- with cargo clean
- with extensions
- extract arch
- with full support for rust services
- with middleware
- with logs
- add cuddle ci draft
- with offline mode
- with cargo clean
- without export
- with output
- with nested mold
- fix name
- with mold
- with mold
- with htmx
- add leptos
- ignore cache
- update lock
- with updated dagger-sdk
- *(rust-publish)* with rust publish
### Fixed
- *(deps)* update rust crate chrono to 0.4.38
- typo
- *(deps)* update all dependencies
- *(deps)* update rust crate chrono to 0.4.35
- *(deps)* update rust crate chrono to 0.4.34
- *(deps)* update all dependencies
- *(deps)* update rust crate futures to 0.3.30
- as isize
- actually build the builder
- build errors on ssh agent
- *(deps)* update rust crate async-scoped to 0.8.0
- *(deps)* update rust crate futures to 0.3.29
- *(git)* make sure we actually fail when running an invalid git command
### Other
- *(deps)* update rust crate serde to v1.0.215
- *(deps)* update rust crate serde to v1.0.214
- *(deps)* update rust crate serde to v1.0.213
- *(deps)* update rust crate serde to v1.0.210
- *(deps)* update rust crate serde to v1.0.209
- *(deps)* update rust crate serde to v1.0.208
- *(deps)* update rust crate serde to v1.0.203
- *(deps)* update rust crate async-trait to 0.1.80
- split module
- *(deps)* update rust crate async-trait to 0.1.79
- *(deps)* update rust crate async-trait to 0.1.78
- *(deps)* update rust crate tokio to 1.36.0
- *(deps)* update rust crate eyre to 0.6.12
- rename with_socket to with_ssh_agent
- fmt
- fmt
- *(deps)* update rust crate async-scoped to 0.8.0
- *(deps)* update rust crate futures to 0.3.29
- *(deps)* update rust crate eyre to 0.6.9
- *(deps)* update rust crate tokio to 1.34.0
- *(deps)* update all dependencies
- with version 0.2.0
- publish
- add noop release script
## [0.2.0] - 2023-08-12
### Added
- with rust build and test
- *(ci)* with internal please action
### Fixed
- *(ci)* only set local url instead of insteadOf
- *(ci)* trim remote_url newlines
- *(ci)* trim remote_url newlines
- *(ci)* repo should be ssh
- *(ci)* make sure to run ssh as user git
- ci
### Other
- set fixed versions
- *(deps)* update rust crate tokio to 1.31.0
- *(ci)* move cuddle please image to cuddle
- Add renovate.json
## [0.1.0] - 2023-08-11 ## [0.1.0] - 2023-08-11
### Added ### Added

1601
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,30 +1,18 @@
[workspace] [workspace]
members = ["crates/*", "examples/*"] members = [
"crates/*",
"examples/*",
"ci"
]
resolver = "2" resolver = "2"
[workspace.package]
version = "0.3.0"
edition = "2021"
license = "MIT"
authors = ["kjuulh <contact@kjuulh.io>"]
readme = "README.md"
repository = "https://git.front.kjuulh.io/kjuulh/dagger-components"
[workspace.dependencies] [workspace.dependencies]
cuddle-components = { path = "crates/cuddle-components" } cuddle-components = {path = "crates/cuddle-components"}
dagger-components = { path = "crates/dagger-components" } dagger-components = {path = "crates/dagger-components"}
dagger-cuddle-please = { path = "crates/dagger-cuddle-please" } dagger-cuddle-please = {path = "crates/dagger-cuddle-please"}
dagger-rust = { path = "crates/dagger-rust" } ci = {path = "ci"}
dagger-sdk = "0.13.7" dagger-sdk = "0.2.22"
eyre = "0.6" eyre = "0.6.8"
tokio = "1" tokio = "1.30.0"
dotenv = "0.15.0" dotenv = "*"
async-trait = "0.1"
color-eyre = "*"
clap = { version = "4", features = ["derive"] }
futures = "0.3"
async-scoped = { version = "0.9.0", features = ["tokio", "use-tokio"] }
serde_json = { version = "1" }
serde_yaml = { version = "0.9" }
serde = { version = "1", features = ["derive"] }

1861
ci/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

16
ci/Cargo.toml Normal file
View File

@ -0,0 +1,16 @@
[package]
name = "ci"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dagger-sdk = "*"
eyre = "*"
color-eyre = "*"
tokio = "1"
clap = {version = "4", features = ["derive"]}
futures = "0.3.28"
async-scoped = { version = "0.7.1", features = ["tokio", "use-tokio"] }
dotenv.workspace = true

380
ci/src/main.rs Normal file
View File

@ -0,0 +1,380 @@
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use clap::Args;
use clap::Parser;
use clap::Subcommand;
use clap::ValueEnum;
use crate::please_release::run_release_please;
#[derive(Parser, Clone)]
#[command(author, version, about, long_about = None, subcommand_required = true)]
pub struct Command {
#[command(subcommand)]
commands: Commands,
#[command(flatten)]
global: GlobalArgs,
}
#[derive(Subcommand, Clone)]
pub enum Commands {
#[command(subcommand_required = true)]
Local {
#[command(subcommand)]
command: LocalCommands,
},
PullRequest {},
Main {},
Release,
}
#[derive(Subcommand, Clone)]
pub enum LocalCommands {
Test,
PleaseRelease,
}
#[derive(Debug, Clone, ValueEnum)]
pub enum BuildProfile {
Debug,
Release,
}
#[derive(Debug, Clone, Args)]
pub struct GlobalArgs {
#[arg(long, global = true, help_heading = "Global")]
dry_run: bool,
#[arg(long, global = true, help_heading = "Global")]
rust_builder_image: Option<String>,
#[arg(long, global = true, help_heading = "Global")]
source: Option<PathBuf>,
}
#[tokio::main]
async fn main() -> eyre::Result<()> {
let _ = dotenv::dotenv();
let _ = color_eyre::install();
let client = dagger_sdk::connect().await?;
let cli = Command::parse();
match &cli.commands {
Commands::Local { command } => match command {
LocalCommands::Test => {
let base_image =
base_rust_image(client.clone(), &cli.global, &None, &"debug".into()).await?;
test::execute(client, &cli.global, base_image).await?;
}
LocalCommands::PleaseRelease => todo!(),
},
Commands::PullRequest {} => {
async fn test(client: Arc<dagger_sdk::Query>, cli: &Command) {
let args = &cli.global;
let base_image = base_rust_image(client.clone(), args, &None, &"debug".into())
.await
.unwrap();
test::execute(client.clone(), args, base_image)
.await
.unwrap();
}
tokio::join!(test(client.clone(), &cli),);
}
Commands::Main {} => {
async fn test(client: Arc<dagger_sdk::Query>, cli: &Command) {
let args = &cli.global;
let base_image = base_rust_image(client.clone(), args, &None, &"debug".into())
.await
.unwrap();
test::execute(client.clone(), args, base_image)
.await
.unwrap();
}
async fn cuddle_please(client: Arc<dagger_sdk::Query>, cli: &Command) {
run_release_please(client.clone(), &cli.global)
.await
.unwrap();
}
tokio::join!(
test(client.clone(), &cli),
cuddle_please(client.clone(), &cli)
);
}
Commands::Release => todo!(),
}
Ok(())
}
mod please_release {
use std::sync::Arc;
use crate::GlobalArgs;
pub async fn run_release_please(
client: Arc<dagger_sdk::Query>,
args: &GlobalArgs,
) -> eyre::Result<()> {
let build_image = client
.container()
.from("kasperhermansen/cuddle-please:main-1691504183");
let src = client
.git_opts(
"https://git.front.kjuulh.io/kjuulh/dagger-components",
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", std::env::var("CUDDLE_PLEASE_TOKEN")?)
.id()
.await?,
)
.with_workdir("/mnt/app")
.with_directory(".", src.id().await?)
.with_exec(vec![
"git",
"remote",
"set-url",
"origin",
&format!(
"https://git:{}@git.front.kjuulh.io/kjuulh/dagger-components.git",
std::env::var("CUDDLE_PLEASE_TOKEN")?
),
])
.with_exec(vec![
"cuddle-please",
"release",
"--engine=gitea",
"--owner=kjuulh",
"--repo=dagger-components",
"--branch=main",
"--api-url=https://git.front.kjuulh.io",
"--log-level=debug",
]);
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(())
}
}
mod test {
use std::sync::Arc;
use crate::GlobalArgs;
pub async fn execute(
_client: Arc<dagger_sdk::Query>,
_args: &GlobalArgs,
container: dagger_sdk::Container,
) -> eyre::Result<()> {
let test_image = container
.pipeline("rust:test")
.with_exec(vec!["apt", "update"])
.with_exec(vec!["apt", "install", "-y", "git"])
.with_exec(vec!["cargo", "test"]);
let please_out = test_image.stdout().await?;
println!("{please_out}");
let please_out = test_image.stderr().await?;
println!("{please_out}");
test_image.exit_code().await?;
Ok(())
}
}
pub fn get_src(
client: Arc<dagger_sdk::Query>,
args: &GlobalArgs,
) -> eyre::Result<dagger_sdk::Directory> {
let directory = client.host().directory_opts(
args.source
.clone()
.unwrap_or(PathBuf::from("."))
.display()
.to_string(),
dagger_sdk::HostDirectoryOptsBuilder::default()
.exclude(vec!["node_modules/", ".git/", "target/"])
.build()?,
);
Ok(directory)
}
pub async fn get_rust_dep_src(
client: Arc<dagger_sdk::Query>,
args: &GlobalArgs,
) -> eyre::Result<dagger_sdk::Directory> {
let directory = client.host().directory_opts(
args.source
.clone()
.unwrap_or(PathBuf::from("."))
.display()
.to_string(),
dagger_sdk::HostDirectoryOptsBuilder::default()
.include(vec!["**/Cargo.toml", "**/Cargo.lock"])
.build()?,
);
Ok(directory)
}
pub async fn get_rust_skeleton_files(
client: Arc<dagger_sdk::Query>,
_args: &GlobalArgs,
) -> eyre::Result<(dagger_sdk::Directory, Vec<String>)> {
let mut rust_crates = vec![PathBuf::from("ci")];
let mut dirs = tokio::fs::read_dir("crates").await?;
while let Some(entry) = dirs.next_entry().await? {
if entry.metadata().await?.is_dir() {
rust_crates.push(entry.path())
}
}
let mut dirs = tokio::fs::read_dir("examples").await?;
while let Some(entry) = dirs.next_entry().await? {
if entry.metadata().await?.is_dir() {
rust_crates.push(entry.path())
}
}
fn create_skeleton_files(
directory: dagger_sdk::Directory,
path: &Path,
) -> eyre::Result<dagger_sdk::Directory> {
println!("found crates: {}", path.display());
let main_content = r#"
#[allow(dead_code)]
fn main() { panic!("should never be executed"); }"#;
let lib_content = r#"
#[allow(dead_code)]
fn some() { panic!("should never be executed"); }"#;
let directory = directory.with_new_file(
path.join("src").join("main.rs").display().to_string(),
main_content,
);
let directory = directory.with_new_file(
path.join("src").join("lib.rs").display().to_string(),
lib_content,
);
Ok(directory)
}
let mut directory = client.directory();
let mut crate_names = Vec::new();
for rust_crate in rust_crates.iter() {
if let Some(file_name) = rust_crate.file_name() {
crate_names.push(file_name.to_str().unwrap().to_string());
}
directory = create_skeleton_files(directory, rust_crate)?;
}
Ok((directory, crate_names))
}
pub async fn base_rust_image(
client: Arc<dagger_sdk::Query>,
args: &GlobalArgs,
platform: &Option<String>,
profile: &String,
) -> eyre::Result<dagger_sdk::Container> {
let dep_src = get_rust_dep_src(client.clone(), args).await?;
let (skeleton_files, crates) = get_rust_skeleton_files(client.clone(), args).await?;
let src = get_src(client.clone(), args)?;
let client = client.pipeline("rust_base_image");
let rust_target = match platform
.clone()
.unwrap_or("linux/amd64".to_string())
.as_str()
{
"linux/amd64" => "x86_64-unknown-linux-gnu",
"linux/arm64" => "aarch64-unknown-linux-gnu",
_ => eyre::bail!("architecture not supported"),
};
let rust_build_image = client
.container()
.from(
args.rust_builder_image
.as_ref()
.unwrap_or(&"rustlang/rust:nightly".into()),
)
.with_exec(vec!["rustup", "target", "add", rust_target])
.with_exec(vec!["apt", "update"])
.with_exec(vec!["apt", "install", "-y", "jq"]);
let target_cache = client.cache_volume(format!("rust_target_{}", profile));
let mut build_options = vec!["cargo", "build", "--target", rust_target, "--workspace"];
if profile == "release" {
build_options.push("--release");
}
let rust_prebuild = rust_build_image
.with_workdir("/mnt/src")
.with_directory("/mnt/src", dep_src.id().await?)
.with_directory("/mnt/src/", skeleton_files.id().await?)
.with_exec(build_options)
.with_mounted_cache("/mnt/src/target/", target_cache.id().await?);
let exclude = crates
.iter()
.filter(|c| **c != "ci")
.map(|c| format!("**/*{}*", c.replace('-', "_")))
.collect::<Vec<_>>();
let exclude = exclude.iter().map(|c| c.as_str()).collect();
let incremental_dir = client.directory().with_directory_opts(
".",
rust_prebuild.directory("target").id().await?,
dagger_sdk::DirectoryWithDirectoryOpts {
exclude: Some(exclude),
include: None,
},
);
let rust_with_src = rust_build_image
.with_workdir("/mnt/src")
.with_directory(
"/usr/local/cargo",
rust_prebuild.directory("/usr/local/cargo").id().await?,
)
.with_directory("target", incremental_dir.id().await?)
.with_directory("/mnt/src/", src.id().await?);
Ok(rust_with_src)
}

View File

@ -1,36 +0,0 @@
[package]
name = "cuddle-ci"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
readme.workspace = true
repository.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dagger-rust.workspace = true
dagger-cuddle-please.workspace = true
dagger-sdk.workspace = true
eyre.workspace = true
clap.workspace = true
async-trait.workspace = true
futures.workspace = true
tokio.workspace = true
serde_json.workspace = true
serde_yaml.workspace = true
serde.workspace = true
tracing = {version = "0.1.40", features = ["log"]}
chrono = {version = "0.4.38"}
toml = "0.8.12"
[dev-dependencies]
pretty_assertions = "1.4.0"
tokio.workspace = true
[features]
default = []
dagger = []
integration = []

View File

@ -1,198 +0,0 @@
use std::{
collections::BTreeMap,
ops::{Deref, DerefMut},
};
use async_trait::async_trait;
pub struct CuddleCI {
pr_action: Vec<Box<dyn PullRequestAction + Send + Sync>>,
main_action: Vec<Box<dyn MainAction + Send + Sync>>,
release_action: Vec<Box<dyn ReleaseAction + Send + Sync>>,
}
#[derive(Default, Debug)]
pub struct Context {
store: BTreeMap<String, String>,
}
impl Deref for Context {
type Target = BTreeMap<String, String>;
fn deref(&self) -> &Self::Target {
&self.store
}
}
impl DerefMut for Context {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.store
}
}
impl CuddleCI {
pub fn new(
pr: Box<dyn PullRequestAction + Send + Sync>,
main: Box<dyn MainAction + Send + Sync>,
release: Box<dyn ReleaseAction + Send + Sync>,
) -> Self {
Self {
pr_action: vec![pr],
main_action: vec![main],
release_action: vec![release],
}
}
pub fn with_pull_request<T>(&mut self, pr: &T) -> &mut Self
where
T: PullRequestAction + ToOwned + Send + Sync + 'static,
T: ToOwned<Owned = T>,
{
self.pr_action.push(Box::new(pr.to_owned()));
self
}
pub fn with_main<T>(&mut self, main: &T) -> &mut Self
where
T: MainAction + Send + Sync + 'static,
T: ToOwned<Owned = T>,
{
self.main_action.push(Box::new(main.to_owned()));
self
}
pub fn with_release(&mut self, release: Box<dyn ReleaseAction + Send + Sync>) -> &mut Self {
self.release_action.push(release);
self
}
pub async fn execute(
&mut self,
args: impl IntoIterator<Item = impl Into<String>>,
) -> eyre::Result<()> {
let matches = clap::Command::new("cuddle-ci")
.about("is a wrapper around common CI actions")
.subcommand(clap::Command::new("pr"))
.subcommand(clap::Command::new("main"))
.subcommand(clap::Command::new("release"))
.subcommand_required(true)
.try_get_matches_from(args.into_iter().map(|a| a.into()).collect::<Vec<String>>())?;
let mut context = Context::default();
match matches.subcommand() {
Some((name, args)) => match (name, args) {
("pr", _args) => {
eprintln!("starting pr validate");
for pr_action in self.pr_action.iter() {
pr_action.execute_pull_request(&mut context).await?;
}
eprintln!("finished pr validate");
}
("main", _args) => {
eprintln!("starting main validate");
for main_action in self.main_action.iter() {
main_action.execute_main(&mut context).await?;
}
eprintln!("finished main validate");
}
("release", _args) => {
eprintln!("starting release validate");
for release_action in self.release_action.iter() {
release_action.execute_release(&mut context).await?;
}
eprintln!("finished release validate");
}
(command_name, _) => {
eyre::bail!("command is not recognized: {}", command_name)
}
},
None => eyre::bail!("command required a subcommand [pr, main, release] etc."),
}
Ok(())
}
}
impl Default for CuddleCI {
fn default() -> Self {
Self::new(
Box::new(DefaultPullRequestAction {}),
Box::new(DefaultMainAction {}),
Box::new(DefaultReleaseAction {}),
)
}
}
#[async_trait]
pub trait PullRequestAction {
async fn execute_pull_request(&self, _ctx: &mut Context) -> eyre::Result<()> {
eprintln!("validate pull request: noop");
Ok(())
}
}
pub struct DefaultPullRequestAction {}
#[async_trait]
impl PullRequestAction for DefaultPullRequestAction {}
#[async_trait]
pub trait MainAction {
async fn execute_main(&self, _ctx: &mut Context) -> eyre::Result<()>;
}
pub struct DefaultMainAction {}
#[async_trait]
impl MainAction for DefaultMainAction {
async fn execute_main(&self, _ctx: &mut Context) -> eyre::Result<()> {
Ok(())
}
}
#[async_trait]
pub trait ReleaseAction {
async fn execute_release(&self, _ctx: &mut Context) -> eyre::Result<()> {
eprintln!("validate release: noop");
Ok(())
}
}
pub struct DefaultReleaseAction {}
#[async_trait]
impl ReleaseAction for DefaultReleaseAction {}
#[cfg(test)]
mod test {
use super::*;
#[tokio::test]
async fn test_can_call_default() -> eyre::Result<()> {
CuddleCI::default().execute(["cuddle-ci", "pr"]).await?;
Ok(())
}
#[tokio::test]
async fn test_fails_on_no_command() -> eyre::Result<()> {
let res = CuddleCI::default().execute(["cuddle-ci"]).await;
assert!(res.is_err());
Ok(())
}
#[tokio::test]
async fn test_fails_on_wrong_command() -> eyre::Result<()> {
let res = CuddleCI::default()
.execute(["cuddle-ci", "something"])
.await;
assert!(res.is_err());
Ok(())
}
}

View File

@ -1,273 +0,0 @@
use std::{collections::BTreeMap, path::PathBuf};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct CuddleFile {
pub vars: CuddleVars,
pub deployment: Option<CuddleDeployment>,
pub components: Option<CuddleComponents>,
pub please: Option<CuddlePlease>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct CuddleComponents {
pub database: Option<CuddleDatabase>,
pub assets: Option<CuddleAssets>,
pub packages: Option<Packages>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct CuddleAssets {
pub volumes: Option<Vec<CuddleAssetInclude>>,
pub clean: Option<bool>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct CuddlePlease {}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct Packages {
pub debian: DebianPackages,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct DebianPackages {
pub dev: Vec<String>,
pub release: Vec<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct CuddleAssetInclude {
pub from: PathBuf,
pub to: PathBuf,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(untagged)]
pub enum CuddleDatabase {
Enabled(bool),
Values { migrations: PathBuf },
Default {},
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct CuddleVars {
pub service: String,
pub registry: String,
pub clusters: Option<CuddleClusters>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct CuddleDeployment {
pub registry: String,
pub env: CuddleDeploymentEnv,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct CuddleDeploymentEnv(pub BTreeMap<String, CuddleDeploymentCluster>);
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct CuddleDeploymentCluster {
pub clusters: Vec<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct CuddleClusters(pub BTreeMap<String, CuddleCluster>);
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct CuddleCluster {
pub namespace: String,
}
impl CuddleFile {
pub async fn from_cuddle_file() -> eyre::Result<Self> {
let cuddle_file_content = tokio::fs::read_to_string("cuddle.yaml").await?;
Self::parse_cuddle_file(&cuddle_file_content)
}
pub fn parse_cuddle_file(content: &str) -> eyre::Result<Self> {
let cuddle_file: CuddleFile = serde_yaml::from_str(content)?;
Ok(cuddle_file)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn parse_file() {
let cuddle_file = r#"
base: "git@git.front.kjuulh.io:kjuulh/cuddle-base.git"
vars:
service: "infrastructure-example"
registry: kasperhermansen
clusters:
clank_prod:
replicas: "3"
namespace: clank_prod
deployment:
registry: git@git.front.kjuulh.io:kjuulh/clank-clusters
env:
prod:
clusters:
- clank_prod
components:
database: true
assets:
volumes:
- from: somewhere
to: somewhere-else
packages:
debian:
dev:
- "capnp"
release:
- "capnp"
scripts:
render:
type: shell
args:
cluster:
name: cluster
type: flag
image_tag:
name: image_tag
type: flag"#;
let res = CuddleFile::parse_cuddle_file(cuddle_file).expect("to parse file");
let mut clusters = BTreeMap::new();
clusters.insert(
"clank_prod".into(),
CuddleCluster {
namespace: "clank_prod".into(),
},
);
let mut deployment = BTreeMap::new();
deployment.insert(
"prod".into(),
CuddleDeploymentCluster {
clusters: vec!["clank_prod".into()],
},
);
let expected = CuddleFile {
vars: CuddleVars {
service: "infrastructure-example".into(),
registry: "kasperhermansen".into(),
clusters: Some(CuddleClusters(clusters)),
},
deployment: Some(crate::cuddle_file::CuddleDeployment {
registry: "git@git.front.kjuulh.io:kjuulh/clank-clusters".into(),
env: CuddleDeploymentEnv(deployment),
}),
components: Some(CuddleComponents {
database: Some(CuddleDatabase::Enabled(true)),
assets: Some(CuddleAssets {
volumes: Some(vec![CuddleAssetInclude {
from: "somewhere".into(),
to: "somewhere-else".into(),
}]),
clean: None,
}),
packages: Some(Packages {
debian: DebianPackages {
dev: vec!["capnp".into()],
release: vec!["capnp".into()],
},
}),
}),
please: None,
};
pretty_assertions::assert_eq!(expected, res)
}
#[test]
fn cuddle_database_default() {
let cuddle_file = r#"
base: "git@git.front.kjuulh.io:kjuulh/cuddle-base.git"
vars:
service: "infrastructure-example"
registry: kasperhermansen
components:
database: {}
"#;
let res = CuddleFile::parse_cuddle_file(cuddle_file).expect("to parse file");
let expected = CuddleFile {
vars: CuddleVars {
service: "infrastructure-example".into(),
registry: "kasperhermansen".into(),
clusters: None,
},
deployment: None,
components: Some(CuddleComponents {
database: Some(CuddleDatabase::Default {}),
assets: None,
packages: None,
}),
please: None,
};
pretty_assertions::assert_eq!(expected, res)
}
#[test]
fn cuddle_packages() {
let cuddle_file = r#"
base: "git@git.front.kjuulh.io:kjuulh/cuddle-base.git"
vars:
service: "infrastructure-example"
registry: kasperhermansen
components:
packages:
debian:
dev:
- "capnp"
release:
- "capnp"
"#;
let res = CuddleFile::parse_cuddle_file(cuddle_file).expect("to parse file");
let expected = CuddleFile {
vars: CuddleVars {
service: "infrastructure-example".into(),
registry: "kasperhermansen".into(),
clusters: None,
},
deployment: None,
components: Some(CuddleComponents {
database: None,
assets: None,
packages: Some(Packages {
debian: DebianPackages {
dev: vec!["capnp".into()],
release: vec!["capnp".into()],
},
}),
}),
please: None,
};
pretty_assertions::assert_eq!(expected, res)
}
}

View File

@ -1,37 +0,0 @@
use async_trait::async_trait;
use dagger_cuddle_please::{models::CuddlePleaseSrcArgs, DaggerCuddlePlease};
use crate::{Context, MainAction};
#[derive(Clone)]
pub struct CuddlePlease {
client: dagger_sdk::Query,
}
impl CuddlePlease {
pub fn new(client: dagger_sdk::Query) -> Self {
Self { client }
}
}
#[async_trait]
impl MainAction for CuddlePlease {
async fn execute_main(&self, _ctx: &mut Context) -> eyre::Result<()> {
let client = self.client.clone();
let action = DaggerCuddlePlease::new(client);
action
.cuddle_please_src(&CuddlePleaseSrcArgs {
cuddle_image: "kasperhermansen/cuddle-please:main-1712698022".into(),
server: dagger_cuddle_please::models::SrcServer::Gitea {
token: std::env::var("CUDDLE_PLEASE_TOKEN")
.expect("CUDDLE_PLEASE_TOKEN to be present"),
},
log_level: Some(dagger_cuddle_please::models::LogLevel::Debug),
})
.await?;
Ok(())
}
}

View File

@ -1,152 +0,0 @@
use std::fmt::Display;
use async_trait::async_trait;
use eyre::Context;
use crate::{cli, cuddle_file::CuddleFile, MainAction};
#[derive(Clone)]
pub struct CuddleReleaser {
client: dagger_sdk::Query,
env: Option<String>,
cuddle_file: CuddleFile,
folder: String,
}
pub struct CuddleReleaserOptions {
upstream: String,
cluster: String,
namespace: String,
app: String,
}
pub enum CuddleEnv {
Prod,
Dev,
}
impl Display for CuddleEnv {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CuddleEnv::Prod => f.write_str("prod"),
CuddleEnv::Dev => f.write_str("dev"),
}
}
}
impl TryInto<CuddleEnv> for String {
type Error = eyre::Error;
fn try_into(self) -> Result<CuddleEnv, Self::Error> {
let env = match self.as_str() {
"prod" => CuddleEnv::Prod,
"dev" => CuddleEnv::Dev,
_ => eyre::bail!("was not a valid env: {}", self),
};
Ok(env)
}
}
impl CuddleReleaser {
pub async fn new(client: dagger_sdk::Query) -> eyre::Result<Self> {
let cuddle_file = CuddleFile::from_cuddle_file().await?;
let env = std::env::var("CUDDLE_ENV").ok();
Ok(Self {
client,
cuddle_file,
env,
folder: ".cuddle/tmp".into(),
})
}
pub async fn releaser(&self, env: CuddleEnv) -> eyre::Result<()> {
let client = self.client.clone();
if self.cuddle_file.deployment.is_none() {
return Ok(());
}
let chosen_cluster = match self
.cuddle_file
.deployment
.as_ref()
.unwrap()
.env
.0
.get(&self.env.as_ref().unwrap_or(&env.to_string()).to_string())
{
Some(c) => match c.clusters.first().take() {
Some(c) => c,
None => return Ok(()),
},
None => return Ok(()),
};
if let Some(clusters) = &self.cuddle_file.vars.clusters {
let cluster = match clusters.0.get(chosen_cluster) {
Some(c) => c,
None => eyre::bail!("no cluster found for: {}", chosen_cluster),
};
let options = CuddleReleaserOptions {
cluster: chosen_cluster.clone(),
namespace: cluster.namespace.clone(),
app: self.cuddle_file.vars.service.clone(),
upstream: self
.cuddle_file
.deployment
.as_ref()
.unwrap()
.registry
.clone(),
};
let cuddle_releaser_image = "docker.io/kasperhermansen/cuddle-releaser:main-1706726858";
let folder = client.host().directory(&self.folder);
let ssh_sock = std::env::var("SSH_AUTH_SOCK").context("SSH_AUTH_SOCK not set")?;
let cuddle_releaser = client
.container()
.from(cuddle_releaser_image)
.with_env_variable("RUST_LOG", "trace")
.with_directory("/mnt/templates", folder)
.with_unix_socket(
ssh_sock.clone(),
client.host().unix_socket(ssh_sock.clone()),
);
let time = chrono::Local::now();
cuddle_releaser
.with_exec(vec!["echo", &time.to_rfc3339()])
.with_exec(vec![
"cuddle-releaser",
"release",
&format!("--upstream={}", options.upstream),
&format!("--folder={}", "/mnt/templates/k8s"),
&format!("--cluster={}", options.cluster),
&format!("--namespace={}", options.namespace),
&format!("--app={}", options.app),
])
.sync()
.await?;
}
Ok(())
}
}
#[async_trait]
impl MainAction for CuddleReleaser {
async fn execute_main(&self, _ctx: &mut cli::Context) -> eyre::Result<()> {
self.releaser(CuddleEnv::Prod).await?;
Ok(())
}
}

View File

@ -1,68 +0,0 @@
pub struct CuddleX {
command: String,
args: Vec<String>,
}
impl CuddleX {
pub fn command(command: impl Into<String>) -> Self {
Self {
command: command.into(),
args: Vec::new(),
}
}
pub fn arg(&mut self, arg: impl Into<String>) -> &mut Self {
self.args.push(arg.into());
self
}
pub async fn run(&mut self) -> eyre::Result<(String, String, i32)> {
let mut cmd = tokio::process::Command::new("cuddle");
let cmd = cmd.arg("x").arg(&self.command).args(&self.args);
let process = cmd.spawn()?;
let output = process.wait_with_output().await?;
Ok((
std::str::from_utf8(&output.stdout)?.to_string(),
std::str::from_utf8(&output.stderr)?.to_string(),
output.status.code().unwrap_or(0),
))
}
}
pub mod well_known {
use super::CuddleX;
pub async fn render(args: impl IntoIterator<Item = impl Into<String>>) -> eyre::Result<()> {
tracing::info!("running render");
let mut cmd = CuddleX::command("render");
for arg in args.into_iter() {
let arg = arg.into();
cmd.arg(arg);
}
let (stdout, stderr, status) = cmd.run().await?;
for line in stdout.lines() {
tracing::trace!("render: {}", line);
}
for line in stderr.lines() {
tracing::trace!("render: {}", line);
}
if status != 0 {
tracing::warn!("finished render with non-zero exit code: {}", status);
}
tracing::info!("finished running render");
Ok(())
}
}

View File

@ -1,54 +0,0 @@
use async_trait::async_trait;
use dagger_sdk::Container;
use std::{future::Future, pin::Pin, sync::Arc};
pub type DynMiddleware = Arc<dyn DaggerMiddleware + Send + Sync>;
#[async_trait]
pub trait DaggerMiddleware {
async fn handle(
&self,
container: dagger_sdk::Container,
) -> eyre::Result<dagger_sdk::Container> {
Ok(container)
}
}
pub struct DaggerMiddlewareFn<F>
where
F: Fn(Container) -> Pin<Box<dyn Future<Output = eyre::Result<Container>> + Send>>,
{
pub func: F,
}
pub fn middleware<F>(func: F) -> Box<DaggerMiddlewareFn<F>>
where
F: Fn(Container) -> Pin<Box<dyn Future<Output = eyre::Result<Container>> + Send>>,
{
Box::new(DaggerMiddlewareFn { func })
}
#[async_trait]
impl<F> DaggerMiddleware for DaggerMiddlewareFn<F>
where
F: Fn(Container) -> Pin<Box<dyn Future<Output = eyre::Result<Container>> + Send>> + Send + Sync,
{
async fn handle(&self, container: Container) -> eyre::Result<Container> {
// Call the closure stored in the struct
(self.func)(container).await
}
}
#[cfg(test)]
mod test {
use futures::FutureExt;
use super::*;
#[tokio::test]
async fn can_add_middleware() -> eyre::Result<()> {
middleware(|c| async move { Ok(c) }.boxed());
Ok(())
}
}

View File

@ -1,87 +0,0 @@
use std::{collections::BTreeMap, path::PathBuf, time::UNIX_EPOCH};
const DRONE_TEMPLATER_IMAGE: &str = "kasperhermansen/drone-templater:main-1711807810";
use async_trait::async_trait;
use eyre::{Context, OptionExt};
use crate::{rust_service::RustServiceContext, MainAction};
#[derive(Clone)]
pub struct DroneTemplater {
client: dagger_sdk::Query,
template: PathBuf,
variables: BTreeMap<String, String>,
}
impl DroneTemplater {
pub fn new(client: dagger_sdk::Query, template: impl Into<PathBuf>) -> Self {
Self {
client,
template: template.into(),
variables: BTreeMap::default(),
}
}
pub fn with_variable(
&mut self,
name: impl Into<String>,
value: impl Into<String>,
) -> &mut Self {
self.variables.insert(name.into(), value.into());
self
}
}
#[async_trait]
impl MainAction for DroneTemplater {
async fn execute_main(&self, ctx: &mut crate::Context) -> eyre::Result<()> {
let image_tag = ctx
.get_image_tag()?
.ok_or_eyre(eyre::eyre!("failed to find image tag"))?;
let src = self.client.host().directory(".cuddle/tmp/");
let drone_host = std::env::var("DRONE_HOST").context("DRONE_HOST is missing")?;
let drone_user = std::env::var("DRONE_USER").context("DRONE_USER is missing")?;
let drone_token = std::env::var("DRONE_TOKEN").context("DRONE_TOKEN is missing")?;
let drone_token_secret = self.client.set_secret("DRONE_TOKEN", drone_token);
let now = std::time::SystemTime::now()
.duration_since(UNIX_EPOCH)
.context("failed to get system time")?;
let template_name = self.template.display().to_string();
let cmd = vec!["drone-templater", "upload", "--template", &template_name]
.into_iter()
.map(|v| v.to_string())
.chain(
self.variables
.iter()
.map(|(name, value)| format!(r#"--variable={name}={value}"#)),
)
.chain(vec![format!("--variable=image_tag={}", image_tag)])
.collect::<Vec<_>>();
self.client
.container()
.from(DRONE_TEMPLATER_IMAGE)
.with_env_variable("RUST_LOG", "drone_templater=trace")
.with_exec(vec!["echo", &format!("{}", now.as_secs())])
.with_directory("/src/templates", src)
.with_workdir("/src")
.with_env_variable("DRONE_HOST", drone_host)
.with_env_variable("DRONE_USER", drone_user)
.with_secret_variable("DRONE_TOKEN", drone_token_secret)
.with_exec(cmd)
.sync()
.await
.context("failed to upload drone templates with error")?;
Ok(())
}
}

View File

@ -1,423 +0,0 @@
use std::{path::PathBuf, sync::Arc};
use async_trait::async_trait;
use dagger_rust::source::RustSource;
use dagger_sdk::Container;
use futures::{stream, StreamExt};
use crate::{
dagger_middleware::DaggerMiddleware,
rust_service::{
architecture::{Architecture, Os},
extensions::CargoBInstallExt,
RustServiceContext, RustServiceStage,
},
Context, MainAction, PullRequestAction,
};
#[derive(Clone)]
pub struct LeptosService {
pub(crate) client: dagger_sdk::Query,
base_image: Option<dagger_sdk::Container>,
final_image: Option<dagger_sdk::Container>,
stages: Vec<RustServiceStage>,
source: Option<PathBuf>,
crates: Vec<String>,
bin_name: String,
arch: Option<Architecture>,
os: Option<Os>,
deploy_target_name: Option<String>,
deploy: bool,
}
impl LeptosService {
pub fn new(client: dagger_sdk::Query, bin_name: impl Into<String>) -> Self {
Self {
client,
base_image: None,
final_image: None,
stages: Vec::new(),
source: None,
crates: Vec::new(),
bin_name: bin_name.into(),
arch: None,
os: None,
deploy_target_name: None,
deploy: true,
}
}
pub fn with_base_image(&mut self, base: dagger_sdk::Container) -> &mut Self {
self.base_image = Some(base);
self
}
pub fn with_stage(&mut self, stage: RustServiceStage) -> &mut Self {
self.stages.push(stage);
self
}
pub fn with_source(&mut self, path: impl Into<PathBuf>) -> &mut Self {
self.source = Some(path.into());
self
}
pub fn with_deploy_target(&mut self, deploy_target: impl Into<String>) -> &mut Self {
self.deploy_target_name = Some(deploy_target.into());
self
}
pub fn with_crates(
&mut self,
crates: impl IntoIterator<Item = impl Into<String>>,
) -> &mut Self {
self.crates = crates.into_iter().map(|c| c.into()).collect();
self
}
pub fn with_arch(&mut self, arch: Architecture) -> &mut Self {
self.arch = Some(arch);
self
}
pub fn with_os(&mut self, os: Os) -> &mut Self {
self.os = Some(os);
self
}
pub fn with_deploy(&mut self, deploy: bool) -> &mut Self {
self.deploy = deploy;
self
}
fn get_src(&self) -> PathBuf {
self.source
.clone()
.unwrap_or(std::env::current_dir().unwrap())
}
pub fn get_arch(&self) -> Architecture {
self.arch
.clone()
.unwrap_or_else(|| match std::env::consts::ARCH {
"x86" | "x86_64" | "amd64" => Architecture::Amd64,
"arm" => Architecture::Arm64,
arch => panic!("unsupported architecture: {arch}"),
})
}
pub fn get_os(&self) -> Os {
self.os
.clone()
.unwrap_or_else(|| match std::env::consts::OS {
"linux" => Os::Linux,
"macos" => Os::MacOS,
os => panic!("unsupported os: {os}"),
})
}
async fn run_stage(
&self,
stages: impl IntoIterator<Item = &Arc<dyn DaggerMiddleware + Send + Sync>>,
container: Container,
) -> eyre::Result<Container> {
let before_deps_stream = stream::iter(stages.into_iter().map(Ok));
let res = StreamExt::fold(before_deps_stream, Ok(container), |base, m| async move {
match (base, m) {
(Ok(base), Ok(m)) => m.handle(base).await,
(_, Err(e)) | (Err(e), _) => eyre::bail!("failed with {e}"),
}
})
.await?;
Ok(res)
}
pub async fn build_base(&self) -> eyre::Result<Container> {
let rust_src = RustSource::new(self.client.clone());
let (src, dep_src) = rust_src
.get_rust_src(Some(&self.get_src()), self.crates.clone())
.await?;
let base_image = self
.base_image
.clone()
.unwrap_or(self.client.container().from("rustlang/rust:nightly"));
let before_deps = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::BeforeDeps(middleware) => Some(middleware),
_ => None,
})
.collect::<Vec<_>>();
let image = self.run_stage(before_deps, base_image).await?;
let image = image.with_exec(vec!["rustup", "target", "add", "wasm32-unknown-unknown"]);
let after_deps = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::AfterDeps(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let image = self.run_stage(after_deps, image).await?;
let before_base = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::BeforeBase(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let image = self.run_stage(before_base, image).await?;
let cache = self.client.cache_volume("rust_target_cache");
let rust_prebuild = image
.with_workdir("/mnt/src")
.with_directory("/mnt/src", dep_src)
.with_exec(vec![
"cargo",
"leptos",
"build",
"--release",
"-vv",
"--project",
&self.bin_name,
])
.with_mounted_cache("/mnt/src/target/", cache);
let incremental_dir = rust_src
.get_rust_target_src(&self.get_src(), rust_prebuild.clone(), self.crates.clone())
.await?;
let rust_with_src = image
.with_workdir("/mnt/src")
.with_directory(
"/usr/local/cargo",
rust_prebuild.directory("/usr/local/cargo"),
)
.with_directory("/mnt/src/target", incremental_dir)
.with_directory("/mnt/src/", src);
let after_base = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::AfterBase(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let image = self.run_stage(after_base, rust_with_src).await?;
Ok(image)
}
pub async fn build_release(&self) -> eyre::Result<Container> {
let base = self.build_base().await?;
let before_build = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::BeforeBuild(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let base = self.run_stage(before_build, base).await?;
let binary_build = base.with_exec(vec![
"cargo",
"leptos",
"build",
"--release",
"--project",
&self.bin_name,
]);
let after_build = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::AfterBuild(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let binary_build = self.run_stage(after_build, binary_build).await?;
let dest = self
.final_image
.clone()
.unwrap_or(self.client.container().from("debian:bookworm"));
let before_package = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::BeforePackage(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let dest = self.run_stage(before_package, dest).await?;
let final_image = dest
.with_workdir("/mnt/app")
.with_file(
format!("/usr/local/bin/{}", self.bin_name),
binary_build.file(format!("/mnt/src/target/release/{}", self.bin_name)),
)
.with_directory(
"/mnt/app/target/site",
binary_build.directory("/mnt/src/target/site"),
)
.with_file(
"/mnt/app/Cargo.toml",
binary_build.file(format!("/mnt/src/crates/{}/Cargo.toml", self.bin_name)),
)
.with_env_variable("APP_ENVIRONMENT", "production")
.with_env_variable("LEPTOS_OUTPUT_NAME", &self.bin_name)
.with_env_variable("LEPTOS_SITE_ADDR", "0.0.0.0:3000")
.with_env_variable("LEPTOS_SITE_PKG_DIR", "pkg")
.with_exposed_port(3000);
let after_package = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::AfterPackage(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let final_image = self.run_stage(after_package, final_image).await?;
Ok(final_image)
}
pub async fn build_test(&self) -> eyre::Result<()> {
let base = self.build_base().await?;
let before_build = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::BeforeBuild(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let base = self.run_stage(before_build, base).await?;
base.with_exec(vec!["cargo", "leptos", "test", "--release"])
.sync()
.await?;
Ok(())
}
fn get_deploy_target(&self) -> String {
self.deploy_target_name
.clone()
.unwrap_or(self.bin_name.clone())
}
}
#[async_trait]
impl PullRequestAction for LeptosService {
async fn execute_pull_request(&self, _ctx: &mut Context) -> eyre::Result<()> {
let mut s = self.clone();
s.with_cargo_binstall("latest", ["cargo-leptos"])
.build_test()
.await?;
Ok(())
}
}
#[async_trait]
impl MainAction for LeptosService {
async fn execute_main(&self, ctx: &mut Context) -> eyre::Result<()> {
let mut s = self.clone();
let container = s
.with_cargo_binstall("latest", ["cargo-leptos"])
.build_release()
.await?;
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let tag = format!(
"docker.io/kasperhermansen/{}:main-{}",
self.bin_name, timestamp,
);
container.publish(tag.clone()).await?;
tracing::info!("published: {}", tag);
ctx.set_image_tag(format!("main-{}", &timestamp.to_string()))?;
if self.deploy {
let update_deployments_docker_image =
"docker.io/kasperhermansen/update-deployment:1701123940";
let dep = self
.client
.container()
.from(update_deployments_docker_image);
let dep = if let Ok(sock) = std::env::var("SSH_AUTH_SOCK") {
dep.with_unix_socket("/tmp/ssh_sock", self.client.host().unix_socket(sock))
.with_env_variable("SSH_AUTH_SOCK", "/tmp/ssh_sock")
.with_exec(vec![
"update-deployment",
"--repo",
&format!(
"git@git.front.kjuulh.io:kjuulh/{}-deployment.git",
self.get_deploy_target()
),
"--service",
&self.bin_name,
"--image",
&format!("kasperhermansen/{}:main-{}", self.bin_name, timestamp),
])
} else {
dep.with_env_variable("GIT_USERNAME", "kjuulh")
.with_env_variable(
"GIT_PASSWORD",
std::env::var("GIT_PASSWORD").expect("GIT_PASSWORD to be set"),
)
.with_exec(vec![
"update-deployment",
"--repo",
&format!(
"https://git.front.kjuulh.io/kjuulh/{}-deployment.git",
self.get_deploy_target()
),
"--service",
&self.bin_name,
"--image",
&format!("kasperhermansen/{}:main-{}", self.bin_name, timestamp),
])
};
dep.sync().await?;
}
Ok(())
}
}

View File

@ -1,15 +0,0 @@
pub mod cli;
pub use cli::*;
pub mod leptos_service;
pub mod node_service;
pub mod rust_lib;
pub mod rust_service;
pub mod cuddle_file;
pub mod cuddle_please;
pub mod cuddle_releaser;
pub mod cuddle_x;
pub mod dagger_middleware;
pub mod drone_templater;
pub mod rust_workspace;

View File

@ -1,290 +0,0 @@
use std::path::PathBuf;
use async_trait::async_trait;
use dagger_sdk::{Container, ContainerWithDirectoryOptsBuilder, HostDirectoryOptsBuilder};
use crate::{
dagger_middleware::DynMiddleware,
rust_service::architecture::{Architecture, Os},
Context, MainAction, PullRequestAction,
};
#[derive(Clone)]
pub enum NodeServiceStage {
BeforeDeps(DynMiddleware),
AfterDeps(DynMiddleware),
BeforeBase(DynMiddleware),
AfterBase(DynMiddleware),
BeforeBuild(DynMiddleware),
AfterBuild(DynMiddleware),
BeforePackage(DynMiddleware),
AfterPackage(DynMiddleware),
BeforeRelease(DynMiddleware),
AfterRelease(DynMiddleware),
}
#[derive(Clone)]
pub struct NodeService {
client: dagger_sdk::Query,
service: String,
base_image: Option<Container>,
final_image: Option<Container>,
stages: Vec<NodeServiceStage>,
source: Option<PathBuf>,
arch: Option<Architecture>,
os: Option<Os>,
}
impl NodeService {
pub fn new(value: dagger_sdk::Query, service: impl Into<String>) -> Self {
Self {
client: value,
service: service.into(),
base_image: None,
final_image: None,
stages: Vec::new(),
source: None,
arch: None,
os: None,
}
}
pub fn with_base_image(&mut self, base: dagger_sdk::Container) -> &mut Self {
self.base_image = Some(base);
self
}
pub fn with_service(&mut self, service: impl Into<String>) -> &mut Self {
self.service = service.into();
self
}
pub fn with_stage(&mut self, stage: NodeServiceStage) -> &mut Self {
self.stages.push(stage);
self
}
pub fn with_source(&mut self, path: impl Into<PathBuf>) -> &mut Self {
self.source = Some(path.into());
self
}
pub fn with_arch(&mut self, arch: Architecture) -> &mut Self {
self.arch = Some(arch);
self
}
pub fn with_os(&mut self, os: Os) -> &mut Self {
self.os = Some(os);
self
}
fn get_src(&self) -> PathBuf {
self.source
.clone()
.unwrap_or(std::env::current_dir().unwrap())
}
fn get_arch(&self) -> Architecture {
self.arch
.clone()
.unwrap_or_else(|| match std::env::consts::ARCH {
"x86" | "x86_64" | "amd64" => Architecture::Amd64,
"arm" => Architecture::Arm64,
arch => panic!("unsupported architecture: {arch}"),
})
}
fn get_os(&self) -> Os {
self.os
.clone()
.unwrap_or_else(|| match std::env::consts::OS {
"linux" => Os::Linux,
"macos" => Os::MacOS,
os => panic!("unsupported os: {os}"),
})
}
pub async fn build_base(&self) -> eyre::Result<Container> {
let src = self.client.host().directory_opts(
self.get_src().to_string_lossy(),
HostDirectoryOptsBuilder::default()
.exclude(vec!["node_modules/", ".git/", ".cuddle/"])
.build()?,
);
let pkg_files = self.client.host().directory_opts(
self.get_src().to_string_lossy(),
HostDirectoryOptsBuilder::default()
.include(vec!["package.json", "yarn.lock"])
.build()?,
);
let deps =
"apk add --no-cache build-base gcc autoconf automake zlib-dev libpng-dev vips-dev git postgresql-dev"
.split_whitespace();
let base_image = match self.base_image.clone() {
Some(image) => image,
None => self
.client
.container()
.from("node:20-alpine")
.with_exec(vec!["apk", "update"])
.with_exec(deps.collect()),
}
.with_env_variable("NODE_ENV", "production");
let base_yarn_image = base_image
.with_workdir("/opt/")
.with_directory(".", pkg_files)
.with_exec(vec!["yarn", "global", "add", "node-gyp"])
.with_exec(vec![
"yarn",
"config",
"set",
"network-timeout",
"600000",
"-g",
])
.with_exec(vec!["yarn", "install", "--production"]);
let base_build = base_yarn_image
.with_env_variable(
"PATH",
format!(
"/opt/node_modules/.bin:{}",
base_yarn_image.env_variable("PATH").await?
),
)
.with_workdir("/opt/app")
.with_directory(".", src)
.with_exec(vec!["yarn", "build"]);
Ok(base_build)
}
pub async fn build_release(&self) -> eyre::Result<Container> {
let base = self.build_base().await?;
let final_build_image = match self.final_image.clone() {
Some(c) => c,
None => self
.client
.container()
.from("node:20-alpine")
.with_exec(vec![
"apk",
"add",
"--no-cache",
"vips-dev",
"postgresql-dev",
]),
}
.with_env_variable("NODE_ENV", "production");
let final_image = final_build_image
.with_workdir("/opt/")
.with_directory("/opt/node_modules", base.directory("/opt/node_modules"))
.with_workdir("/opt/app")
.with_directory_opts(
"/opt/app",
base.directory("/opt/app"),
ContainerWithDirectoryOptsBuilder::default()
.owner("node:node")
.build()?,
)
.with_env_variable(
"PATH",
format!(
"/opt/node_modules/.bin:{}",
final_build_image.env_variable("PATH").await?
),
)
.with_user("node")
.with_exposed_port(1337)
.with_entrypoint(vec!["yarn", "start"]);
Ok(final_image)
}
}
#[async_trait]
impl PullRequestAction for NodeService {
async fn execute_pull_request(&self, _ctx: &mut Context) -> eyre::Result<()> {
let release = self.build_release().await?;
release.sync().await?;
Ok(())
}
}
#[async_trait]
impl MainAction for NodeService {
async fn execute_main(&self, _ctx: &mut Context) -> eyre::Result<()> {
let container = self.build_release().await?;
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
container
.publish(format!(
"docker.io/kasperhermansen/{}:main-{}",
self.service, timestamp,
))
.await?;
let update_deployments_docker_image =
"docker.io/kasperhermansen/update-deployment:1701123940";
let dep = self
.client
.container()
.from(update_deployments_docker_image);
let dep = match std::env::var("SSH_AUTH_SOCK").ok() {
Some(sock) => dep
.with_unix_socket("/tmp/ssh_sock", self.client.host().unix_socket(sock))
.with_env_variable("SSH_AUTH_SOCK", "/tmp/ssh_sock")
.with_exec(vec![
"update-deployment",
"--repo",
&format!(
"git@git.front.kjuulh.io:kjuulh/{}-deployment.git",
self.service
),
"--service",
&self.service,
"--image",
&format!("kasperhermansen/{}:main-{}", self.service, timestamp),
]),
_ => dep
.with_env_variable("GIT_USERNAME", "kjuulh")
.with_env_variable(
"GIT_PASSWORD",
std::env::var("GIT_PASSWORD").expect("GIT_PASSWORD to be set"),
)
.with_exec(vec![
"update-deployment",
"--repo",
&format!(
"https://git.front.kjuulh.io/kjuulh/{}-deployment.git",
self.service
),
"--service",
&self.service,
"--image",
&format!("kasperhermansen/{}:main-{}", self.service, timestamp),
]),
};
dep.sync().await?;
Ok(())
}
}

View File

@ -1,147 +0,0 @@
use std::path::PathBuf;
use async_trait::async_trait;
use dagger_rust::source::RustSource;
use dagger_sdk::Container;
use crate::{
cli,
rust_service::architecture::{Architecture, Os},
rust_workspace, MainAction, PullRequestAction,
};
#[derive(Clone)]
pub struct RustLib {
client: dagger_sdk::Query,
base_image: Option<dagger_sdk::Container>,
source: Option<PathBuf>,
crates: Vec<String>,
arch: Option<Architecture>,
os: Option<Os>,
}
impl RustLib {
pub fn new(value: dagger_sdk::Query) -> Self {
Self {
client: value,
source: None,
crates: Vec::new(),
arch: None,
os: None,
base_image: None,
}
}
pub fn with_source(&mut self, path: impl Into<PathBuf>) -> &mut Self {
self.source = Some(path.into());
self
}
pub fn with_base_image(&mut self, base: dagger_sdk::Container) -> &mut Self {
self.base_image = Some(base);
self
}
pub fn with_crates(
&mut self,
crates: impl IntoIterator<Item = impl Into<String>>,
) -> &mut Self {
self.crates = crates.into_iter().map(|c| c.into()).collect();
self
}
pub async fn with_workspace_crates(&mut self) -> &mut Self {
if let Ok(Some(file)) = rust_workspace::File::read_file().await {
if let Some(members) = file.get_workspace_members() {
return self.with_crates(members);
}
}
self
}
pub fn with_arch(&mut self, arch: Architecture) -> &mut Self {
self.arch = Some(arch);
self
}
pub fn with_os(&mut self, os: Os) -> &mut Self {
self.os = Some(os);
self
}
fn get_src(&self) -> PathBuf {
self.source
.clone()
.unwrap_or(std::env::current_dir().unwrap())
}
pub async fn build_base(&self) -> eyre::Result<Container> {
let rust_src = RustSource::new(self.client.clone());
let (src, dep_src) = rust_src
.get_rust_src(Some(&self.get_src()), self.crates.clone())
.await?;
let base_image = self
.base_image
.clone()
.unwrap_or(self.client.container().from("rustlang/rust:nightly"));
let cache = self.client.cache_volume("rust_target_cache");
let rust_prebuild = base_image
.with_workdir("/mnt/src")
.with_directory("/mnt/src", dep_src)
.with_exec(vec!["cargo", "build", "--tests", "--workspace"])
.with_mounted_cache("/mnt/src/target/", cache);
let incremental_dir = rust_src
.get_rust_target_src(&self.get_src(), rust_prebuild.clone(), self.crates.clone())
.await?;
let rust_with_src = base_image
.with_workdir("/mnt/src")
.with_directory(
"/usr/local/cargo",
rust_prebuild.directory("/usr/local/cargo"),
)
.with_directory("/mnt/src/target", incremental_dir)
.with_directory("/mnt/src/", src);
Ok(rust_with_src)
}
pub async fn build_test(&self) -> eyre::Result<()> {
let base = self.build_base().await?;
base.with_exec(vec!["cargo", "test", "--tests", "--workspace"])
.sync()
.await?;
Ok(())
}
}
#[async_trait]
impl PullRequestAction for RustLib {
async fn execute_pull_request(&self, _ctx: &mut cli::Context) -> eyre::Result<()> {
self.build_test().await?;
Ok(())
}
}
#[async_trait]
impl MainAction for RustLib {
async fn execute_main(&self, _ctx: &mut cli::Context) -> eyre::Result<()> {
self.build_test().await?;
Ok(())
}
}

View File

@ -1,527 +0,0 @@
use std::{path::PathBuf, sync::Arc};
use async_trait::async_trait;
use dagger_rust::source::RustSource;
use dagger_sdk::Container;
use futures::{stream, StreamExt};
use crate::{
dagger_middleware::{DaggerMiddleware, DynMiddleware},
Context, MainAction, PullRequestAction,
};
use self::architecture::{Architecture, Os};
#[derive(Clone)]
pub enum RustServiceStage {
BeforeDeps(DynMiddleware),
AfterDeps(DynMiddleware),
BeforeBase(DynMiddleware),
AfterBase(DynMiddleware),
BeforeBuild(DynMiddleware),
AfterBuild(DynMiddleware),
BeforePackage(DynMiddleware),
AfterPackage(DynMiddleware),
BeforeRelease(DynMiddleware),
AfterRelease(DynMiddleware),
}
#[derive(Clone)]
pub struct RustService {
client: dagger_sdk::Query,
base_image: Option<dagger_sdk::Container>,
final_image: Option<dagger_sdk::Container>,
stages: Vec<RustServiceStage>,
source: Option<PathBuf>,
crates: Vec<String>,
bin_name: String,
arch: Option<Architecture>,
os: Option<Os>,
deployment: bool,
}
impl From<dagger_sdk::Query> for RustService {
fn from(value: dagger_sdk::Query) -> Self {
Self {
client: value,
base_image: None,
final_image: None,
stages: Vec::new(),
source: None,
crates: Vec::new(),
bin_name: String::new(),
arch: None,
os: None,
deployment: true,
}
}
}
impl RustService {
pub async fn new(client: dagger_sdk::Query) -> eyre::Result<Self> {
Ok(Self {
client,
base_image: None,
final_image: None,
stages: Vec::new(),
source: None,
crates: Vec::new(),
bin_name: String::new(),
arch: None,
os: None,
deployment: true,
})
}
pub fn with_base_image(&mut self, base: dagger_sdk::Container) -> &mut Self {
self.base_image = Some(base);
self
}
pub fn with_stage(&mut self, stage: RustServiceStage) -> &mut Self {
self.stages.push(stage);
self
}
pub fn with_source(&mut self, path: impl Into<PathBuf>) -> &mut Self {
self.source = Some(path.into());
self
}
pub fn with_bin_name(&mut self, bin_name: impl Into<String>) -> &mut Self {
self.bin_name = bin_name.into();
self
}
pub fn with_crates(
&mut self,
crates: impl IntoIterator<Item = impl Into<String>>,
) -> &mut Self {
self.crates = crates.into_iter().map(|c| c.into()).collect();
self
}
pub fn with_arch(&mut self, arch: Architecture) -> &mut Self {
self.arch = Some(arch);
self
}
pub fn with_os(&mut self, os: Os) -> &mut Self {
self.os = Some(os);
self
}
pub fn with_deployment(&mut self, deployment: bool) -> &mut Self {
self.deployment = deployment;
self
}
fn get_src(&self) -> PathBuf {
self.source
.clone()
.unwrap_or(std::env::current_dir().unwrap())
}
fn get_arch(&self) -> Architecture {
self.arch
.clone()
.unwrap_or_else(|| match std::env::consts::ARCH {
"x86" | "x86_64" | "amd64" => Architecture::Amd64,
"arm" => Architecture::Arm64,
arch => panic!("unsupported architecture: {arch}"),
})
}
fn get_os(&self) -> Os {
self.os
.clone()
.unwrap_or_else(|| match std::env::consts::OS {
"linux" => Os::Linux,
"macos" => Os::MacOS,
os => panic!("unsupported os: {os}"),
})
}
async fn run_stage(
&self,
stages: impl IntoIterator<Item = &Arc<dyn DaggerMiddleware + Send + Sync>>,
container: Container,
) -> eyre::Result<Container> {
let before_deps_stream = stream::iter(stages.into_iter().map(Ok));
let res = StreamExt::fold(before_deps_stream, Ok(container), |base, m| async move {
match (base, m) {
(Ok(base), Ok(m)) => m.handle(base).await,
(_, Err(e)) | (Err(e), _) => eyre::bail!("failed with {e}"),
}
})
.await?;
Ok(res)
}
pub async fn build_base(&self) -> eyre::Result<Container> {
let client = self.client.clone();
let rust_src = RustSource::new(client.clone());
let (src, dep_src) = rust_src
.get_rust_src(Some(&self.get_src()), self.crates.clone())
.await?;
let base_image = self
.base_image
.clone()
.unwrap_or(client.container().from("rustlang/rust:nightly"));
let before_deps = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::BeforeDeps(middleware) => Some(middleware),
_ => None,
})
.collect::<Vec<_>>();
let image = self.run_stage(before_deps, base_image).await?;
let after_deps = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::AfterDeps(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let image = self.run_stage(after_deps, image).await?;
let before_base = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::BeforeBase(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let image = self.run_stage(before_base, image).await?;
let cache = client.cache_volume("rust_target_cache");
let rust_prebuild = image
.with_workdir("/mnt/src")
.with_directory("/mnt/src", dep_src)
.with_exec(vec!["cargo", "build", "--release", "--bin", &self.bin_name])
.with_mounted_cache("/mnt/src/target/", cache);
let incremental_dir = rust_src
.get_rust_target_src(&self.get_src(), rust_prebuild.clone(), self.crates.clone())
.await?;
let rust_with_src = image
.with_workdir("/mnt/src")
.with_directory(
"/usr/local/cargo",
rust_prebuild.directory("/usr/local/cargo"),
)
.with_directory("/mnt/src/target", incremental_dir)
.with_directory("/mnt/src/", src);
let after_base = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::AfterBase(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let image = self.run_stage(after_base, rust_with_src).await?;
Ok(image)
}
pub async fn build_release(&self) -> eyre::Result<Container> {
let base = self.build_base().await?;
let client = self.client.clone();
let before_build = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::BeforeBuild(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let base = self.run_stage(before_build, base).await?;
let binary_build =
base.with_exec(vec!["cargo", "build", "--release", "--bin", &self.bin_name]);
let after_build = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::AfterBuild(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let binary_build = self.run_stage(after_build, binary_build).await?;
let dest = self
.final_image
.clone()
.unwrap_or(client.container().from("debian:bookworm"));
let before_package = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::BeforePackage(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let dest = self.run_stage(before_package, dest).await?;
let final_image = dest.with_workdir("/mnt/app").with_file(
format!("/usr/local/bin/{}", self.bin_name),
binary_build.file(format!("/mnt/src/target/release/{}", self.bin_name)),
);
let after_package = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::AfterPackage(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let final_image = self.run_stage(after_package, final_image).await?;
Ok(final_image)
}
pub async fn build_test(&self) -> eyre::Result<()> {
let base = self.build_base().await?;
let before_build = self
.stages
.iter()
.filter_map(|s| match s {
RustServiceStage::BeforeBuild(m) => Some(m),
_ => None,
})
.collect::<Vec<_>>();
let base = self.run_stage(before_build, base).await?;
base.with_exec(vec!["cargo", "test", "--release"])
.sync()
.await?;
Ok(())
}
}
#[async_trait]
impl PullRequestAction for RustService {
async fn execute_pull_request(&self, ctx: &mut Context) -> eyre::Result<()> {
self.build_test().await?;
let container = self.build_release().await?;
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let tag = format!(
"docker.io/kasperhermansen/{}:dev-{}",
self.bin_name, timestamp,
);
container.publish(&tag).await?;
ctx.set_image_tag(format!("dev-{}", &timestamp.to_string()))?;
Ok(())
}
}
const IMAGE_TAG: &str = "RUST_SERVICE_IMAGE_TAG";
pub trait RustServiceContext {
fn set_image_tag(&mut self, tag: impl Into<String>) -> eyre::Result<()>;
fn get_image_tag(&self) -> eyre::Result<Option<String>>;
}
impl RustServiceContext for Context {
fn get_image_tag(&self) -> eyre::Result<Option<String>> {
Ok(self.get(IMAGE_TAG).cloned())
}
fn set_image_tag(&mut self, tag: impl Into<String>) -> eyre::Result<()> {
let tag = tag.into();
self.insert(IMAGE_TAG.to_string(), tag);
Ok(())
}
}
#[async_trait]
impl MainAction for RustService {
async fn execute_main(&self, ctx: &mut Context) -> eyre::Result<()> {
let container = self.build_release().await?;
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let tag = format!(
"docker.io/kasperhermansen/{}:main-{}",
self.bin_name, timestamp,
);
container.publish(&tag).await?;
ctx.set_image_tag(format!("main-{}", &timestamp.to_string()))?;
if self.deployment {
let update_deployments_docker_image =
"docker.io/kasperhermansen/update-deployment:1701123940";
let dep = self
.client
.container()
.from(update_deployments_docker_image);
let dep = if let Ok(sock) = std::env::var("SSH_AUTH_SOCK") {
dep.with_unix_socket("/tmp/ssh_sock", self.client.host().unix_socket(sock))
.with_env_variable("SSH_AUTH_SOCK", "/tmp/ssh_sock")
.with_exec(vec![
"update-deployment",
"--repo",
&format!(
"git@git.front.kjuulh.io:kjuulh/{}-deployment.git",
self.bin_name
),
"--service",
&self.bin_name,
"--image",
&format!("kasperhermansen/{}:main-{}", self.bin_name, timestamp),
])
} else {
dep.with_env_variable("GIT_USERNAME", "kjuulh")
.with_env_variable(
"GIT_PASSWORD",
std::env::var("GIT_PASSWORD").expect("GIT_PASSWORD to be set"),
)
.with_exec(vec![
"update-deployment",
"--repo",
&format!(
"https://git.front.kjuulh.io/kjuulh/{}-deployment.git",
self.bin_name
),
"--service",
&self.bin_name,
"--image",
&format!("kasperhermansen/{}:main-{}", self.bin_name, timestamp),
])
};
dep.sync().await?;
}
Ok(())
}
}
pub mod architecture {
#[derive(Debug, Clone)]
pub enum Architecture {
Amd64,
Arm64,
}
#[derive(Debug, Clone)]
pub enum Os {
Linux,
MacOS,
}
}
mod apt;
mod apt_ca_certificates;
mod assets;
mod cargo_binstall;
mod cargo_clean;
mod clap_sanity_test;
mod cuddle_cli;
mod cuddle_file;
mod dagger_bin;
mod docker_cache;
mod docker_cli;
mod kubectl;
mod mold;
mod rust_workspace;
mod sqlx;
mod ssh_agent;
pub mod extensions {
pub use super::apt::*;
pub use super::apt_ca_certificates::*;
pub use super::assets::*;
pub use super::cargo_binstall::*;
pub use super::cargo_clean::*;
pub use super::clap_sanity_test::*;
pub use super::cuddle_cli::*;
pub use super::cuddle_file::*;
pub use super::dagger_bin::*;
pub use super::docker_cache::*;
pub use super::docker_cli::*;
pub use super::kubectl::*;
pub use super::mold::*;
pub use super::rust_workspace::*;
pub use super::sqlx::*;
pub use super::ssh_agent::*;
}
#[cfg(test)]
mod test {
#[tokio::test]
#[cfg(any(feature = "dagger", feature = "integration"))]
async fn test_can_build_rust() -> eyre::Result<()> {
dagger_sdk::connect(|client| async move {
let root_dir = std::path::PathBuf::from("../../").canonicalize()?;
let container = RustService::from(client.clone())
.with_arch(Architecture::Amd64)
.with_os(Os::Linux)
.with_source(root_dir)
.with_bin_name("ci")
.with_crates(["crates/*", "examples/*", "ci"])
.with_apt(&["git"])
.with_cargo_binstall("latest", ["sqlx-cli"])
.with_mold("2.3.3")
// .with_stage(RustServiceStage::BeforeDeps(middleware(|c| {
// async move {
// // Noop
// Ok(c)
// }
// .boxed()
// })))
.with_clap_sanity_test()
.build_release()
.await?;
container.sync().await?;
Ok(())
})
.await?;
Ok(())
}
}

View File

@ -1,86 +0,0 @@
use std::sync::Arc;
use async_trait::async_trait;
use dagger_sdk::Container;
use crate::{dagger_middleware::DaggerMiddleware, leptos_service::LeptosService};
use super::RustService;
pub struct Apt {
deps: Vec<String>,
}
impl Default for Apt {
fn default() -> Self {
Self::new()
}
}
impl Apt {
pub fn new() -> Self {
Self { deps: Vec::new() }
}
pub fn add(mut self, dep_name: impl Into<String>) -> Self {
self.deps.push(dep_name.into());
self
}
pub fn extend(mut self, deps: &[&str]) -> Self {
self.deps.extend(deps.iter().map(|s| s.to_string()));
self
}
}
#[async_trait]
impl DaggerMiddleware for Apt {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
let mut deps = vec!["apt", "install", "-y"];
deps.extend(self.deps.iter().map(|s| s.as_str()));
let c = container.with_exec(vec!["apt", "update"]).with_exec(deps);
Ok(c)
}
}
pub trait AptExt {
fn with_apt(&mut self, deps: &[&str]) -> &mut Self {
self
}
fn with_apt_release(&mut self, deps: &[&str]) -> &mut Self {
self
}
}
impl AptExt for RustService {
fn with_apt(&mut self, deps: &[&str]) -> &mut Self {
self.with_stage(super::RustServiceStage::BeforeDeps(Arc::new(
Apt::new().extend(deps),
)))
}
fn with_apt_release(&mut self, deps: &[&str]) -> &mut Self {
self.with_stage(super::RustServiceStage::BeforePackage(Arc::new(
Apt::new().extend(deps),
)))
}
}
impl AptExt for LeptosService {
fn with_apt(&mut self, deps: &[&str]) -> &mut Self {
self.with_stage(super::RustServiceStage::BeforeDeps(Arc::new(
Apt::new().extend(deps),
)))
}
fn with_apt_release(&mut self, deps: &[&str]) -> &mut Self {
self.with_stage(super::RustServiceStage::BeforePackage(Arc::new(
Apt::new().extend(deps),
)))
}
}

View File

@ -1,62 +0,0 @@
use std::sync::Arc;
use async_trait::async_trait;
use dagger_sdk::Container;
use crate::{dagger_middleware::DaggerMiddleware, leptos_service::LeptosService};
use super::RustService;
pub struct AptCaCertificates {}
impl Default for AptCaCertificates {
fn default() -> Self {
Self::new()
}
}
impl AptCaCertificates {
pub fn new() -> Self {
Self {}
}
}
#[async_trait]
impl DaggerMiddleware for AptCaCertificates {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
let c = container
.with_exec(vec!["apt", "update"])
.with_exec(vec!["apt", "install", "-y", "ca-certificates"])
.with_exec(vec!["update-ca-certificates"]);
Ok(c)
}
}
pub trait AptCaCertificatesExt {
fn with_apt_ca_certificates(&mut self) -> &mut Self {
self
}
}
impl AptCaCertificatesExt for RustService {
fn with_apt_ca_certificates(&mut self) -> &mut Self {
self.with_stage(super::RustServiceStage::BeforeDeps(Arc::new(
AptCaCertificates::new(),
)))
.with_stage(super::RustServiceStage::BeforePackage(Arc::new(
AptCaCertificates::new(),
)))
}
}
impl AptCaCertificatesExt for LeptosService {
fn with_apt_ca_certificates(&mut self) -> &mut Self {
self.with_stage(super::RustServiceStage::BeforeDeps(Arc::new(
AptCaCertificates::new(),
)))
.with_stage(super::RustServiceStage::BeforePackage(Arc::new(
AptCaCertificates::new(),
)))
}
}

View File

@ -1,68 +0,0 @@
use std::{path::PathBuf, sync::Arc};
use async_trait::async_trait;
use dagger_sdk::Container;
use crate::{dagger_middleware::DaggerMiddleware, leptos_service::LeptosService};
use super::RustService;
pub struct Assets {
client: dagger_sdk::Query,
assets: Vec<(PathBuf, PathBuf)>,
}
impl Assets {
pub fn new(client: dagger_sdk::Query) -> Self {
Self {
client,
assets: Vec::default(),
}
}
fn with_folders(mut self, folders: impl IntoIterator<Item = (PathBuf, PathBuf)>) -> Self {
let mut folders = folders.into_iter().collect::<Vec<(PathBuf, PathBuf)>>();
self.assets.append(&mut folders);
self
}
}
#[async_trait]
impl DaggerMiddleware for Assets {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
let container =
self.assets
.iter()
.fold(container, |container, (src_asset_path, dest_asset_path)| {
let src_path = src_asset_path.display().to_string();
let dest_path = dest_asset_path.display().to_string();
let path = self.client.host().directory(src_path);
container.with_directory(dest_path, path)
});
Ok(container)
}
}
pub trait AssetsExt {
fn with_assets(&mut self, folders: impl IntoIterator<Item = (PathBuf, PathBuf)>) -> &mut Self {
self
}
}
impl AssetsExt for RustService {
fn with_assets(&mut self, folders: impl IntoIterator<Item = (PathBuf, PathBuf)>) -> &mut Self {
self.with_stage(super::RustServiceStage::AfterPackage(Arc::new(
Assets::new(self.client.clone()).with_folders(folders),
)))
}
}
impl AssetsExt for LeptosService {
fn with_assets(&mut self, folders: impl IntoIterator<Item = (PathBuf, PathBuf)>) -> &mut Self {
self.with_stage(super::RustServiceStage::AfterPackage(Arc::new(
Assets::new(self.client.clone()).with_folders(folders),
)))
}
}

View File

@ -1,120 +0,0 @@
use std::sync::Arc;
use async_trait::async_trait;
use dagger_sdk::Container;
use crate::{dagger_middleware::DaggerMiddleware, leptos_service::LeptosService};
use super::{
architecture::{Architecture, Os},
RustService,
};
pub struct CargoBInstall {
arch: Architecture,
os: Os,
version: String,
crates: Vec<String>,
}
impl CargoBInstall {
pub fn new(
arch: Architecture,
os: Os,
version: impl Into<String>,
crates: impl Into<Vec<String>>,
) -> Self {
Self {
arch,
os,
version: version.into(),
crates: crates.into(),
}
}
fn get_arch(&self) -> String {
match self.arch {
Architecture::Amd64 => "x86_64",
Architecture::Arm64 => "armv7",
}
.into()
}
fn get_os(&self) -> String {
match self.os {
Os::Linux => "linux",
Os::MacOS => "darwin",
}
.into()
}
pub fn get_download_url(&self) -> String {
format!("https://github.com/cargo-bins/cargo-binstall/releases/{}/download/cargo-binstall-{}-unknown-{}-musl.tgz", self.version, self.get_arch(), self.get_os())
}
pub fn get_archive(&self) -> String {
format!(
"cargo-binstall-{}-unknown-{}-musl.tgz",
self.get_arch(),
self.get_os()
)
}
}
#[async_trait]
impl DaggerMiddleware for CargoBInstall {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
let c = container
.with_exec(vec!["wget", &self.get_download_url()])
.with_exec(vec!["tar", "-xvf", &self.get_archive()])
.with_exec(
"mv cargo-binstall /usr/local/cargo/bin"
.split_whitespace()
.collect(),
);
let c = self.crates.iter().cloned().fold(c, |acc, item| {
acc.with_exec(vec!["cargo", "binstall", &item, "-y"])
});
Ok(c)
}
}
pub trait CargoBInstallExt {
fn with_cargo_binstall(
&mut self,
version: impl Into<String>,
crates: impl IntoIterator<Item = impl Into<String>>,
) -> &mut Self {
self
}
}
impl CargoBInstallExt for RustService {
fn with_cargo_binstall(
&mut self,
version: impl Into<String>,
crates: impl IntoIterator<Item = impl Into<String>>,
) -> &mut Self {
let crates: Vec<String> = crates.into_iter().map(|s| s.into()).collect();
self.with_stage(super::RustServiceStage::BeforeDeps(Arc::new(
CargoBInstall::new(self.get_arch(), self.get_os(), version, crates),
)))
}
}
impl CargoBInstallExt for LeptosService {
fn with_cargo_binstall(
&mut self,
version: impl Into<String>,
crates: impl IntoIterator<Item = impl Into<String>>,
) -> &mut Self {
let crates: Vec<String> = crates.into_iter().map(|s| s.into()).collect();
self.with_stage(super::RustServiceStage::BeforeDeps(Arc::new(
CargoBInstall::new(self.get_arch(), self.get_os(), version, crates),
)))
}
}

View File

@ -1,45 +0,0 @@
use std::sync::Arc;
use async_trait::async_trait;
use dagger_sdk::Container;
use crate::dagger_middleware::DaggerMiddleware;
use super::RustService;
pub struct CargoClean;
impl Default for CargoClean {
fn default() -> Self {
Self::new()
}
}
impl CargoClean {
pub fn new() -> Self {
Self {}
}
}
#[async_trait]
impl DaggerMiddleware for CargoClean {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
Ok(container.with_exec(vec!["cargo", "clean"]))
}
}
pub trait CargoCleanExt {
fn with_cargo_clean(&mut self) -> &mut Self {
self
}
}
impl CargoCleanExt for RustService {
fn with_cargo_clean(&mut self) -> &mut Self {
self.with_stage(super::RustServiceStage::BeforeBuild(Arc::new(
CargoClean::new(),
)));
self
}
}

View File

@ -1,43 +0,0 @@
use std::sync::Arc;
use async_trait::async_trait;
use dagger_sdk::Container;
use crate::dagger_middleware::DaggerMiddleware;
use super::RustService;
pub struct ClapSanityTest {
bin_name: String,
}
impl ClapSanityTest {
pub fn new(bin_name: impl Into<String>) -> Self {
Self {
bin_name: bin_name.into(),
}
}
}
#[async_trait]
impl DaggerMiddleware for ClapSanityTest {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
Ok(container.with_exec(vec![&self.bin_name, "--help"]))
}
}
pub trait ClapSanityTestExt {
fn with_clap_sanity_test(&mut self) -> &mut Self {
self
}
}
impl ClapSanityTestExt for RustService {
fn with_clap_sanity_test(&mut self) -> &mut Self {
self.with_stage(super::RustServiceStage::AfterPackage(Arc::new(
ClapSanityTest::new(&self.bin_name),
)));
self
}
}

View File

@ -1,49 +0,0 @@
use std::sync::Arc;
use async_trait::async_trait;
use dagger_sdk::Container;
use crate::dagger_middleware::DaggerMiddleware;
use super::RustService;
pub struct CuddleCli {
client: dagger_sdk::Query,
}
impl CuddleCli {
pub fn new(client: dagger_sdk::Query) -> Self {
Self { client }
}
}
#[async_trait]
impl DaggerMiddleware for CuddleCli {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
let cuddle = self
.client
.container()
.from("kasperhermansen/cuddle:latest");
Ok(container.with_file(
"/usr/local/bin/cuddle",
cuddle.file("/usr/local/cargo/bin/cuddle"),
))
}
}
pub trait CuddleCliExt {
fn with_cuddle_cli(&mut self) -> &mut Self {
self
}
}
impl CuddleCliExt for RustService {
fn with_cuddle_cli(&mut self) -> &mut Self {
self.with_stage(super::RustServiceStage::BeforePackage(Arc::new(
CuddleCli::new(self.client.clone()),
)));
self
}
}

View File

@ -1,86 +0,0 @@
use std::path::PathBuf;
use crate::{
cuddle_file::{CuddleDatabase, CuddleFile},
rust_service::extensions::AptExt,
};
use super::{
extensions::{AssetsExt, CargoCleanExt, SqlxExt},
RustService,
};
#[derive(Default)]
pub struct CuddleFileAction {}
impl CuddleFileAction {
pub fn new() -> Self {
Self {}
}
}
pub trait CuddleFileExt {
fn with_cuddle_file(&mut self, cuddle_file: &CuddleFile) -> &mut Self;
}
impl CuddleFileExt for RustService {
fn with_cuddle_file(&mut self, cuddle_file: &CuddleFile) -> &mut Self {
let mut s = self
.with_bin_name(&cuddle_file.vars.service)
.with_deployment(false);
tracing::trace!("with cuddle file: {:+?}", &cuddle_file);
if let Some(components) = &cuddle_file.components {
s = if let Some(database) = &components.database {
match database {
CuddleDatabase::Enabled(true) => s.with_sqlx_migrations(
PathBuf::from("crates")
.join(&cuddle_file.vars.service)
.join("migrations/crdb"),
),
CuddleDatabase::Values { migrations } => s.with_sqlx_migrations(migrations),
CuddleDatabase::Enabled(false) | CuddleDatabase::Default {} => s,
}
} else {
s
};
if let Some(assets) = &components.assets {
if let Some(true) = assets.clean {
s = s.with_cargo_clean()
}
if let Some(volumes) = &assets.volumes {
let mappings = volumes.iter().cloned().map(|val| (val.from, val.to));
s = s.with_assets(mappings);
}
}
if let Some(packages) = &components.packages {
s = s
.with_apt(
packages
.debian
.dev
.iter()
.map(|r| r.as_str())
.collect::<Vec<_>>()
.as_slice(),
)
.with_apt_release(
packages
.debian
.release
.iter()
.map(|r| r.as_str())
.collect::<Vec<_>>()
.as_slice(),
);
}
}
s
}
}

View File

@ -1,72 +0,0 @@
use std::sync::Arc;
use async_trait::async_trait;
use dagger_sdk::Container;
use crate::dagger_middleware::DaggerMiddleware;
use super::RustService;
pub struct DaggerBin {
client: dagger_sdk::Query,
version: String,
}
impl DaggerBin {
pub fn new(client: dagger_sdk::Query, version: impl Into<String>) -> Self {
Self {
client,
version: version.into(),
}
}
}
#[async_trait]
impl DaggerMiddleware for DaggerBin {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
let install_script = self.client.http("https://dl.dagger.io/dagger/install.sh");
let dagger_bin = self
.client
.container()
.from("alpine")
.with_file_opts(
"/mnt/install.sh",
install_script,
dagger_sdk::ContainerWithFileOpts {
owner: None,
permissions: Some(0o755),
expand: None,
},
)
.with_env_variable("DAGGER_VERSION", &self.version)
.with_exec(vec!["/mnt/install.sh"])
.file("/bin/dagger");
Ok(container
.with_file_opts(
"/usr/local/bin/dagger",
dagger_bin,
dagger_sdk::ContainerWithFileOpts {
owner: None,
permissions: Some(0o755),
expand: None,
},
)
.with_exec(vec!["/usr/local/bin/dagger", "version"]))
}
}
pub trait DaggerBinExt {
fn with_dagger_bin(&mut self, dagger_version: impl Into<String>) -> &mut Self;
}
impl DaggerBinExt for RustService {
fn with_dagger_bin(&mut self, dagger_version: impl Into<String>) -> &mut Self {
self.with_stage(super::RustServiceStage::AfterPackage(Arc::new(
DaggerBin::new(self.client.clone(), dagger_version.into()),
)));
self
}
}

View File

@ -1,70 +0,0 @@
use std::sync::Arc;
use async_trait::async_trait;
use dagger_sdk::{Container, ImageMediaTypes};
use crate::dagger_middleware::DaggerMiddleware;
use super::RustService;
pub struct DockerCache {
client: dagger_sdk::Query,
image_name: String,
}
impl DockerCache {
pub fn new(client: dagger_sdk::Query, image_name: impl Into<String>) -> Self {
Self {
client,
image_name: image_name.into(),
}
}
}
#[async_trait]
impl DaggerMiddleware for DockerCache {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
match (
std::env::var("REGISTRY_CACHE_USERNAME"),
std::env::var("REGISTRY_CACHE_PASSWORD"),
) {
(Ok(username), Ok(password)) => {
let url = format!("harbor.front.kjuulh.io/cache/{}:cache", self.image_name);
let secret = self.client.set_secret("REGISTRY_CACHE_PASSWORD", password);
container
.with_registry_auth(&url, &username, secret)
.publish_opts(
&url,
dagger_sdk::ContainerPublishOpts {
forced_compression: Some(dagger_sdk::ImageLayerCompression::Zstd),
media_types: Some(ImageMediaTypes::OciMediaTypes),
platform_variants: None,
},
)
.await?;
}
_ => {
eprintln!("failed to find REGISTRY_CACHE_USERNAME or REGISTRY_CACHE_PASSWORD");
}
}
Ok(container)
}
}
pub trait DockerCacheExt {
fn with_docker_cache(&mut self) -> &mut Self {
self
}
}
impl DockerCacheExt for RustService {
fn with_docker_cache(&mut self) -> &mut Self {
self.with_stage(super::RustServiceStage::AfterPackage(Arc::new(
DockerCache::new(self.client.clone(), self.bin_name.clone()),
)));
self
}
}

View File

@ -1,48 +0,0 @@
use std::sync::Arc;
use async_trait::async_trait;
use dagger_sdk::{Container};
use crate::dagger_middleware::DaggerMiddleware;
use super::RustService;
pub struct DockerCli {
client: dagger_sdk::Query,
}
impl DockerCli {
pub fn new(client: dagger_sdk::Query) -> Self {
Self { client }
}
}
#[async_trait]
impl DaggerMiddleware for DockerCli {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
let docker = self.client.container().from("docker:cli");
Ok(container
.with_file(
"/usr/local/bin/docker",
docker.file("/usr/local/bin/docker"),
)
.with_directory("/certs", docker.directory("/certs")))
}
}
pub trait DockerCliExt {
fn with_docker_cli(&mut self) -> &mut Self {
self
}
}
impl DockerCliExt for RustService {
fn with_docker_cli(&mut self) -> &mut Self {
self.with_stage(super::RustServiceStage::BeforePackage(Arc::new(
DockerCli::new(self.client.clone()),
)));
self
}
}

View File

@ -1,75 +0,0 @@
use std::sync::Arc;
use async_trait::async_trait;
use dagger_sdk::Container;
use crate::dagger_middleware::DaggerMiddleware;
use super::RustService;
pub struct Kubectl {
client: dagger_sdk::Query,
}
impl Kubectl {
pub fn new(client: dagger_sdk::Query) -> Self {
Self { client }
}
}
const KUBESLICEDOWNLOAD: &str = r#"slice_VERSION=v1.2.7 && \
wget -O kubectl-slice_linux_x86_64.tar.gz "https://github.com/patrickdappollonio/kubectl-slice/releases/download/$slice_VERSION/kubectl-slice_linux_x86_64.tar.gz" && \
tar -xf kubectl-slice_linux_x86_64.tar.gz && \
chmod +x ./kubectl-slice && \
mv ./kubectl-slice /usr/local/bin/kubectl-slice && \
rm kubectl-slice_linux_x86_64.tar.gz"#;
#[async_trait]
impl DaggerMiddleware for Kubectl {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
let kubectl = self
.client
.container()
.from("line/kubectl-kustomize:1.29.1-5.3.0");
let kubeslice = self
.client
.container()
.from("alpine:3.19")
.with_exec(vec!["apk", "add", "tar", "wget"])
.with_exec(vec!["sh", "-c", KUBESLICEDOWNLOAD]);
let helm = self.client.container().from("alpine/helm:3.11.1");
Ok(container
.with_file(
"/usr/local/bin/kubectl",
kubectl.file("/usr/local/bin/kubectl"),
)
.with_file(
"/usr/local/bin/kustomize",
kubectl.file("/usr/local/bin/kustomize"),
)
.with_file(
"/usr/local/bin/kubectl-slice",
kubeslice.file("/usr/local/bin/kubectl-slice"),
)
.with_file("/usr/local/bin/helm", helm.file("/usr/bin/helm")))
}
}
pub trait KubectlExt {
fn with_kubectl(&mut self) -> &mut Self {
self
}
}
impl KubectlExt for RustService {
fn with_kubectl(&mut self) -> &mut Self {
self.with_stage(super::RustServiceStage::BeforePackage(Arc::new(
Kubectl::new(self.client.clone()),
)));
self
}
}

View File

@ -1,104 +0,0 @@
use std::sync::Arc;
use async_trait::async_trait;
use crate::dagger_middleware::DaggerMiddleware;
use super::{
architecture::{Architecture, Os},
RustService,
};
pub struct MoldInstall {
arch: Architecture,
os: Os,
version: String,
}
impl MoldInstall {
pub fn new(arch: Architecture, os: Os, version: impl Into<String>) -> Self {
Self {
arch,
os,
version: version.into(),
}
}
fn get_arch(&self) -> String {
match self.arch {
Architecture::Amd64 => "x86_64",
Architecture::Arm64 => "arm",
}
.into()
}
fn get_os(&self) -> String {
match &self.os {
Os::Linux => "linux",
o => todo!("os not implemented for mold: {:?}", o),
}
.into()
}
pub fn get_download_url(&self) -> String {
format!(
"https://github.com/rui314/mold/releases/download/v{}/mold-{}-{}-{}.tar.gz",
self.version,
self.version,
self.get_arch(),
self.get_os()
)
}
pub fn get_folder(&self) -> String {
format!(
"mold-{}-{}-{}",
self.version,
self.get_arch(),
self.get_os()
)
}
pub fn get_archive_name(&self) -> String {
format!(
"mold-{}-{}-{}.tar.gz",
self.version,
self.get_arch(),
self.get_os()
)
}
}
#[async_trait]
impl DaggerMiddleware for MoldInstall {
async fn handle(
&self,
container: dagger_sdk::Container,
) -> eyre::Result<dagger_sdk::Container> {
println!("installing mold");
let c = container
.with_exec(vec!["wget", &self.get_download_url()])
.with_exec(vec!["tar", "-xvf", &self.get_archive_name()])
.with_exec(vec![
"mv",
&format!("{}/bin/mold", self.get_folder()),
"/usr/bin/mold",
]);
Ok(c)
}
}
pub trait MoldActionExt {
fn with_mold(&mut self, version: impl Into<String>) -> &mut Self {
self
}
}
impl MoldActionExt for RustService {
fn with_mold(&mut self, version: impl Into<String>) -> &mut Self {
self.with_stage(super::RustServiceStage::AfterDeps(Arc::new(
MoldInstall::new(self.get_arch(), self.get_os(), version),
)))
}
}

View File

@ -1,66 +0,0 @@
use std::sync::Arc;
use async_trait::async_trait;
use dagger_sdk::Container;
use crate::{dagger_middleware::DaggerMiddleware, leptos_service::LeptosService, rust_workspace};
use super::RustService;
pub struct RustWorkspace {}
impl Default for RustWorkspace {
fn default() -> Self {
Self::new()
}
}
impl RustWorkspace {
pub fn new() -> Self {
Self {}
}
}
#[async_trait]
impl DaggerMiddleware for RustWorkspace {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
Ok(container)
}
}
#[async_trait]
pub trait RustWorkspaceExt {
async fn with_workspace_crates(&mut self) -> &mut Self {
self
}
}
#[async_trait]
impl RustWorkspaceExt for RustService {
async fn with_workspace_crates(&mut self) -> &mut Self {
self.with_crates(get_members().await)
.with_stage(super::RustServiceStage::BeforeDeps(Arc::new(
RustWorkspace::new(),
)))
}
}
#[async_trait]
impl RustWorkspaceExt for LeptosService {
async fn with_workspace_crates(&mut self) -> &mut Self {
self.with_crates(get_members().await)
.with_stage(super::RustServiceStage::BeforeDeps(Arc::new(
RustWorkspace::new(),
)))
}
}
async fn get_members() -> Vec<String> {
if let Ok(Some(file)) = rust_workspace::File::read_file().await {
if let Some(members) = file.get_workspace_members() {
return members;
}
}
Vec::new()
}

View File

@ -1,81 +0,0 @@
use std::{path::PathBuf, sync::Arc};
use async_trait::async_trait;
use dagger_sdk::Container;
use crate::dagger_middleware::DaggerMiddleware;
use super::RustService;
pub struct Sqlx {
client: dagger_sdk::Query,
migration_path: Option<PathBuf>,
}
impl Sqlx {
pub fn new(client: dagger_sdk::Query) -> Self {
Self {
client,
migration_path: None,
}
}
pub fn with_migration_path(mut self, migration_path: impl Into<PathBuf>) -> Self {
self.migration_path = Some(migration_path.into());
self
}
}
#[async_trait]
impl DaggerMiddleware for Sqlx {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
let container = if std::path::PathBuf::from(".sqlx/").exists() {
tracing::debug!("found .sqlx folder enabling offline mode");
let src = self.client.host().directory(".sqlx/");
container
.with_directory(".sqlx", src)
.with_env_variable("SQLX_OFFLINE", "true")
} else {
tracing::debug!("did not find a .sqlx folder, requires a running database");
container
};
let container = if let Some(migration_path) = &self.migration_path {
container
.with_directory(
"/mnt/sqlx/migrations",
self.client
.host()
.directory(migration_path.display().to_string()),
)
.with_env_variable("NEFARIOUS_DB_MIGRATION_PATH", "/mnt/sqlx/migrations")
} else {
container
};
Ok(container)
}
}
pub trait SqlxExt {
fn with_sqlx(&mut self) -> &mut Self;
fn with_sqlx_migrations(&mut self, path: impl Into<PathBuf>) -> &mut Self;
}
impl SqlxExt for RustService {
fn with_sqlx(&mut self) -> &mut Self {
self.with_stage(super::RustServiceStage::BeforeBuild(Arc::new(Sqlx::new(
self.client.clone(),
))))
}
fn with_sqlx_migrations(&mut self, path: impl Into<PathBuf>) -> &mut Self {
self.with_stage(super::RustServiceStage::BeforeBuild(Arc::new(
Sqlx::new(self.client.clone()).with_migration_path(path),
)))
}
}

View File

@ -1,79 +0,0 @@
use std::sync::Arc;
use async_trait::async_trait;
use dagger_sdk::{Container, ContainerWithNewFileOptsBuilder};
use eyre::Context;
use crate::{dagger_middleware::DaggerMiddleware, leptos_service::LeptosService};
use super::RustService;
pub struct SshAgent {
client: dagger_sdk::Query,
}
impl SshAgent {
pub fn new(client: dagger_sdk::Query) -> Self {
Self { client }
}
}
#[async_trait]
impl DaggerMiddleware for SshAgent {
async fn handle(&self, container: Container) -> eyre::Result<Container> {
let sock_var =
std::env::var("SSH_AUTH_SOCK").context("failed to find variable SSH_AUTH_SOCK")?;
let socket = self.client.host().unix_socket(&sock_var);
let c = container
.with_new_file_opts(
".ssh/config".to_string(),
r#"
Host *
UserKnownHostsFile=/dev/null
StrictHostKeyChecking no
"#,
ContainerWithNewFileOptsBuilder::default()
.permissions(0o700_isize)
.build()?,
)
.with_unix_socket(&sock_var, socket)
.with_env_variable("SSH_AUTH_SOCK", &sock_var);
Ok(c)
}
}
pub trait SshAgentExt {
fn with_ssh_agent(&mut self) -> &mut Self {
self
}
}
impl SshAgentExt for RustService {
fn with_ssh_agent(&mut self) -> &mut Self {
let client = self.client.clone();
self.with_stage(super::RustServiceStage::BeforeDeps(Arc::new(
SshAgent::new(client.clone()),
)))
.with_stage(super::RustServiceStage::BeforeBase(Arc::new(
SshAgent::new(client),
)))
}
}
impl SshAgentExt for LeptosService {
fn with_ssh_agent(&mut self) -> &mut Self {
let client = self.client.clone();
self.with_stage(super::RustServiceStage::BeforeDeps(Arc::new(
SshAgent::new(client.clone()),
)))
.with_stage(super::RustServiceStage::AfterBase(Arc::new(SshAgent::new(
client,
))))
}
}

View File

@ -1,31 +0,0 @@
use serde::Deserialize;
#[derive(Deserialize, Clone, Debug)]
pub struct Workspace {
pub members: Vec<String>,
}
#[derive(Deserialize, Clone, Debug)]
pub struct File {
pub workspace: Option<Workspace>,
}
impl File {
pub async fn read_file() -> eyre::Result<Option<Self>> {
let file = match tokio::fs::read_to_string("Cargo.toml").await {
Ok(file) => file,
Err(e) => {
tracing::warn!("Cargo.toml was not found: {}", e);
return Ok(None);
}
};
let workspace_file: File = toml::from_str(&file)?;
Ok(Some(workspace_file))
}
pub fn get_workspace_members(&self) -> Option<Vec<String>> {
self.workspace.as_ref().map(|w| w.members.clone())
}
}

View File

@ -1,16 +1,11 @@
[package] [package]
name = "dagger-cuddle-please" name = "dagger-cuddle-please"
description = "A set of components for running cuddle-please in dagger" version = "0.1.0"
version.workspace = true edition = "2021"
edition.workspace = true
authors.workspace = true
readme.workspace = true
license.workspace = true
repository.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
dagger-sdk.workspace = true dagger-sdk.workspace = true
eyre.workspace = true eyre.workspace = true
async-trait.workspace = true async-trait = "*"

View File

@ -80,11 +80,11 @@ pub mod traits {
} }
} }
pub struct DaggerCuddlePleaseAction(Arc<dyn CuddlePlease>); pub struct DaggerCuddlePleaseAction(Arc<dyn CuddlePlease + Send + Sync + 'static>);
impl DaggerCuddlePleaseAction { impl DaggerCuddlePleaseAction {
/// Create a [`traits::CuddlePlease`] client based on dagger /// Create a [`traits::CuddlePlease`] client based on dagger
pub fn dagger(client: dagger_sdk::Query) -> Self { pub fn dagger(client: Arc<dagger_sdk::Query>) -> Self {
Self(Arc::new(DaggerCuddlePlease::new(client))) Self(Arc::new(DaggerCuddlePlease::new(client)))
} }
@ -103,28 +103,32 @@ impl DaggerCuddlePleaseAction {
} }
#[derive(Clone)] #[derive(Clone)]
pub struct DaggerCuddlePlease { struct DaggerCuddlePlease {
client: dagger_sdk::Query, client: Arc<dagger_sdk::Query>,
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl CuddlePlease for DaggerCuddlePlease { impl CuddlePlease for DaggerCuddlePlease {
async fn execute(&self, args: &CuddlePleaseArgs) -> eyre::Result<()> { async fn execute(&self, args: &CuddlePleaseArgs) -> eyre::Result<()> {
self.cuddle_please(args).await self.cuddle_please(self.client.clone(), args).await
} }
async fn execute_src(&self, args: &CuddlePleaseSrcArgs) -> eyre::Result<()> { async fn execute_src(&self, args: &CuddlePleaseSrcArgs) -> eyre::Result<()> {
self.cuddle_please_src(args).await self.cuddle_please_src(self.client.clone(), args).await
} }
} }
impl DaggerCuddlePlease { impl DaggerCuddlePlease {
pub fn new(client: dagger_sdk::Query) -> Self { pub fn new(client: Arc<dagger_sdk::Query>) -> Self {
Self { client } Self { client }
} }
pub async fn cuddle_please(&self, args: &CuddlePleaseArgs) -> eyre::Result<()> { pub async fn cuddle_please(
let build_image = self.client.container().from(&args.cuddle_image); &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 { let repo_url = match &args.server {
Server::Gitea { Server::Gitea {
@ -178,32 +182,30 @@ impl DaggerCuddlePlease {
}; };
let src = if args.use_ssh_socket { let src = if args.use_ssh_socket {
let socket = self let socket = client
.client
.host() .host()
.unix_socket(std::env::var("SSH_AGENT").expect("SSH_AGENT to be set")); .unix_socket(std::env::var("SSH_AGENT").expect("SSH_AGENT to be set"));
self.client client
.git_opts( .git_opts(
&repo_url, &repo_url,
dagger_sdk::QueryGitOpts { dagger_sdk::QueryGitOpts {
experimental_service_host: None, experimental_service_host: None,
keep_git_dir: Some(true), keep_git_dir: Some(true),
ssh_auth_socket: Some(socket.id().await?),
ssh_known_hosts: None,
}, },
) )
.branch("main") .branch("main")
.tree() .tree_opts(dagger_sdk::GitRefTreeOpts {
ssh_auth_socket: Some(socket.id().await?),
ssh_known_hosts: None,
})
} else { } else {
self.client client
.git_opts( .git_opts(
&repo_url, &repo_url,
dagger_sdk::QueryGitOpts { dagger_sdk::QueryGitOpts {
experimental_service_host: None, experimental_service_host: None,
keep_git_dir: Some(true), keep_git_dir: Some(true),
ssh_auth_socket: None,
ssh_known_hosts: None,
}, },
) )
.branch("main") .branch("main")
@ -213,16 +215,19 @@ impl DaggerCuddlePlease {
let res = build_image let res = build_image
.with_secret_variable( .with_secret_variable(
"CUDDLE_PLEASE_TOKEN", "CUDDLE_PLEASE_TOKEN",
self.client.set_secret( client
.set_secret(
"CUDDLE_PLEASE_TOKEN", "CUDDLE_PLEASE_TOKEN",
match &args.server { match &args.server {
Server::Gitea { token, .. } => token, Server::Gitea { token, .. } => token,
Server::GitHub { token } => token, Server::GitHub { token } => token,
}, },
), )
.id()
.await?,
) )
.with_workdir("/mnt/app") .with_workdir("/mnt/app")
.with_directory(".", src) .with_directory(".", src.id().await?)
.with_exec(vec!["git", "remote", "set-url", "origin", &repo_url]) .with_exec(vec!["git", "remote", "set-url", "origin", &repo_url])
.with_exec(vec![ .with_exec(vec![
"cuddle-please", "cuddle-please",
@ -252,7 +257,10 @@ impl DaggerCuddlePlease {
}, },
]); ]);
res.sync().await?; let exit_code = res.exit_code().await?;
if exit_code != 0 {
eyre::bail!("failed to run cuddle-please");
}
let please_out = res.stdout().await?; let please_out = res.stdout().await?;
println!("{please_out}"); println!("{please_out}");
@ -261,63 +269,55 @@ impl DaggerCuddlePlease {
Ok(()) Ok(())
} }
pub async fn cuddle_please_src(&self, args: &CuddlePleaseSrcArgs) -> eyre::Result<()> { pub async fn cuddle_please_src(
let build_image = self.client.container().from(&args.cuddle_image); &self,
client: Arc<dagger_sdk::Query>,
args: &CuddlePleaseSrcArgs,
) -> eyre::Result<()> {
let build_image = client.container().from(&args.cuddle_image);
let res = build_image let res = build_image
.with_secret_variable( .with_secret_variable(
"CUDDLE_PLEASE_TOKEN", "CUDDLE_PLEASE_TOKEN",
self.client.set_secret( client
.set_secret(
"CUDDLE_PLEASE_TOKEN", "CUDDLE_PLEASE_TOKEN",
match &args.server { match &args.server {
SrcServer::Gitea { token, .. } => token, SrcServer::Gitea { token, .. } => token,
SrcServer::GitHub { token } => token, SrcServer::GitHub { token } => token,
}, },
), )
.id()
.await?,
) )
.with_workdir("/mnt/app") .with_workdir("/mnt/app")
.with_directory(".", self.client.host().directory(".")) .with_directory(".", client.host().directory(".").id().await?)
.with_unix_socket( .with_unix_socket(
"/tmp/ssh.sock", "/tmp/ssh.sock",
self.client.host().unix_socket( client
.host()
.unix_socket(
std::env::var("SSH_AUTH_SOCK").expect("expect SSH_AUTH_SOCK to be present"), 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_env_variable("SSH_AUTH_SOCK", "/tmp/ssh.sock")
.with_new_file_opts( .with_new_file_opts(
"/root/.ssh/config", "/root/.ssh/config",
dagger_sdk::ContainerWithNewFileOpts {
contents: Some(
" "
Host * Host *
User git
StrictHostKeyChecking no StrictHostKeyChecking no
UserKnownHostsFile /dev/null UserKnownHostsFile /dev/null
", ",
dagger_sdk::ContainerWithNewFileOpts { ),
owner: Some("root"), owner: Some("root"),
permissions: Some(700), permissions: Some(700),
expand: None,
}, },
); )
.with_exec(vec![
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", "cuddle-please",
"release", "release",
&format!( &format!(
@ -337,7 +337,10 @@ Host *
}, },
]); ]);
res.sync().await?; let exit_code = res.exit_code().await?;
if exit_code != 0 {
eyre::bail!("failed to run cuddle-please");
}
let please_out = res.stdout().await?; let please_out = res.stdout().await?;
println!("{please_out}"); println!("{please_out}");

View File

@ -1,16 +0,0 @@
[package]
name = "dagger-leptos"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
readme.workspace = true
repository.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dagger-sdk.workspace = true
eyre.workspace = true
async-trait.workspace = true
tokio.workspace = true

View File

@ -1 +0,0 @@

View File

@ -1,19 +0,0 @@
[package]
name = "dagger-rust"
description = "A common set of components for dagger-sdk, which enables patterns such as build, test and publish"
version.workspace = true
edition.workspace = true
authors.workspace = true
readme.workspace = true
license.workspace = true
repository.workspace = true
publish = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dagger-sdk.workspace = true
eyre.workspace = true
async-trait.workspace = true
tokio.workspace = true

View File

@ -1,366 +0,0 @@
use std::path::PathBuf;
use crate::source::RustSource;
#[allow(dead_code)]
pub struct RustBuild {
client: dagger_sdk::Query,
registry: Option<String>,
}
impl RustBuild {
pub fn new(client: dagger_sdk::Query) -> Self {
Self {
client,
registry: None,
}
}
pub async fn build(
&self,
source_path: Option<impl Into<PathBuf>>,
rust_version: impl AsRef<RustVersion>,
target: impl AsRef<BuildTarget>,
profile: impl AsRef<BuildProfile>,
crates: &[&str],
extra_deps: &[&str],
) -> eyre::Result<dagger_sdk::Container> {
let rust_version = rust_version.as_ref();
let target = target.as_ref();
let profile = profile.as_ref();
let source_path = source_path.map(|s| s.into());
let source = source_path.clone().unwrap_or(PathBuf::from("."));
let rust_source = RustSource::new(self.client.clone());
let (src, dep_src) = rust_source
.get_rust_src(source_path, crates.to_vec())
.await?;
let mut deps = vec!["apt", "install", "-y"];
deps.extend(extra_deps);
let rust_build_image = self
.client
.container()
.from(rust_version.to_string())
.with_exec(vec!["rustup", "target", "add", &target.to_string()])
.with_exec(vec!["apt", "update"])
.with_exec(vec!["wget", "https://github.com/rui314/mold/releases/latest/download/mold-2.3.3-x86_64-linux.tar.gz"])
.with_exec("tar -xvf mold-2.3.3-x86_64-linux.tar.gz".split_whitespace().collect())
.with_exec("mv mold-2.3.3-x86_64-linux/bin/mold /usr/bin/mold".split_whitespace().collect())
.with_exec(deps);
let target_cache = self.client.cache_volume(format!(
"rust_target_{}_{}",
profile.to_string(),
target.to_string()
));
let target_str = target.to_string();
let mut build_options = vec!["cargo", "build", "--target", &target_str, "--workspace"];
if matches!(profile, BuildProfile::Release) {
build_options.push("--release");
}
let rust_prebuild = rust_build_image
.with_workdir("/mnt/src")
.with_directory("/mnt/src", dep_src)
.with_exec(build_options)
.with_mounted_cache("/mnt/src/target/", target_cache);
let incremental_dir = rust_source
.get_rust_target_src(&source, rust_prebuild.clone(), crates.to_vec())
.await?;
let rust_with_src = rust_build_image
.with_workdir("/mnt/src")
.with_directory(
"/usr/local/cargo",
rust_prebuild.directory("/usr/local/cargo"),
)
.with_directory("/mnt/src/target", incremental_dir)
.with_directory("/mnt/src/", src);
Ok(rust_with_src)
}
pub async fn build_release(
&self,
source_path: Option<impl Into<PathBuf>>,
rust_version: impl AsRef<RustVersion>,
crates: &[&str],
extra_deps: &[&str],
images: impl IntoIterator<Item = SlimImage>,
bin_name: &str,
) -> eyre::Result<Vec<dagger_sdk::Container>> {
let images = images.into_iter().collect::<Vec<_>>();
let source_path = source_path.map(|s| s.into());
let mut containers = Vec::new();
for container_image in images {
let container = match &container_image {
SlimImage::Debian { image, deps, .. } => {
let target = BuildTarget::from_target(&container_image);
let build_container = self
.build(
source_path.clone(),
&rust_version,
BuildTarget::from_target(&container_image),
BuildProfile::Release,
crates,
extra_deps,
)
.await?;
let bin = build_container
.with_env_variable("SQLX_OFFLINE", "true")
.with_exec(vec!["cargo", "clean"])
.with_exec(vec![
"cargo",
"build",
"--target",
&target.to_string(),
"--release",
"-p",
bin_name,
])
.file(format!(
"target/{}/release/{}",
target.to_string(),
bin_name
));
self.build_debian_image(
bin,
image,
BuildTarget::from_target(&container_image),
deps.iter()
.map(|d| d.as_str())
.collect::<Vec<&str>>()
.as_slice(),
bin_name,
)
.await?
}
SlimImage::Alpine { image, deps, .. } => {
let target = BuildTarget::from_target(&container_image);
let build_container = self
.build(
source_path.clone(),
&rust_version,
BuildTarget::from_target(&container_image),
BuildProfile::Release,
crates,
extra_deps,
)
.await?;
let bin = build_container
.with_exec(vec![
"cargo",
"build",
"--target",
&target.to_string(),
"--release",
"-p",
bin_name,
])
.file(format!(
"target/{}/release/{}",
target.to_string(),
bin_name
));
self.build_alpine_image(
bin,
image,
BuildTarget::from_target(&container_image),
deps.iter()
.map(|d| d.as_str())
.collect::<Vec<&str>>()
.as_slice(),
bin_name,
)
.await?
}
};
containers.push(container);
}
Ok(containers)
}
async fn build_debian_image(
&self,
bin: dagger_sdk::File,
image: &str,
target: BuildTarget,
production_deps: &[&str],
bin_name: &str,
) -> eyre::Result<dagger_sdk::Container> {
let base_debian = self
.client
.container_opts(dagger_sdk::QueryContainerOpts {
platform: Some(target.into_platform()),
})
.from(image);
let mut packages = vec!["apt", "install", "-y"];
packages.extend_from_slice(production_deps);
let base_debian = base_debian
.with_exec(vec!["apt", "update"])
.with_exec(packages);
let final_image = base_debian
.with_file(format!("/usr/local/bin/{}", bin_name), bin)
.with_exec(vec![bin_name, "--help"]);
final_image.sync().await?;
Ok(final_image)
}
async fn build_alpine_image(
&self,
bin: dagger_sdk::File,
image: &str,
target: BuildTarget,
production_deps: &[&str],
bin_name: &str,
) -> eyre::Result<dagger_sdk::Container> {
let base_debian = self
.client
.container_opts(dagger_sdk::QueryContainerOpts {
platform: Some(target.into_platform()),
})
.from(image);
let mut packages = vec!["apk", "add"];
packages.extend_from_slice(production_deps);
let base_debian = base_debian.with_exec(packages);
let final_image = base_debian.with_file(format!("/usr/local/bin/{}", bin_name), bin);
Ok(final_image)
}
}
pub enum RustVersion {
Nightly,
Stable(String),
}
impl AsRef<RustVersion> for RustVersion {
fn as_ref(&self) -> &RustVersion {
self
}
}
impl ToString for RustVersion {
fn to_string(&self) -> String {
match self {
RustVersion::Nightly => "rustlang/rust:nightly".to_string(),
RustVersion::Stable(version) => format!("rust:{}", version),
}
}
}
pub enum BuildTarget {
LinuxAmd64,
LinuxArm64,
LinuxAmd64Musl,
LinuxArm64Musl,
MacOSAmd64,
MacOSArm64,
}
impl BuildTarget {
pub fn from_target(image: &SlimImage) -> Self {
match image {
SlimImage::Debian { architecture, .. } => match architecture {
BuildArchitecture::Amd64 => Self::LinuxAmd64,
BuildArchitecture::Arm64 => Self::LinuxArm64,
},
SlimImage::Alpine { architecture, .. } => match architecture {
BuildArchitecture::Amd64 => Self::LinuxAmd64Musl,
BuildArchitecture::Arm64 => Self::LinuxArm64Musl,
},
}
}
pub fn into_platform(&self) -> dagger_sdk::Platform {
let platform = match self {
BuildTarget::LinuxAmd64 => "linux/amd64",
BuildTarget::LinuxArm64 => "linux/arm64",
BuildTarget::LinuxAmd64Musl => "linux/amd64",
BuildTarget::LinuxArm64Musl => "linux/arm64",
BuildTarget::MacOSAmd64 => "darwin/amd64",
BuildTarget::MacOSArm64 => "darwin/arm64",
};
dagger_sdk::Platform(platform.into())
}
}
impl AsRef<BuildTarget> for BuildTarget {
fn as_ref(&self) -> &BuildTarget {
self
}
}
impl ToString for BuildTarget {
fn to_string(&self) -> String {
let target = match self {
BuildTarget::LinuxAmd64 => "x86_64-unknown-linux-gnu",
BuildTarget::LinuxArm64 => "aarch64-unknown-linux-gnu",
BuildTarget::LinuxAmd64Musl => "x86_64-unknown-linux-musl",
BuildTarget::LinuxArm64Musl => "aarch64-unknown-linux-musl",
BuildTarget::MacOSAmd64 => "x86_64-apple-darwin",
BuildTarget::MacOSArm64 => "aarch64-apple-darwin",
};
target.into()
}
}
pub enum BuildProfile {
Debug,
Release,
}
impl AsRef<BuildProfile> for BuildProfile {
fn as_ref(&self) -> &BuildProfile {
self
}
}
impl ToString for BuildProfile {
fn to_string(&self) -> String {
let profile = match self {
BuildProfile::Debug => "debug",
BuildProfile::Release => "release",
};
profile.into()
}
}
pub enum SlimImage {
Debian {
image: String,
deps: Vec<String>,
architecture: BuildArchitecture,
},
Alpine {
image: String,
deps: Vec<String>,
architecture: BuildArchitecture,
},
}
pub enum BuildArchitecture {
Amd64,
Arm64,
}

View File

@ -1,264 +0,0 @@
use std::path::PathBuf;
use crate::{
build::{BuildProfile, BuildTarget, RustVersion, SlimImage},
source::RustSource,
};
pub struct HtmxBuild {
client: dagger_sdk::Query,
registry: Option<String>,
}
impl HtmxBuild {
pub fn new(client: dagger_sdk::Query) -> Self {
Self {
client,
registry: None,
}
}
pub async fn build(
&self,
source_path: Option<impl Into<PathBuf>>,
rust_version: impl AsRef<RustVersion>,
profile: impl AsRef<BuildProfile>,
crates: &[&str],
extra_deps: &[&str],
) -> eyre::Result<dagger_sdk::Container> {
let source_path = source_path.map(|s| s.into()).unwrap_or(PathBuf::from("."));
let rust_version = rust_version.as_ref();
let profile = profile.as_ref();
let rust_source = RustSource::new(self.client.clone());
let (src, dep_src) = rust_source
.get_rust_src(Some(&source_path), crates.to_vec())
.await?;
let mut deps = vec!["apt", "install", "-y"];
deps.extend(extra_deps);
let rust_build_image = self
.client
.container()
.from(rust_version.to_string())
.with_exec(vec!["rustup", "target", "add", "wasm32-unknown-unknown"])
.with_exec(vec!["apt", "update"])
.with_exec(deps)
.with_exec(vec!["wget", "https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz"])
.with_exec("tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz".split_whitespace().collect())
.with_exec("mv cargo-binstall /usr/local/cargo/bin".split_whitespace().collect())
.with_exec(vec!["wget", "https://github.com/rui314/mold/releases/latest/download/mold-2.3.3-x86_64-linux.tar.gz"])
.with_exec("tar -xvf mold-2.3.3-x86_64-linux.tar.gz".split_whitespace().collect())
.with_exec("mv mold /usr/bin/mold".split_whitespace().collect())
.with_exec(vec!["cargo", "binstall", "sqlx-cli", "-y"]);
let target_cache = self
.client
.cache_volume(format!("rust_htmx_{}", profile.to_string()));
let build_options = vec!["cargo", "sqlx", "prepare"];
let rust_prebuild = rust_build_image
.with_workdir("/mnt/src")
.with_directory("/mnt/src", dep_src)
.with_exec(build_options)
.with_mounted_cache("/mnt/src/target/", target_cache);
let incremental_dir = rust_source
.get_rust_target_src(&source_path, rust_prebuild.clone(), crates.to_vec())
.await?;
let rust_with_src = rust_build_image
.with_workdir("/mnt/src")
.with_directory(
"/usr/local/cargo",
rust_prebuild.directory("/usr/local/cargo"),
)
.with_directory("/mnt/src/target", incremental_dir)
.with_directory("/mnt/src/", src);
Ok(rust_with_src)
}
pub async fn build_release(
&self,
source_path: Option<impl Into<PathBuf>>,
rust_version: impl AsRef<RustVersion>,
crates: &[&str],
extra_deps: &[&str],
images: impl IntoIterator<Item = SlimImage>,
bin_name: &str,
) -> eyre::Result<Vec<dagger_sdk::Container>> {
let images = images.into_iter().collect::<Vec<_>>();
let source_path = source_path.map(|s| s.into());
let postgres_password = "somepassword123";
let postgres = self
.client
.container()
.from("postgres:16.1")
.with_env_variable("POSTGRES_PASSWORD", postgres_password);
let postgres_service = postgres.with_exposed_port(5432);
let mut containers = Vec::new();
for container_image in images {
let container =
match &container_image {
SlimImage::Debian { image, deps, .. } => {
let _target = BuildTarget::from_target(&container_image);
let build_container = self
.build(
source_path.clone(),
&rust_version,
BuildProfile::Release,
crates,
extra_deps,
)
.await?;
let binary_build = build_container
.with_service_binding("postgres", postgres_service.as_service())
.with_env_variable(
"DATABASE_URL",
"root:somepassword123@postgres:5432/postgres",
)
.with_exec(vec!["cargo", "sqlx", "migrate", "run"])
.with_exec(vec!["cargo", "sqlx", "prepare"])
.with_exec(vec!["cargo", "build", "--release", "--bin", bin_name]);
self.build_debian_image(
binary_build,
image,
BuildTarget::from_target(&container_image),
deps.iter()
.map(|d| d.as_str())
.collect::<Vec<&str>>()
.as_slice(),
bin_name,
)
.await?
}
SlimImage::Alpine { image, deps, .. } => {
let target = BuildTarget::from_target(&container_image);
let build_container = self
.build(
source_path.clone(),
&rust_version,
BuildProfile::Release,
crates,
extra_deps,
)
.await?;
let bin = build_container
.with_exec(vec![
"cargo",
"build",
"--target",
&target.to_string(),
"--release",
"-p",
bin_name,
])
.file(format!("target/{}/release/{}", target.to_string(), bin_name));
self.build_alpine_image(
bin,
image,
BuildTarget::from_target(&container_image),
deps.iter()
.map(|d| d.as_str())
.collect::<Vec<&str>>()
.as_slice(),
bin_name,
)
.await?
}
};
containers.push(container);
}
Ok(containers)
}
async fn build_debian_image(
&self,
builder_image: dagger_sdk::Container,
image: &str,
target: BuildTarget,
production_deps: &[&str],
bin_name: &str,
) -> eyre::Result<dagger_sdk::Container> {
let base_debian = self
.client
.container_opts(dagger_sdk::QueryContainerOpts {
platform: Some(target.into_platform()),
})
.from(image);
let mut packages = vec!["apt", "install", "-y"];
packages.extend_from_slice(production_deps);
let base_debian = base_debian
.with_exec(vec!["apt", "update"])
.with_exec(packages);
let final_image = base_debian
.with_workdir("/mnt/app")
.with_file(
format!("/mnt/app/{bin_name}"),
builder_image.file(format!("/mnt/src/target/release/{bin_name}")),
)
.with_directory(
"/mnt/app/target/site",
builder_image.directory("/mnt/src/target/site".to_string()),
)
.with_file(
"/mnt/app/Cargo.toml",
builder_image.file(format!("/mnt/src/crates/{bin_name}/Cargo.toml")),
)
.with_env_variable("RUST_LOG", "debug")
.with_env_variable("APP_ENVIRONMENT", "production")
.with_env_variable("LEPTOS_OUTPUT_NAME", bin_name)
.with_env_variable("LEPTOS_SITE_ADDR", "0.0.0.0:8080")
.with_env_variable("LEPTOS_SITE_ROOT", "site")
.with_env_variable("LEPTOS_SITE_PKG_DIR", "pkg")
.with_exposed_port(8080)
.with_entrypoint(vec![format!("/mnt/app/{bin_name}")]);
final_image.sync().await?;
Ok(final_image)
}
async fn build_alpine_image(
&self,
bin: dagger_sdk::File,
image: &str,
target: BuildTarget,
production_deps: &[&str],
bin_name: &str,
) -> eyre::Result<dagger_sdk::Container> {
let base_debian = self
.client
.container_opts(dagger_sdk::QueryContainerOpts {
platform: Some(target.into_platform()),
})
.from(image);
let mut packages = vec!["apk", "add"];
packages.extend_from_slice(production_deps);
let base_debian = base_debian.with_exec(packages);
let final_image = base_debian.with_file(format!("/usr/local/bin/{}", bin_name), bin);
Ok(final_image)
}
}

View File

@ -1,251 +0,0 @@
use std::path::PathBuf;
use crate::{
build::{BuildProfile, BuildTarget, RustVersion, SlimImage},
source::RustSource,
};
pub struct LeptosBuild {
client: dagger_sdk::Query,
registry: Option<String>,
}
impl LeptosBuild {
pub fn new(client: dagger_sdk::Query) -> Self {
Self {
client,
registry: None,
}
}
pub async fn build(
&self,
source_path: Option<impl Into<PathBuf>>,
rust_version: impl AsRef<RustVersion>,
profile: impl AsRef<BuildProfile>,
crates: &[&str],
extra_deps: &[&str],
) -> eyre::Result<dagger_sdk::Container> {
let source_path = source_path.map(|s| s.into()).unwrap_or(PathBuf::from("."));
let rust_version = rust_version.as_ref();
let profile = profile.as_ref();
let rust_source = RustSource::new(self.client.clone());
let (src, dep_src) = rust_source
.get_rust_src(Some(&source_path), crates.to_vec())
.await?;
let mut deps = vec!["apt", "install", "-y"];
deps.extend(extra_deps);
let rust_build_image = self
.client
.container()
.from(rust_version.to_string())
.with_exec(vec!["rustup", "target", "add", "wasm32-unknown-unknown"])
.with_exec(vec!["apt", "update"])
.with_exec(deps)
.with_exec(vec!["wget", "https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz"])
.with_exec("tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz".split_whitespace().collect())
.with_exec("mv cargo-binstall /usr/local/cargo/bin".split_whitespace().collect())
.with_exec(vec!["cargo", "binstall", "cargo-leptos", "-y"]);
let target_cache = self
.client
.cache_volume(format!("rust_leptos_{}", profile.to_string()));
let build_options = vec!["cargo", "leptos", "build", "--release", "-vv"];
let rust_prebuild = rust_build_image
.with_workdir("/mnt/src")
.with_directory("/mnt/src", dep_src)
.with_exec(build_options)
.with_mounted_cache("/mnt/src/target/", target_cache);
let incremental_dir = rust_source
.get_rust_target_src(&source_path, rust_prebuild.clone(), crates.to_vec())
.await?;
let rust_with_src = rust_build_image
.with_workdir("/mnt/src")
.with_directory(
"/usr/local/cargo",
rust_prebuild.directory("/usr/local/cargo"),
)
.with_directory("/mnt/src/target", incremental_dir)
.with_directory("/mnt/src/", src);
Ok(rust_with_src)
}
pub async fn build_release(
&self,
source_path: Option<impl Into<PathBuf>>,
rust_version: impl AsRef<RustVersion>,
crates: &[&str],
extra_deps: &[&str],
images: impl IntoIterator<Item = SlimImage>,
bin_name: &str,
) -> eyre::Result<Vec<dagger_sdk::Container>> {
let images = images.into_iter().collect::<Vec<_>>();
let source_path = source_path.map(|s| s.into());
let mut containers = Vec::new();
for container_image in images {
let container = match &container_image {
SlimImage::Debian { image, deps, .. } => {
let _target = BuildTarget::from_target(&container_image);
let build_container = self
.build(
source_path.clone(),
&rust_version,
BuildProfile::Release,
crates,
extra_deps,
)
.await?;
let binary_build = build_container.with_exec(vec![
"cargo",
"leptos",
"build",
"--release",
"-vv",
]);
self.build_debian_image(
binary_build,
image,
BuildTarget::from_target(&container_image),
deps.iter()
.map(|d| d.as_str())
.collect::<Vec<&str>>()
.as_slice(),
bin_name,
)
.await?
}
SlimImage::Alpine { image, deps, .. } => {
let target = BuildTarget::from_target(&container_image);
let build_container = self
.build(
source_path.clone(),
&rust_version,
BuildProfile::Release,
crates,
extra_deps,
)
.await?;
let bin = build_container
.with_exec(vec![
"cargo",
"build",
"--target",
&target.to_string(),
"--release",
"-p",
bin_name,
])
.file(format!(
"target/{}/release/{}",
target.to_string(),
bin_name
));
self.build_alpine_image(
bin,
image,
BuildTarget::from_target(&container_image),
deps.iter()
.map(|d| d.as_str())
.collect::<Vec<&str>>()
.as_slice(),
bin_name,
)
.await?
}
};
containers.push(container);
}
Ok(containers)
}
async fn build_debian_image(
&self,
builder_image: dagger_sdk::Container,
image: &str,
target: BuildTarget,
production_deps: &[&str],
bin_name: &str,
) -> eyre::Result<dagger_sdk::Container> {
let base_debian = self
.client
.container_opts(dagger_sdk::QueryContainerOpts {
platform: Some(target.into_platform()),
})
.from(image);
let mut packages = vec!["apt", "install", "-y"];
packages.extend_from_slice(production_deps);
let base_debian = base_debian
.with_exec(vec!["apt", "update"])
.with_exec(packages);
let final_image = base_debian
.with_workdir("/mnt/app")
.with_file(
format!("/mnt/app/{bin_name}"),
builder_image.file(format!("/mnt/src/target/release/{bin_name}")),
)
.with_directory(
"/mnt/app/target/site",
builder_image.directory("/mnt/src/target/site".to_string()),
)
.with_file(
"/mnt/app/Cargo.toml",
builder_image.file(format!("/mnt/src/crates/{bin_name}/Cargo.toml")),
)
.with_env_variable("RUST_LOG", "debug")
.with_env_variable("APP_ENVIRONMENT", "production")
.with_env_variable("LEPTOS_OUTPUT_NAME", bin_name)
.with_env_variable("LEPTOS_SITE_ADDR", "0.0.0.0:8080")
.with_env_variable("LEPTOS_SITE_ROOT", "site")
.with_env_variable("LEPTOS_SITE_PKG_DIR", "pkg")
.with_exposed_port(8080)
.with_entrypoint(vec![format!("/mnt/app/{bin_name}")]);
final_image.sync().await?;
Ok(final_image)
}
async fn build_alpine_image(
&self,
bin: dagger_sdk::File,
image: &str,
target: BuildTarget,
production_deps: &[&str],
bin_name: &str,
) -> eyre::Result<dagger_sdk::Container> {
let base_debian = self
.client
.container_opts(dagger_sdk::QueryContainerOpts {
platform: Some(target.into_platform()),
})
.from(image);
let mut packages = vec!["apk", "add"];
packages.extend_from_slice(production_deps);
let base_debian = base_debian.with_exec(packages);
let final_image = base_debian.with_file(format!("/usr/local/bin/{}", bin_name), bin);
Ok(final_image)
}
}

View File

@ -1,6 +0,0 @@
pub mod build;
pub mod htmx;
pub mod leptos;
pub mod publish;
pub mod source;
pub mod test;

View File

@ -1,40 +0,0 @@
use std::sync::Arc;
pub struct RustPublish {
client: Arc<dagger_sdk::Query>,
}
impl RustPublish {
pub fn new(client: Arc<dagger_sdk::Query>) -> Self {
Self { client }
}
pub async fn publish(
&self,
image: impl Into<String>,
tag: impl Into<String>,
containers: impl IntoIterator<Item = dagger_sdk::Container>,
) -> eyre::Result<()> {
let mut ids = Vec::new();
for container in containers.into_iter() {
let id = container.id().await?;
ids.push(id);
}
let image = self
.client
.container()
.publish_opts(
format!("{}:{}", image.into(), tag.into()),
dagger_sdk::ContainerPublishOpts {
platform_variants: Some(ids),
forced_compression: None,
media_types: None,
},
)
.await?;
println!("published: {}", image);
Ok(())
}
}

View File

@ -1,201 +0,0 @@
use std::path::{Path, PathBuf};
use eyre::Context;
pub struct RustSource {
client: dagger_sdk::Query,
exclude: Vec<String>,
}
impl RustSource {
pub fn new(client: dagger_sdk::Query) -> Self {
Self {
client,
exclude: vec!["node_modules/", ".git/", "target/", ".cuddle/"]
.into_iter()
.map(|s| s.to_string())
.collect(),
}
}
pub fn with_exclude(
&mut self,
exclude: impl IntoIterator<Item = impl Into<String>>,
) -> &mut Self {
self.exclude = exclude.into_iter().map(|s| s.into()).collect();
self
}
pub fn append_exclude(
&mut self,
exclude: impl IntoIterator<Item = impl Into<String>>,
) -> &mut Self {
self.exclude
.append(&mut exclude.into_iter().map(|s| s.into()).collect::<Vec<_>>());
self
}
pub async fn get_rust_src<T, I>(
&self,
source: Option<T>,
crate_paths: I,
) -> eyre::Result<(dagger_sdk::Directory, dagger_sdk::Directory)>
where
T: Into<PathBuf>,
T: Clone,
I: IntoIterator,
I::Item: Into<String>,
{
let source_path = match source.clone() {
Some(s) => s.into(),
None => PathBuf::from("."),
};
let (skeleton_files, _crates) = self
.get_rust_skeleton_files(&source_path, crate_paths)
.await?;
let src = self.get_src(source.clone()).await?;
let rust_src = self.get_rust_dep_src(source).await?;
let rust_src = rust_src.with_directory(".", skeleton_files);
Ok((src, rust_src))
}
pub async fn get_src(
&self,
source: Option<impl Into<PathBuf>>,
) -> eyre::Result<dagger_sdk::Directory> {
let source = source.map(|s| s.into()).unwrap_or(PathBuf::from("."));
let directory = self.client.host().directory_opts(
source.display().to_string(),
dagger_sdk::HostDirectoryOptsBuilder::default()
.exclude(self.exclude.iter().map(|s| s.as_str()).collect::<Vec<_>>())
.build()?,
);
Ok(directory)
}
pub async fn get_rust_dep_src(
&self,
source: Option<impl Into<PathBuf>>,
) -> eyre::Result<dagger_sdk::Directory> {
let source = source.map(|s| s.into()).unwrap_or(PathBuf::from("."));
let mut excludes = self.exclude.clone();
excludes.push("**/src".to_string());
excludes.push("**/tests".to_string());
let directory = self.client.host().directory_opts(
source.display().to_string(),
dagger_sdk::HostDirectoryOptsBuilder::default()
//.include(vec!["**/Cargo.toml", "**/Cargo.lock"])
.exclude(excludes.iter().map(|s| s.as_str()).collect::<Vec<_>>())
.build()?,
);
Ok(directory)
}
pub async fn get_rust_target_src(
&self,
source_path: &Path,
container: dagger_sdk::Container,
crate_paths: impl IntoIterator<Item = impl Into<String>>,
) -> eyre::Result<dagger_sdk::Directory> {
let (_skeleton_files, crates) = self
.get_rust_skeleton_files(source_path, crate_paths)
.await?;
let exclude = crates
.iter()
.map(|c| format!("**/*{}*", c.replace('-', "_")))
.collect::<Vec<_>>();
let mut original_crates = crates.clone();
original_crates.extend(exclude);
let exclude = original_crates.iter().map(|c| c.as_str()).collect();
let incremental_dir = self.client.directory().with_directory_opts(
".",
container.directory("target"),
dagger_sdk::DirectoryWithDirectoryOpts {
exclude: Some(exclude),
include: None,
},
);
Ok(incremental_dir)
}
pub async fn get_rust_skeleton_files(
&self,
source_path: &Path,
crate_paths: impl IntoIterator<Item = impl Into<String>>,
) -> eyre::Result<(dagger_sdk::Directory, Vec<String>)> {
let paths = crate_paths
.into_iter()
.map(|s| s.into())
.collect::<Vec<String>>();
let mut crates = Vec::new();
for path in paths {
if path.ends_with("/*") {
let mut dirs = tokio::fs::read_dir(source_path.join(path.trim_end_matches("/*")))
.await
.context(format!("failed to find path: {}", path.clone()))?;
while let Some(entry) = dirs.next_entry().await? {
if entry.metadata().await?.is_dir() {
crates.push(entry.path());
}
}
} else {
crates.push(PathBuf::from(path));
}
}
fn create_skeleton_files(
directory: dagger_sdk::Directory,
path: &Path,
) -> eyre::Result<dagger_sdk::Directory> {
let main_content = r#"
#[allow(dead_code)]
fn main() { panic!("should never be executed"); }"#;
let lib_content = r#"
#[allow(dead_code)]
fn some() { panic!("should never be executed"); }"#;
let directory = directory.with_new_file(
path.join("src").join("main.rs").display().to_string(),
main_content,
);
let directory = directory.with_new_file(
path.join("src").join("lib.rs").display().to_string(),
lib_content,
);
Ok(directory)
}
let mut directory = self.client.directory();
let mut crate_names = Vec::new();
for rust_crate in crates.iter() {
if let Some(file_name) = rust_crate.file_name() {
crate_names.push(file_name.to_str().unwrap().to_string());
}
directory = create_skeleton_files(
directory,
rust_crate.strip_prefix(source_path).unwrap_or(rust_crate),
)?;
}
Ok((directory, crate_names))
}
}

View File

@ -1,75 +0,0 @@
use std::{path::PathBuf};
use crate::{build::RustVersion, source::RustSource};
pub struct RustTest {
client: dagger_sdk::Query,
registry: Option<String>,
}
impl RustTest {
pub fn new(client: dagger_sdk::Query) -> Self {
Self {
client,
registry: None,
}
}
pub async fn test(
&self,
source_path: Option<impl Into<PathBuf>>,
rust_version: impl AsRef<RustVersion>,
crates: &[&str],
extra_deps: &[&str],
) -> eyre::Result<()> {
let rust_version = rust_version.as_ref();
let source_path = source_path.map(|s| s.into());
let source = source_path.clone().unwrap_or(PathBuf::from("."));
let rust_source = RustSource::new(self.client.clone());
let (src, dep_src) = rust_source
.get_rust_src(source_path, crates.to_vec())
.await?;
let mut deps = vec!["apt", "install", "-y"];
deps.extend(extra_deps);
let rust_build_image = self
.client
.container()
.from(rust_version.to_string())
.with_exec(vec!["apt", "update"])
.with_exec(deps);
let target_cache = self.client.cache_volume("rust_target_test".to_string());
let build_options = vec!["cargo", "build", "--workspace"];
let rust_prebuild = rust_build_image
.with_workdir("/mnt/src")
.with_directory("/mnt/src", dep_src)
.with_exec(build_options)
.with_mounted_cache("/mnt/src/target/", target_cache);
let incremental_dir = rust_source
.get_rust_target_src(&source, rust_prebuild.clone(), crates.to_vec())
.await?;
let rust_with_src = rust_build_image
.with_workdir("/mnt/src")
.with_directory(
"/usr/local/cargo",
rust_prebuild.directory("/usr/local/cargo"),
)
.with_directory("/mnt/src/target", incremental_dir)
.with_directory("/mnt/src/", src);
let test = rust_with_src.with_exec(vec!["cargo", "test"]);
let stdout = test.stdout().await?;
let stderr = test.stderr().await?;
println!("stdout: {}, stderr: {}", stdout, stderr);
test.sync().await?;
Ok(())
}
}

View File

@ -6,8 +6,6 @@ vars:
service: "dagger-components" service: "dagger-components"
registry: kasperhermansen registry: kasperhermansen
cuddle_please_image: "kasperhermansen/cuddle-please:main-1691504183"
please: please:
project: project:
owner: kjuulh owner: kjuulh
@ -15,8 +13,6 @@ please:
branch: main branch: main
settings: settings:
api_url: https://git.front.kjuulh.io api_url: https://git.front.kjuulh.io
actions:
rust:
scripts: scripts:
"ci:main": "ci:main":

View File

@ -2,7 +2,8 @@ use dagger_cuddle_please::{models::CuddlePleaseSrcArgs, DaggerCuddlePleaseAction
#[tokio::main] #[tokio::main]
pub async fn main() -> eyre::Result<()> { pub async fn main() -> eyre::Result<()> {
dagger_sdk::connect(|client| async move { let client = dagger_sdk::connect().await?;
DaggerCuddlePleaseAction::dagger(client.clone()) DaggerCuddlePleaseAction::dagger(client.clone())
.execute_src(&CuddlePleaseSrcArgs { .execute_src(&CuddlePleaseSrcArgs {
cuddle_image: "kasperhermansen/cuddle-please:main-1691504183".into(), cuddle_image: "kasperhermansen/cuddle-please:main-1691504183".into(),
@ -14,9 +15,5 @@ pub async fn main() -> eyre::Result<()> {
}) })
.await?; .await?;
Ok(())
})
.await?;
Ok(()) Ok(())
} }

View File

@ -2,8 +2,9 @@ use dagger_cuddle_please::{models::CuddlePleaseArgs, DaggerCuddlePleaseAction};
#[tokio::main] #[tokio::main]
pub async fn main() -> eyre::Result<()> { pub async fn main() -> eyre::Result<()> {
dagger_sdk::connect(|client| async move { let client = dagger_sdk::connect().await?;
DaggerCuddlePleaseAction::dagger(client)
DaggerCuddlePleaseAction::dagger(client.clone())
.execute(&CuddlePleaseArgs { .execute(&CuddlePleaseArgs {
repository: "dagger-components".into(), repository: "dagger-components".into(),
owner: "kjuulh".into(), owner: "kjuulh".into(),
@ -22,7 +23,4 @@ pub async fn main() -> eyre::Result<()> {
.await?; .await?;
Ok(()) Ok(())
})
.await?;
Ok(())
} }

View File

@ -1,17 +0,0 @@
[package]
name = "htmx"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
readme.workspace = true
repository.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dagger-rust.workspace = true
eyre.workspace = true
dagger-sdk.workspace = true
tokio.workspace = true

View File

@ -1,18 +0,0 @@
use std::path::PathBuf;
#[tokio::main]
pub async fn main() -> eyre::Result<()> {
dagger_sdk::connect(|client| async move {
let crates = ["some-crate"];
let dag = dagger_rust::source::RustSource::new(client.clone());
let (_src, _rust_src) = dag.get_rust_src(None::<PathBuf>, crates).await?;
let _full_src = dag
.get_rust_target_src(&PathBuf::from("."), client.container(), crates)
.await?;
Ok(())
})
.await?;
Ok(())
}

View File

@ -1,13 +0,0 @@
[package]
name = "leptos-build"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dagger-rust.workspace = true
eyre.workspace = true
dagger-sdk.workspace = true
tokio.workspace = true

View File

@ -1,97 +0,0 @@
[package]
name = "hackernews_axum"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[profile.release]
codegen-units = 1
lto = true
[dependencies]
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
cfg-if = "1.0.0"
leptos = { version = "0.5.1", features = ["nightly"] }
leptos_axum = { version = "0.5.1", optional = true }
leptos_meta = { version = "0.5.1", features = ["nightly"] }
leptos_router = { version = "0.5.1", features = ["nightly"] }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
tracing = "0.1"
gloo-net = { version = "0.2.5", features = ["http"] }
reqwest = { version = "0.11.13", features = ["json"] }
axum = { version = "0.6.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.4", features = ["fs"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.8", optional = true }
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
wasm-bindgen = "0.2"
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"dep:http",
"leptos/ssr",
"leptos_axum",
"leptos_meta/ssr",
"leptos_router/ssr",
]
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "http", "leptos_axum"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "hackernews_axum"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "0.0.0.0:8080"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
end2end-cmd = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false

View File

@ -1,64 +0,0 @@
use dagger_rust::build::{RustVersion, SlimImage};
#[tokio::main]
pub async fn main() -> eyre::Result<()> {
dagger_sdk::connect(|client| async move {
let rust_build = dagger_rust::leptos::LeptosBuild::new(client.clone());
let containers = rust_build
.build_release(
Some("testdata"),
RustVersion::Nightly,
&["crates/*"],
&[
"openssl",
"libssl-dev",
"pkg-config",
"musl-tools",
"ca-certificates",
],
vec![SlimImage::Debian {
image: "debian:bullseye".into(),
deps: vec![
"openssl".into(),
"libssl-dev".into(),
"pkg-config".into(),
"musl-tools".into(),
"ca-certificates".into(),
],
architecture: dagger_rust::build::BuildArchitecture::Amd64,
}],
"hackernews_axum",
)
.await?;
let container = containers.first().unwrap();
container.directory("/mnt/app").export("output").await?;
let tunnel = client.host().tunnel(
container
.with_env_variable("LEPTOS_SITE_ADDR", "0.0.0.0:8080")
.with_exec(vec!["/mnt/app/hackernews_axum"])
.as_service(),
);
tunnel.start().await?;
let endpoint = tunnel
.endpoint_opts(
dagger_sdk::ServiceEndpointOptsBuilder::default()
.scheme("http")
.build()?,
)
.await?;
println!("running on: {endpoint}, press enter to stop");
std::io::stdin().read_line(&mut String::new()).unwrap();
Ok(())
})
.await?;
Ok(())
}

File diff suppressed because it is too large Load Diff

View File

@ -1,56 +0,0 @@
[workspace]
members = ["crates/*"]
resolver = "2"
[profile.release]
codegen-units = 1
lto = true
[[workspace.metadata.leptos]]
name = "hackernews_axum"
bin-package = "hackernews_axum"
lib-package = "hackernews_axum"
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "hackernews_axum"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "./crates/hackernews_axum/style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "./crates/hackernews_axum/public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "0.0.0.0:8080"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
end2end-cmd = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false

View File

@ -1 +0,0 @@
/target

View File

@ -1,97 +0,0 @@
[package]
name = "hackernews_axum"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[profile.release]
codegen-units = 1
lto = true
[dependencies]
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
cfg-if = "1.0.0"
leptos = { version = "0.5.1", features = ["nightly"] }
leptos_axum = { version = "0.5.1", optional = true }
leptos_meta = { version = "0.5.1", features = ["nightly"] }
leptos_router = { version = "0.5.1", features = ["nightly"] }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
tracing = "0.1"
gloo-net = { version = "0.2.5", features = ["http"] }
reqwest = { version = "0.11.13", features = ["json"] }
axum = { version = "0.6.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.4", features = ["fs"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.8", optional = true }
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
wasm-bindgen = "0.2"
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"dep:http",
"leptos/ssr",
"leptos_axum",
"leptos_meta/ssr",
"leptos_router/ssr",
]
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "http", "leptos_axum"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "hackernews_axum"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "0.0.0.0:8080"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
end2end-cmd = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 Greg Johnston
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,8 +0,0 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/cargo-leptos.toml" },
]
[env]
CLIENT_PROCESS_NAME = "hackernews_axum"

View File

@ -1,7 +0,0 @@
# Leptos Hacker News Example with Axum
This example creates a basic clone of the Hacker News site. It showcases Leptos' ability to create both a client-side rendered app, and a server side rendered app with hydration, in a single repository. This repo differs from the main Hacker News example by using Axum as it's server.
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.

View File

@ -1,8 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="css" href="/style.css"/>
</head>
<body></body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,90 +0,0 @@
use leptos::Serializable;
use serde::{Deserialize, Serialize};
pub fn story(path: &str) -> String {
format!("https://node-hnapi.herokuapp.com/{path}")
}
pub fn user(path: &str) -> String {
format!("https://hacker-news.firebaseio.com/v0/user/{path}.json")
}
#[cfg(not(feature = "ssr"))]
pub async fn fetch_api<T>(path: &str) -> Option<T>
where
T: Serializable,
{
let abort_controller = web_sys::AbortController::new().ok();
let abort_signal = abort_controller.as_ref().map(|a| a.signal());
// abort in-flight requests if e.g., we've navigated away from this page
leptos::on_cleanup(move || {
if let Some(abort_controller) = abort_controller {
abort_controller.abort()
}
});
let json = gloo_net::http::Request::get(path)
.abort_signal(abort_signal.as_ref())
.send()
.await
.map_err(|e| log::error!("{e}"))
.ok()?
.text()
.await
.ok()?;
T::de(&json).ok()
}
#[cfg(feature = "ssr")]
pub async fn fetch_api<T>(path: &str) -> Option<T>
where
T: Serializable,
{
let json = reqwest::get(path)
.await
.map_err(|e| log::error!("{e}"))
.ok()?
.text()
.await
.ok()?;
T::de(&json).map_err(|e| log::error!("{e}")).ok()
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
pub struct Story {
pub id: usize,
pub title: String,
pub points: Option<i32>,
pub user: Option<String>,
pub time: usize,
pub time_ago: String,
#[serde(alias = "type")]
pub story_type: String,
pub url: String,
#[serde(default)]
pub domain: String,
#[serde(default)]
pub comments: Option<Vec<Comment>>,
pub comments_count: Option<usize>,
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
pub struct Comment {
pub id: usize,
pub level: usize,
pub user: Option<String>,
pub time: usize,
pub time_ago: String,
pub content: Option<String>,
pub comments: Vec<Comment>,
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
pub struct User {
pub created: usize,
pub id: String,
pub karma: i32,
pub about: Option<String>,
}

View File

@ -1,28 +0,0 @@
use leptos::{view, Errors, For, IntoView, RwSignal, View};
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
// here than just displaying them
pub fn error_template(errors: Option<RwSignal<Errors>>) -> View {
let Some(errors) = errors else {
panic!("No Errors found and we expected errors!");
};
view! {
<h1>"Errors"</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
each=errors
// a unique key for each item as a reference
key=|(key, _)| key.clone()
// renders each item to a view
children=move | (_, error)| {
let error_string = error.to_string();
view! {
<p>"Error: " {error_string}</p>
}
}
/>
}
.into_view()
}

View File

@ -1,44 +0,0 @@
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::State,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
use axum::response::Response as AxumResponse;
use tower::ServiceExt;
use tower_http::services::ServeDir;
use leptos::{LeptosOptions};
use crate::error_template::error_template;
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else{
let handler = leptos_axum::render_app_to_stream(options.to_owned(), || error_template( None));
handler(req).await.into_response()
}
}
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
}
}
}

View File

@ -1,63 +0,0 @@
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
http::{Request, Response, StatusCode, Uri},
};
use tower::ServiceExt;
use tower_http::services::ServeDir;
pub async fn file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
let res = get_static_file(uri.clone(), "/pkg").await?;
if res.status() == StatusCode::NOT_FOUND {
// try with `.html`
// TODO: handle if the Uri has query parameters
match format!("{}.html", uri).parse() {
Ok(uri_html) => get_static_file(uri_html, "/pkg").await,
Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string())),
}
} else {
Ok(res)
}
}
pub async fn get_static_file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
let res = get_static_file(uri.clone(), "/static").await?;
if res.status() == StatusCode::NOT_FOUND {
Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string()))
} else {
Ok(res)
}
}
async fn get_static_file(uri: Uri, base: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(&uri).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// When run normally, the root should be the crate root
if base == "/static" {
match ServeDir::new("./static").oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
))
}
} else if base == "/pkg" {
match ServeDir::new("./pkg").oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
} else{
Err((StatusCode::NOT_FOUND, "Not Found".to_string()))
}
}
}
}

View File

@ -1,49 +0,0 @@
use cfg_if::cfg_if;
use leptos::{component, view, IntoView};
use leptos_meta::*;
use leptos_router::*;
mod api;
pub mod error_template;
pub mod fallback;
pub mod handlers;
mod routes;
use routes::{nav::*, stories::*, story::*, users::*};
#[component]
pub fn App() -> impl IntoView {
provide_meta_context();
view! {
<>
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Stylesheet id="leptos" href="/pkg/hackernews_axum.css"/>
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
<Router>
<Nav />
<main>
<Routes>
<Route path="users/:id" view=User/>
<Route path="stories/:id" view=Story/>
<Route path=":stories?" view=Stories/>
</Routes>
</main>
</Router>
</>
}
}
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(move || {
view! { <App/> }
});
}
}
}

View File

@ -1,54 +0,0 @@
use cfg_if::cfg_if;
use leptos::{logging::log, *};
// boilerplate to run in different modes
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
Router,
routing::get,
};
use leptos_axum::{generate_route_list, LeptosRoutes};
use hackernews_axum::fallback::file_and_error_handler;
#[tokio::main]
async fn main() {
use hackernews_axum::*;
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(App);
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
// build our application with a route
let app = Router::new()
.route("/favicon.ico", get(file_and_error_handler))
.leptos_routes(&leptos_options, routes, || view! { <App/> } )
.fallback(file_and_error_handler)
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
}
// client-only stuff for Trunk
else {
use hackernews_axum::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|| {
view! { <App/> }
});
}
}
}

View File

@ -1,4 +0,0 @@
pub mod nav;
pub mod stories;
pub mod story;
pub mod users;

View File

@ -1,30 +0,0 @@
use leptos::{component, view, IntoView};
use leptos_router::*;
#[component]
pub fn Nav() -> impl IntoView {
view! {
<header class="header">
<nav class="inner">
<A href="/">
<strong>"HN"</strong>
</A>
<A href="/new">
<strong>"New"</strong>
</A>
<A href="/show">
<strong>"Show"</strong>
</A>
<A href="/ask">
<strong>"Ask"</strong>
</A>
<A href="/job">
<strong>"Jobs"</strong>
</A>
<a class="github" href="http://github.com/leptos-rs/leptos" target="_blank" rel="noreferrer">
"Built with Leptos"
</a>
</nav>
</header>
}
}

View File

@ -1,156 +0,0 @@
use crate::api;
use leptos::*;
use leptos_router::*;
fn category(from: &str) -> &'static str {
match from {
"new" => "newest",
"show" => "show",
"ask" => "ask",
"job" => "jobs",
_ => "news",
}
}
#[component]
pub fn Stories() -> impl IntoView {
let query = use_query_map();
let params = use_params_map();
let page = move || {
query
.with(|q| q.get("page").and_then(|page| page.parse::<usize>().ok()))
.unwrap_or(1)
};
let story_type = move || {
params
.with(|p| p.get("stories").cloned())
.unwrap_or_else(|| "top".to_string())
};
let stories = create_resource(
move || (page(), story_type()),
move |(page, story_type)| async move {
let path = format!("{}?page={}", category(&story_type), page);
api::fetch_api::<Vec<api::Story>>(&api::story(&path)).await
},
);
let (pending, set_pending) = create_signal(false);
let hide_more_link = move || {
stories.get().unwrap_or(None).unwrap_or_default().len() < 28
|| pending()
};
view! {
<div class="news-view">
<div class="news-list-nav">
<span>
{move || if page() > 1 {
view! {
<a class="page-link"
href=move || format!("/{}?page={}", story_type(), page() - 1)
attr:aria_label="Previous Page"
>
"< prev"
</a>
}.into_any()
} else {
view! {
<span class="page-link disabled" aria-hidden="true">
"< prev"
</span>
}.into_any()
}}
</span>
<span>"page " {page}</span>
<span class="page-link"
class:disabled=hide_more_link
aria-hidden=hide_more_link
>
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
aria-label="Next Page"
>
"more >"
</a>
</span>
</div>
<main class="news-list">
<div>
<Transition
fallback=move || view! { <p>"Loading..."</p> }
set_pending
>
{move || match stories.get() {
None => None,
Some(None) => Some(view! { <p>"Error loading stories."</p> }.into_any()),
Some(Some(stories)) => {
Some(view! {
<ul>
<For
each=move || stories.clone()
key=|story| story.id
let:story
>
<Story story/>
</For>
</ul>
}.into_any())
}
}}
</Transition>
</div>
</main>
</div>
}
}
#[component]
fn Story(story: api::Story) -> impl IntoView {
view! {
<li class="news-item">
<span class="score">{story.points}</span>
<span class="title">
{if !story.url.starts_with("item?id=") {
view! {
<span>
<a href=story.url target="_blank" rel="noreferrer">
{story.title.clone()}
</a>
<span class="host">"("{story.domain}")"</span>
</span>
}.into_view()
} else {
let title = story.title.clone();
view! { <A href=format!("/stories/{}", story.id)>{title.clone()}</A> }.into_view()
}}
</span>
<br />
<span class="meta">
{if story.story_type != "job" {
view! {
<span>
{"by "}
{story.user.map(|user| view ! { <A href=format!("/users/{user}")>{user.clone()}</A>})}
{format!(" {} | ", story.time_ago)}
<A href=format!("/stories/{}", story.id)>
{if story.comments_count.unwrap_or_default() > 0 {
format!("{} comments", story.comments_count.unwrap_or_default())
} else {
"discuss".into()
}}
</A>
</span>
}.into_view()
} else {
let title = story.title.clone();
view! { <A href=format!("/item/{}", story.id)>{title.clone()}</A> }.into_view()
}}
</span>
{(story.story_type != "link").then(|| view! {
" "
<span class="label">{story.story_type}</span>
})}
</li>
}
}

View File

@ -1,125 +0,0 @@
use crate::api;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
#[component]
pub fn Story() -> impl IntoView {
let params = use_params_map();
let story = create_resource(
move || params().get("id").cloned().unwrap_or_default(),
move |id| async move {
if id.is_empty() {
None
} else {
api::fetch_api::<api::Story>(&api::story(&format!("item/{id}")))
.await
}
},
);
let meta_description = move || {
story
.get()
.and_then(|story| story.map(|story| story.title))
.unwrap_or_else(|| "Loading story...".to_string())
};
view! {
<Suspense fallback=|| view! { "Loading..." }>
<Meta name="description" content=meta_description/>
{move || story.get().map(|story| match story {
None => view! { <div class="item-view">"Error loading this story."</div> },
Some(story) => view! {
<div class="item-view">
<div class="item-view-header">
<a href=story.url target="_blank">
<h1>{story.title}</h1>
</a>
<span class="host">
"("{story.domain}")"
</span>
{story.user.map(|user| view! { <p class="meta">
{story.points}
" points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
{format!(" {}", story.time_ago)}
</p>})}
</div>
<div class="item-view-comments">
<p class="item-view-comments-header">
{if story.comments_count.unwrap_or_default() > 0 {
format!("{} comments", story.comments_count.unwrap_or_default())
} else {
"No comments yet.".into()
}}
</p>
<ul class="comment-children">
<For
each=move || story.comments.clone().unwrap_or_default()
key=|comment| comment.id
let:comment
>
<Comment comment />
</For>
</ul>
</div>
</div>
}})}
</Suspense>
}
}
#[component]
pub fn Comment(comment: api::Comment) -> impl IntoView {
let (open, set_open) = create_signal(true);
view! {
<li class="comment">
<div class="by">
<A href=format!("/users/{}", comment.user.clone().unwrap_or_default())>{comment.user.clone()}</A>
{format!(" {}", comment.time_ago)}
</div>
<div class="text" inner_html=comment.content></div>
{(!comment.comments.is_empty()).then(|| {
view! {
<div>
<div class="toggle" class:open=open>
<a on:click=move |_| set_open.update(|n| *n = !*n)>
{
let comments_len = comment.comments.len();
move || if open() {
"[-]".into()
} else {
format!("[+] {}{} collapsed", comments_len, pluralize(comments_len))
}
}
</a>
</div>
{move || open().then({
let comments = comment.comments.clone();
move || view! {
<ul class="comment-children">
<For
each=move || comments.clone()
key=|comment| comment.id
let:comment
>
<Comment comment />
</For>
</ul>
}
})}
</div>
}
})}
</li>
}
}
fn pluralize(n: usize) -> &'static str {
if n == 1 {
" reply"
} else {
" replies"
}
}

View File

@ -1,46 +0,0 @@
use crate::api::{self, User};
use leptos::*;
use leptos_router::*;
#[component]
pub fn User() -> impl IntoView {
let params = use_params_map();
let user = create_resource(
move || params().get("id").cloned().unwrap_or_default(),
move |id| async move {
if id.is_empty() {
None
} else {
api::fetch_api::<User>(&api::user(&id)).await
}
},
);
view! {
<div class="user-view">
<Suspense fallback=|| view! { "Loading..." }>
{move || user.get().map(|user| match user {
None => view! { <h1>"User not found."</h1> }.into_any(),
Some(user) => view! {
<div>
<h1>"User: " {&user.id}</h1>
<ul class="meta">
<li>
<span class="label">"Created: "</span> {user.created}
</li>
<li>
<span class="label">"Karma: "</span> {user.karma}
</li>
{user.about.as_ref().map(|about| view! { <li inner_html=about class="about"></li> })}
</ul>
<p class="links">
<a href=format!("https://news.ycombinator.com/submitted?id={}", user.id)>"submissions"</a>
" | "
<a href=format!("https://news.ycombinator.com/threads?id={}", user.id)>"comments"</a>
</p>
</div>
}.into_any()
})}
</Suspense>
</div>
}
}

View File

@ -1,326 +0,0 @@
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
font-size: 15px;
background-color: #f2f3f5;
margin: 0;
padding-top: 55px;
color: #34495e;
overflow-y: scroll
}
a {
color: #34495e;
text-decoration: none
}
.header {
background-color: #335d92;
position: fixed;
z-index: 999;
height: 55px;
top: 0;
left: 0;
right: 0
}
.header .inner {
max-width: 800px;
box-sizing: border-box;
margin: 0 auto;
padding: 15px 5px
}
.header a {
color: rgba(255, 255, 255, .8);
line-height: 24px;
transition: color .15s ease;
display: inline-block;
vertical-align: middle;
font-weight: 300;
letter-spacing: .075em;
margin-right: 1.8em
}
.header a:hover {
color: #fff
}
.header a.active {
color: #fff;
font-weight: 400
}
.header a:nth-child(6) {
margin-right: 0
}
.header .github {
color: #fff;
font-size: .9em;
margin: 0;
float: right
}
.logo {
width: 24px;
margin-right: 10px;
display: inline-block;
vertical-align: middle
}
.view {
max-width: 800px;
margin: 0 auto;
position: relative
}
.fade-enter-active,
.fade-exit-active {
transition: all .2s ease
}
.fade-enter,
.fade-exit-active {
opacity: 0
}
@media (max-width:860px) {
.header .inner {
padding: 15px 30px
}
}
@media (max-width:600px) {
.header .inner {
padding: 15px
}
.header a {
margin-right: 1em
}
.header .github {
display: none
}
}
.news-view {
padding-top: 45px
}
.news-list,
.news-list-nav {
background-color: #fff;
border-radius: 2px
}
.news-list-nav {
padding: 15px 30px;
position: fixed;
text-align: center;
top: 55px;
left: 0;
right: 0;
z-index: 998;
box-shadow: 0 1px 2px rgba(0, 0, 0, .1)
}
.news-list-nav .page-link {
margin: 0 1em
}
.news-list-nav .disabled {
color: #aaa
}
.news-list {
position: absolute;
margin: 30px 0;
width: 100%;
transition: all .5s cubic-bezier(.55, 0, .1, 1)
}
.news-list ul {
list-style-type: none;
padding: 0;
margin: 0
}
@media (max-width:600px) {
.news-list {
margin: 10px 0
}
}
.news-item {
background-color: #fff;
padding: 20px 30px 20px 80px;
border-bottom: 1px solid #eee;
position: relative;
line-height: 20px
}
.news-item .score {
color: #335d92;
font-size: 1.1em;
font-weight: 700;
position: absolute;
top: 50%;
left: 0;
width: 80px;
text-align: center;
margin-top: -10px
}
.news-item .host,
.news-item .meta {
font-size: .85em;
color: #626262
}
.news-item .host a,
.news-item .meta a {
color: #626262;
text-decoration: underline
}
.news-item .host a:hover,
.news-item .meta a:hover {
color: #335d92
}
.item-view-header {
background-color: #fff;
padding: 1.8em 2em 1em;
box-shadow: 0 1px 2px rgba(0, 0, 0, .1)
}
.item-view-header h1 {
display: inline;
font-size: 1.5em;
margin: 0;
margin-right: .5em
}
.item-view-header .host,
.item-view-header .meta,
.item-view-header .meta a {
color: #626262
}
.item-view-header .meta a {
text-decoration: underline
}
.item-view-comments {
background-color: #fff;
margin-top: 10px;
padding: 0 2em .5em
}
.item-view-comments-header {
margin: 0;
font-size: 1.1em;
padding: 1em 0;
position: relative
}
.item-view-comments-header .spinner {
display: inline-block;
margin: -15px 0
}
.comment-children {
list-style-type: none;
padding: 0;
margin: 0
}
@media (max-width:600px) {
.item-view-header h1 {
font-size: 1.25em
}
}
.comment-children .comment-children {
margin-left: 1.5em
}
.comment {
border-top: 1px solid #eee;
position: relative
}
.comment .by,
.comment .text,
.comment .toggle {
font-size: .9em;
margin: 1em 0
}
.comment .by {
color: #626262
}
.comment .by a {
color: #626262;
text-decoration: underline
}
.comment .text {
overflow-wrap: break-word
}
.comment .text a:hover {
color: #335d92
}
.comment .text pre {
white-space: pre-wrap
}
.comment .toggle {
background-color: #fffbf2;
padding: .3em .5em;
border-radius: 4px
}
.comment .toggle a {
color: #626262;
cursor: pointer
}
.comment .toggle.open {
padding: 0;
background-color: transparent;
margin-bottom: -.5em
}
.user-view {
background-color: #fff;
box-sizing: border-box;
padding: 2em 3em
}
.user-view h1 {
margin: 0;
font-size: 1.5em
}
.user-view .meta {
list-style-type: none;
padding: 0
}
.user-view .label {
display: inline-block;
min-width: 4em
}
.user-view .about {
margin: 1em 0
}
.user-view .links a {
text-decoration: underline
}

View File

@ -1,13 +0,0 @@
[package]
name = "rust-build"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dagger-rust.workspace = true
eyre.workspace = true
dagger-sdk.workspace = true
tokio.workspace = true

View File

@ -1,31 +0,0 @@
use dagger_rust::build::{RustVersion, SlimImage};
#[tokio::main]
pub async fn main() -> eyre::Result<()> {
dagger_sdk::connect(|client| async move {
let rust_build = dagger_rust::build::RustBuild::new(client.clone());
let containers = rust_build
.build_release(
Some("testdata"),
RustVersion::Nightly,
&["crates/*"],
&["openssl"],
vec![SlimImage::Debian {
image: "debian:bookworm".into(),
deps: vec!["openssl".into()],
architecture: dagger_rust::build::BuildArchitecture::Amd64,
}],
"example_bin",
)
.await?;
for container in containers {
container.sync().await?;
}
Ok(())
})
.await?;
Ok(())
}

View File

@ -1,3 +0,0 @@
[workspace]
members = ["crates/*"]
resolver = "2"

View File

@ -1 +0,0 @@
/target

View File

@ -1,8 +0,0 @@
[package]
name = "example_bin"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@ -1,3 +0,0 @@
fn main() {
println!("Hello, world!");
}

View File

@ -1,13 +0,0 @@
[package]
name = "rust-src"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dagger-rust.workspace = true
eyre.workspace = true
dagger-sdk.workspace = true
tokio.workspace = true

View File

@ -1,18 +0,0 @@
use std::path::PathBuf;
#[tokio::main]
pub async fn main() -> eyre::Result<()> {
dagger_sdk::connect(|client| async move {
let crates = ["some-crate"];
let dag = dagger_rust::source::RustSource::new(client.clone());
let (_src, _rust_src) = dag.get_rust_src(None::<PathBuf>, crates).await?;
let _full_src = dag
.get_rust_target_src(&PathBuf::from("."), client.container(), crates)
.await?;
Ok(())
})
.await?;
Ok(())
}

View File

@ -1,13 +0,0 @@
[package]
name = "rust-test"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dagger-rust.workspace = true
eyre.workspace = true
dagger-sdk.workspace = true
tokio.workspace = true

View File

@ -1,20 +0,0 @@
use dagger_rust::{build::RustVersion, test::RustTest};
#[tokio::main]
pub async fn main() -> eyre::Result<()> {
dagger_sdk::connect(|client| async move {
RustTest::new(client.clone())
.test(
Some("testdata"),
RustVersion::Nightly,
&["crates/*"],
&["openssl"],
)
.await?;
Ok(())
})
.await?;
Ok(())
}

View File

@ -1,7 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "example_bin"
version = "0.1.0"

View File

@ -1,3 +0,0 @@
[workspace]
members = ["crates/*"]
resolver = "2"

View File

@ -1 +0,0 @@
/target

View File

@ -1,8 +0,0 @@
[package]
name = "example_bin"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@ -1,11 +0,0 @@
fn main() {
println!("Hello, world!");
}
#[cfg(test)]
mod tests {
#[test]
fn test_main() {
assert_eq!(1, 1)
}
}

View File

@ -1,3 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}

View File

@ -9,4 +9,4 @@ if [[ -n "$CI_PREFIX" ]]; then
fi fi
$CMD_PREFIX main --cuddle-please-image="$CUDDLE_PLEASE_IMAGE" $CMD_PREFIX main

View File

@ -8,4 +8,4 @@ if [[ -n "$CI_PREFIX" ]]; then
CMD_PREFIX="$CI_PREFIX" CMD_PREFIX="$CI_PREFIX"
fi fi
$CMD_PREFIX pull-request --cuddle-please-image="$CUDDLE_PLEASE_IMAGE" $CMD_PREFIX pull-request

View File

@ -1,11 +0,0 @@
#!/usr/bin/env bash
set -e
CMD_PREFIX="cargo run -p ci --"
if [[ -n "$CI_PREFIX" ]]; then
CMD_PREFIX="$CI_PREFIX"
fi
$CMD_PREFIX pull-request --cuddle-please-image="$CUDDLE_PLEASE_IMAGE"