feat: add basic ci

Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
Kasper Juul Hermansen 2023-08-08 15:50:47 +02:00
parent 9cd29a6b10
commit 67b2b14567
Signed by: kjuulh
GPG Key ID: 9AA7BC13CE474394
14 changed files with 2729 additions and 0 deletions

171
.drone.yml Normal file
View File

@ -0,0 +1,171 @@
kind: pipeline
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
- cd ci
- cargo build --target=x86_64-unknown-linux-musl
#- 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: {}

2
.gitignore vendored
View File

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

1902
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 = "*"

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

@ -0,0 +1,537 @@
use std::path::PathBuf;
use std::sync::Arc;
use clap::Args;
use clap::Parser;
use clap::Subcommand;
use clap::ValueEnum;
use dagger_sdk::Platform;
use dagger_sdk::QueryContainerOpts;
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 {
PullRequest {
#[arg(long)]
image: String,
#[arg(long)]
tag: String,
#[arg(long)]
bin_name: String,
},
Main {
#[arg(long)]
image: String,
#[arg(long)]
tag: String,
#[arg(long)]
bin_name: String,
},
Release,
}
#[derive(Subcommand, Clone)]
pub enum LocalCommands {
Build {
#[arg(long, default_value = "debug")]
profile: BuildProfile,
#[arg(long)]
bin_name: String,
},
Test,
DockerImage {
#[arg(long)]
image: String,
#[arg(long)]
tag: String,
#[arg(long)]
bin_name: String,
},
PleaseRelease,
BuildDocs {},
}
#[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")]
golang_builder_image: Option<String>,
#[arg(long, global = true, help_heading = "Global")]
production_image: Option<String>,
#[arg(long, global = true, help_heading = "Global")]
docker_image: Option<String>,
#[arg(long, global = true, help_heading = "Global")]
source: Option<PathBuf>,
#[arg(long, global = true, help_heading = "Global")]
docs_image: Option<String>,
#[arg(long, global = true, help_heading = "Global")]
docs_image_tag: Option<String>,
}
#[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::PullRequest {
image,
tag,
bin_name,
} => {
async fn test(client: Arc<dagger_sdk::Query>, cli: &Command, bin_name: &String) {
let args = &cli.global;
let base_image = base_golang_image(client.clone(), args, &None, bin_name)
.await
.unwrap();
test::execute(client.clone(), args, base_image)
.await
.unwrap();
}
async fn build(
client: Arc<dagger_sdk::Query>,
cli: &Command,
bin_name: &String,
image: &String,
tag: &String,
) {
let args = &cli.global;
build::build(client.clone(), args, bin_name, image, tag)
.await
.unwrap();
}
tokio::join!(
test(client.clone(), &cli, bin_name),
build(client.clone(), &cli, bin_name, image, tag),
);
}
Commands::Main {
image,
tag,
bin_name,
} => {
async fn test(client: Arc<dagger_sdk::Query>, cli: &Command, bin_name: &String) {
let args = &cli.global;
let base_image = base_golang_image(client.clone(), args, &None, bin_name)
.await
.unwrap();
test::execute(client.clone(), args, base_image)
.await
.unwrap();
}
async fn build(
client: Arc<dagger_sdk::Query>,
cli: &Command,
bin_name: &String,
image: &String,
tag: &String,
) {
let args = &cli.global;
build::build_and_deploy(client.clone(), args, bin_name, image, tag)
.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, bin_name),
build(client.clone(), &cli, bin_name, image, tag),
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-1691463075");
let src = client
.git_opts(
"https://git.front.kjuulh.io/kjuulh/contractor",
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/contractor.git",
std::env::var("CUDDLE_PLEASE_TOKEN")?
),
])
.with_exec(vec![
"cuddle-please",
"release",
"--engine=gitea",
"--owner=kjuulh",
"--repo=contractor",
"--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 build {
use std::sync::Arc;
use dagger_sdk::Container;
use crate::{base_golang_image, get_base_debian_image, GlobalArgs};
pub async fn build_and_deploy(
client: Arc<dagger_sdk::Query>,
args: &GlobalArgs,
bin_name: &String,
image: &String,
tag: &String,
) -> eyre::Result<()> {
// let containers = vec!["linux/amd64", "linux/arm64"];
let base_image = get_base_debian_image(
client.clone(),
&args.clone(),
Some("linux/amd64".to_string()),
)
.await?;
let container = base_golang_image(
client.clone(),
args,
&Some("linux/amd64".to_string()),
&bin_name.clone(),
)
.await?;
let build_image = execute(client.clone(), args, &container, &base_image, bin_name).await?;
let build_id = build_image.id().await?;
let _container = client
.clone()
.container()
.publish_opts(
format!("{image}:{tag}"),
dagger_sdk::ContainerPublishOpts {
platform_variants: Some(vec![build_id]),
},
)
.await?;
Ok(())
}
pub async fn build(
client: Arc<dagger_sdk::Query>,
args: &GlobalArgs,
bin_name: &String,
_image: &String,
_tag: &String,
) -> eyre::Result<()> {
// let containers = vec!["linux/amd64", "linux/arm64"];
let base_image = get_base_debian_image(
client.clone(),
&args.clone(),
Some("linux/amd64".to_string()),
)
.await?;
let container = base_golang_image(
client.clone(),
args,
&Some("linux/amd64".to_string()),
&bin_name.clone(),
)
.await?;
let build_image = execute(client.clone(), args, &container, &base_image, bin_name).await?;
build_image.exit_code().await?;
Ok(())
}
pub async fn execute(
_client: Arc<dagger_sdk::Query>,
_args: &GlobalArgs,
container: &dagger_sdk::Container,
base_image: &dagger_sdk::Container,
bin_name: &String,
) -> eyre::Result<Container> {
let final_image = base_image
.with_file(
format!("/usr/local/bin/{}", &bin_name),
container
.file(format!("/mnt/src/dist/{}", &bin_name))
.id()
.await?,
)
.with_exec(vec![bin_name, "--help"]);
let output = final_image.stdout().await?;
println!("{output}");
Ok(final_image)
}
}
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("test")
.with_exec(vec!["go", "test", "./..."]);
test_image.exit_code().await?;
Ok(())
}
}
pub async fn get_base_docker_image(
client: Arc<dagger_sdk::Query>,
args: &GlobalArgs,
platform: Option<String>,
) -> eyre::Result<dagger_sdk::Container> {
let default_platform = client.default_platform().await?;
let platform = platform.map(Platform).unwrap_or(default_platform);
let image = client
.container_opts(QueryContainerOpts {
id: None,
platform: Some(platform),
})
.from(
args.docker_image
.clone()
.unwrap_or("docker:dind".to_string()),
);
Ok(image)
}
pub async fn get_base_debian_image(
client: Arc<dagger_sdk::Query>,
args: &GlobalArgs,
platform: Option<String>,
) -> eyre::Result<dagger_sdk::Container> {
let docker_image = get_base_docker_image(client.clone(), args, platform.clone()).await?;
let default_platform = client.default_platform().await?;
let platform = platform.map(Platform).unwrap_or(default_platform);
let image = client
.container_opts(QueryContainerOpts {
id: None,
platform: Some(platform),
})
.from(
args.production_image
.clone()
.unwrap_or("alpine:latest".to_string()),
);
let base_image = image
.with_exec(vec!["apk", "add", "openssl", "openssl-dev", "pkgconfig"])
.with_file(
"/usr/local/bin/docker",
docker_image.file("/usr/local/bin/docker").id().await?,
);
Ok(base_image)
}
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/",
".cuddle/",
"docs/",
"ci/",
])
.build()?,
);
Ok(directory)
}
pub async fn get_golang_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!["**/go.*"])
.build()?,
);
Ok(directory)
}
pub async fn base_golang_image(
client: Arc<dagger_sdk::Query>,
args: &GlobalArgs,
platform: &Option<String>,
bin_name: &String,
) -> eyre::Result<dagger_sdk::Container> {
let dep_src = get_golang_dep_src(client.clone(), args).await?;
let src = get_src(client.clone(), args)?;
let client = client.pipeline("golang_base_image");
let goarch = match platform
.clone()
.unwrap_or("linux/amd64".to_string())
.as_str()
{
"linux/amd64" => "amd64",
"linux/arm64" => "arm64",
_ => eyre::bail!("architecture not supported"),
};
let goos = match platform
.clone()
.unwrap_or("linux/amd64".to_string())
.as_str()
{
"linux/amd64" => "linux",
"linux/arm64" => "linux",
_ => eyre::bail!("os not supported"),
};
let golang_build_image = client
.container()
.from(
args.golang_builder_image
.as_ref()
.unwrap_or(&"golang:latest".into()),
)
.with_env_variable("GOOS", goos)
.with_env_variable("GOARCH", goarch)
.with_env_variable("CGO_ENABLED", "0");
let golang_dep_download = golang_build_image
.with_directory("/mnt/src", dep_src.id().await?)
.with_exec(vec!["go", "mod", "download"])
.with_mounted_cache(
"/root/go",
client.cache_volume("golang_mod_cache").id().await?,
);
let golang_bin = golang_build_image
.with_workdir("/mnt/src")
// .with_directory(
// "/root/go",
// golang_dep_download.directory("/root/go").id().await?,
// )
.with_directory("/mnt/src/", src.id().await?)
.with_exec(vec![
"go",
"build",
"-o",
&format!("dist/{bin_name}"),
"main.go",
]);
golang_bin.exit_code().await?;
Ok(golang_bin)
}

27
cuddle.yaml Normal file
View File

@ -0,0 +1,27 @@
# yaml-language-server: $schema=https://git.front.kjuulh.io/kjuulh/cuddle/raw/branch/main/schemas/base.json
base: "git@git.front.kjuulh.io:kjuulh/cuddle-golang-service-plan.git"
vars:
service: "contractor"
registry: kasperhermansen
docker_image: "docker:dind"
golang_builder_image: "golang:latest"
production_image: "alpine:latest"
please:
project:
owner: kjuulh
repository: contractor
branch: main
settings:
api_url: https://git.front.kjuulh.io
scripts:
"ci:main":
type: shell
"ci:pr":
type: shell
"ci:release":
type: shell

23
scripts/ci:main.sh Executable file
View File

@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -e
CMD_PREFIX=""
if [[ -n "$CI_PREFIX" ]]; then
CMD_PREFIX="$CI_PREFIX"
else
cd ci || return 1
cargo build
cd - || return 1
CMD_PREFIX="ci/target/debug/ci"
fi
$CMD_PREFIX main \
--docker-image "$DOCKER_IMAGE" \
--golang-builder-image "$GOLANG_BUILDER_IMAGE" \
--production-image "$PRODUCTION_IMAGE" \
--image "$REGISTRY/$SERVICE" \
--tag "main-$(date +%s)" \
--bin-name "$SERVICE"

25
scripts/ci:pr.sh Executable file
View File

@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -e
CMD_PREFIX="cargo run -p ci --"
CMD_PREFIX=""
if [[ -n "$CI_PREFIX" ]]; then
CMD_PREFIX="$CI_PREFIX"
else
cd ci || return 1
cargo build
cd - || return 1
CMD_PREFIX="ci/target/debug/ci"
fi
$CMD_PREFIX pull-request \
--docker-image "$DOCKER_IMAGE" \
--golang-builder-image "$GOLANG_BUILDER_IMAGE" \
--production-image "$PRODUCTION_IMAGE" \
--image "$REGISTRY/$SERVICE" \
--tag "main-$(date +%s)" \
--bin-name "$SERVICE"

5
scripts/local:docker.sh Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -e
cargo run -p ci -- local docker-image --image kasperhermansen/cuddle-please --tag dev --bin-name cuddle-please

5
scripts/local:docker:docs.sh Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -e
cargo run -p ci -- local build-docs --mkdocs-image $MKDOCS_IMAGE --caddy-image $CADDY_IMAGE

5
scripts/mkdocs:build.sh Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -e
docker run --rm -it -p 8000:8000 -v ${PWD}:/docs ${MKDOCS_IMAGE}

5
scripts/mkdocs:dev.sh Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -e
docker run --rm -it -p 8000:8000 -v ${PWD}:/docs ${MKDOCS_IMAGE}

5
scripts/mkdocs:new.sh Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -e
docker run --rm -it -v ${PWD}:/docs ${MKDOCS_IMAGE} new .

View File

@ -0,0 +1 @@