Compare commits
No commits in common. "v0.1.0" and "main" have entirely different histories.
173
.drone.yml
173
.drone.yml
@ -1,171 +1,2 @@
|
||||
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: {}
|
||||
kind: template
|
||||
load: cuddle-rust-service-plan.yaml
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,3 +1,3 @@
|
||||
.env
|
||||
.cuddle/
|
||||
target/
|
||||
.cuddle/
|
||||
.env
|
||||
|
29
CHANGELOG.md
29
CHANGELOG.md
@ -1,29 +0,0 @@
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.1.0] - 2023-08-08
|
||||
|
||||
### Added
|
||||
- add basic ci
|
||||
- add basic readme
|
||||
- with main loop
|
||||
- with initial cmd and server
|
||||
|
||||
### Docs
|
||||
- *(README)* added some more milestones and fixed docker-compose
|
||||
- add clarification about expectations and what milestones are missing
|
||||
- add license
|
||||
- clarification
|
||||
|
||||
### Other
|
||||
- Merge pull request 'Configure Renovate' (#1) from renovate/configure into main
|
||||
|
||||
Reviewed-on: https://git.front.kjuulh.io/kjuulh/contractor/pulls/1
|
||||
|
||||
- Add renovate.json
|
||||
|
3191
Cargo.lock
generated
Normal file
3191
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
||||
[workspace]
|
||||
members = ["crates/*"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
contractor = { path = "crates/contractor" }
|
||||
|
||||
anyhow = { version = "1" }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tracing = { version = "0.1", features = ["log"] }
|
||||
tracing-subscriber = { version = "0.3.18" }
|
||||
clap = { version = "4", features = ["derive", "env"] }
|
||||
dotenv = { version = "0.15" }
|
||||
axum = { version = "0.7" }
|
7
LICENSE
7
LICENSE
@ -1,7 +0,0 @@
|
||||
Copyright 2023 Kasper J. Hermansen
|
||||
|
||||
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.
|
139
README.md
139
README.md
@ -1,138 +1 @@
|
||||
# Contractor - A renovate bot for gitea and github
|
||||
|
||||
Contractor is a chatops like bot, integrating with github/gitea issues, allowing
|
||||
commands to trigger renovate runs.
|
||||
|
||||
```bash
|
||||
/contractor refresh
|
||||
```
|
||||
|
||||
Contractor runs in a regular docker image and uses the official renovate slim
|
||||
image behind the scenes, this can be changed in the configuration.
|
||||
|
||||
![command](./assets/command.png)
|
||||
<small>Do note that the contractor was run under a personal user, hence the same
|
||||
user replied</small>
|
||||
|
||||
## DISCLAIMER
|
||||
|
||||
The project is still 0.x.x As such the api is subject to change, and the
|
||||
examples will probably be out of date. The below should be seen as an example of
|
||||
what the project will look like once feature-complete.
|
||||
|
||||
## Milestones
|
||||
|
||||
- [x] 0.1.0
|
||||
- Includes basic setup such as working server bot, and installation command,
|
||||
automation is missing however. Also only gitea support for now, because this
|
||||
is where the project initially is supposed to be in use.
|
||||
- [ ] 0.2.0
|
||||
- Add GitHub support
|
||||
- [ ] 0.3.0
|
||||
- Add Delegation support (not clustering, just delegation of renovate jobs)
|
||||
- [ ] 0.4.0
|
||||
- Slack integration
|
||||
- [ ] 0.5.0
|
||||
- GitHub App and such support
|
||||
- [ ] 0.6.0
|
||||
- Add api key support
|
||||
|
||||
## Getting started
|
||||
|
||||
First you need to pull and run the contractor image, docker is the preferred way
|
||||
of execution, but golang is also available from source.
|
||||
|
||||
Docker compose is given as an example, but you're free to run using `docker run`
|
||||
if you prefer.
|
||||
|
||||
See example for a ready-to-run image
|
||||
|
||||
```yaml
|
||||
# file: docker-compose.yaml
|
||||
version: "3"
|
||||
services:
|
||||
contractor:
|
||||
image: docker.io/kjuulh/contractor:latest
|
||||
restart: unless-stopped
|
||||
commands:
|
||||
- contractor server serve
|
||||
volumes:
|
||||
- "./templates/contractor:/mnt/config"
|
||||
- "/var/run/docker.sock:/var/run/docker.sock"
|
||||
env_file:
|
||||
- .env
|
||||
```
|
||||
|
||||
```bash
|
||||
# file: .env
|
||||
GITEA_RENOVATE_TOKEN=<gitea application token> # needs repo and pull request permissions
|
||||
GITHUB_RENOVATE_TOKEN=<github personel access token> # needs repo and pull request permissions
|
||||
GITHUB_COM_TOKEN=<github personel access token> # used for communication, doesn't need much
|
||||
RENOVATE_SECRETS='{"HARBOR_SERVER_PASSWORD": "<whatever secret you need in your config>"}'
|
||||
CONTRACTOR_API_KEY='<some sufficiently secret password used for webhooks to authenticate to your server>'
|
||||
```
|
||||
|
||||
```json
|
||||
// file: templates/contractor/config.json
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"platform": "gitea",
|
||||
"extends": [
|
||||
"config:base"
|
||||
]
|
||||
}
|
||||
// Remember to fill out the options as you see fit, this is not a complete example
|
||||
```
|
||||
|
||||
Use renovate secret for each `{{ secrets.HARBOR_SERVER_PASSWORD }}` in your
|
||||
config, replace `HARBOR_SERVER_PASSWORD` with your own
|
||||
|
||||
And then run the server with: `docker compose up`
|
||||
|
||||
This has started the server, but github doesn't know that it needs to talk to
|
||||
you yet.
|
||||
|
||||
As such host the server somewhere with a public hostname, such that github or
|
||||
gitea webhooks can reach it, i.e. contractor.some-domain.com:9111
|
||||
|
||||
To install the webhook, either use the docker image, or download the cli from
|
||||
source.
|
||||
|
||||
### CLI
|
||||
|
||||
To install the cli
|
||||
|
||||
```bash
|
||||
go install git.front.kjuulh.io/kjuulh/contractor@latest
|
||||
```
|
||||
|
||||
contractor will automatically read any .env file, so you can leave out the
|
||||
secrets.
|
||||
|
||||
```bash
|
||||
contractor install \
|
||||
--owner kjuulh \
|
||||
--repository contractor \
|
||||
--url https://git.front.kjuulh.io/api/v1 \
|
||||
--backend gitea
|
||||
```
|
||||
|
||||
If you leave any of these out, contractor will prompt your for required values.
|
||||
|
||||
### Docker
|
||||
|
||||
You can also use docker for it.
|
||||
|
||||
```bash
|
||||
docker compose run contractor \
|
||||
install \
|
||||
--owner kjuulh \
|
||||
--repository contractor \
|
||||
--url https://git.front.kjuulh.io/api/v1 \
|
||||
--backend gitea
|
||||
```
|
||||
|
||||
### GitHub App
|
||||
|
||||
TBD, this should automatically install the webhook for allowed repositories, I
|
||||
just haven't gotten around to it yet. It is on the 0.3.0 Roadmap.
|
||||
# contractor
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 42 KiB |
1902
ci/Cargo.lock
generated
1902
ci/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,16 +0,0 @@
|
||||
[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
537
ci/src/main.rs
@ -1,537 +0,0 @@
|
||||
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)
|
||||
}
|
@ -1,750 +0,0 @@
|
||||
package contractor
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"dagger.io/dagger"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type createHook struct {
|
||||
Active bool `json:"active"`
|
||||
AuthorizationHeader string `json:"authorization_header"`
|
||||
BranchFilter string `json:"branch_filter"`
|
||||
Config map[string]string `json:"config"`
|
||||
Events []string `json:"events"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func installCmd() *cobra.Command {
|
||||
var (
|
||||
owner string
|
||||
repository string
|
||||
serverType string
|
||||
|
||||
url string
|
||||
token string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "install",
|
||||
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := NewGiteaClient(&url, &token).CreateWebhook(owner, repository); err != nil {
|
||||
log.Printf("failed to add create webhook: %s", err.Error())
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&owner, "owner", "o", "", "the owner for which the repository belongs")
|
||||
cmd.Flags().StringVarP(&repository, "repository", "p", "", "the repository to install")
|
||||
cmd.Flags().
|
||||
StringVar(&serverType, "server-type", "gitea", "the server type to use [gitea, github]")
|
||||
cmd.MarkFlagRequired("owner")
|
||||
cmd.MarkFlagRequired("repository")
|
||||
|
||||
cmd.PersistentFlags().StringVar(&url, "url", "", "the api url of the server")
|
||||
cmd.PersistentFlags().StringVar(&token, "token", "", "the token to authenticate with")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func serverCmd() *cobra.Command {
|
||||
var (
|
||||
url string
|
||||
token string
|
||||
)
|
||||
|
||||
giteaClient := NewGiteaClient(&url, &token)
|
||||
renovateClient := NewRenovateClient("")
|
||||
queue := NewGoQueue()
|
||||
queue.Subscribe(
|
||||
MessageTypeRefreshRepository,
|
||||
func(ctx context.Context, item *QueueMessage) error {
|
||||
log.Printf("handling message: %s, content: %s", item.Type, item.Content)
|
||||
return nil
|
||||
},
|
||||
)
|
||||
queue.Subscribe(
|
||||
MessageTypeRefreshRepositoryDone,
|
||||
func(ctx context.Context, item *QueueMessage) error {
|
||||
log.Printf("handling message: %s, content: %s", item.Type, item.Content)
|
||||
return nil
|
||||
},
|
||||
)
|
||||
queue.Subscribe(
|
||||
MessageTypeRefreshRepository,
|
||||
func(ctx context.Context, item *QueueMessage) error {
|
||||
var request RefreshRepositoryRequest
|
||||
if err := json.Unmarshal([]byte(item.Content), &request); err != nil {
|
||||
log.Printf("failed to unmarshal request body: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
cancelCtx, cancel := context.WithTimeout(ctx, time.Second*30)
|
||||
defer cancel()
|
||||
|
||||
if err := renovateClient.RefreshRepository(cancelCtx, request.Owner, request.Repository); err != nil {
|
||||
queue.Insert(MessageTypeRefreshRepositoryDone, RefreshDoneRepositoryRequest{
|
||||
Repository: request.Repository,
|
||||
Owner: request.Owner,
|
||||
PullRequestID: request.PullRequestID,
|
||||
CommentID: request.CommentID,
|
||||
CommentBody: request.CommentBody,
|
||||
ReportProgress: request.ReportProgress,
|
||||
Status: "failed",
|
||||
Error: err.Error(),
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
queue.Insert(MessageTypeRefreshRepositoryDone, RefreshDoneRepositoryRequest{
|
||||
Repository: request.Repository,
|
||||
Owner: request.Owner,
|
||||
PullRequestID: request.PullRequestID,
|
||||
CommentID: request.CommentID,
|
||||
CommentBody: request.CommentBody,
|
||||
ReportProgress: request.ReportProgress,
|
||||
Status: "done",
|
||||
Error: "",
|
||||
})
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
queue.Subscribe(
|
||||
MessageTypeRefreshRepositoryDone,
|
||||
func(ctx context.Context, item *QueueMessage) error {
|
||||
var doneRequest RefreshDoneRepositoryRequest
|
||||
if err := json.Unmarshal([]byte(item.Content), &doneRequest); err != nil {
|
||||
log.Printf("failed to unmarshal request body: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
return giteaClient.EditComment(ctx, &doneRequest)
|
||||
},
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "server",
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringVar(&url, "url", "", "the api url of the server")
|
||||
cmd.PersistentFlags().StringVar(&token, "token", "", "the token to authenticate with")
|
||||
|
||||
cmd.AddCommand(serverServeCmd(&url, &token, queue, giteaClient))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
const (
|
||||
MessageTypeRefreshRepository = "refresh_repository"
|
||||
MessageTypeRefreshRepositoryDone = "refresh_repository_done"
|
||||
)
|
||||
|
||||
type RefreshRepositoryRequest struct {
|
||||
Repository string `json:"repository"`
|
||||
Owner string `json:"owner"`
|
||||
PullRequestID int `json:"pullRequestId"`
|
||||
CommentID int `json:"commentId"`
|
||||
CommentBody string `json:"commentBody"`
|
||||
ReportProgress bool `json:"reportProgress"`
|
||||
}
|
||||
|
||||
type RefreshDoneRepositoryRequest struct {
|
||||
Repository string `json:"repository"`
|
||||
Owner string `json:"owner"`
|
||||
PullRequestID int `json:"pullRequestId"`
|
||||
CommentID int `json:"commentId"`
|
||||
CommentBody string `json:"commentBody"`
|
||||
ReportProgress bool `json:"reportProgress"`
|
||||
Status string `json:"status"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func serverServeCmd(
|
||||
url *string,
|
||||
token *string,
|
||||
queue *GoQueue,
|
||||
giteaClient *GiteaClient,
|
||||
) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "serve",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
engine := gin.Default()
|
||||
|
||||
gitea := engine.Group("/gitea")
|
||||
{
|
||||
gitea.POST("/webhook", func(ctx *gin.Context) {
|
||||
log.Println("received")
|
||||
|
||||
type GiteaWebhookRequest struct {
|
||||
Action string `json:"action"`
|
||||
Issue struct {
|
||||
Id int `json:"id"`
|
||||
Number int `json:"number"`
|
||||
} `json:"issue"`
|
||||
Comment struct {
|
||||
Body string `json:"body"`
|
||||
} `json:"comment"`
|
||||
Repository struct {
|
||||
FullName string `json:"full_name"`
|
||||
}
|
||||
}
|
||||
|
||||
var request GiteaWebhookRequest
|
||||
|
||||
if err := ctx.BindJSON(&request); err != nil {
|
||||
ctx.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
|
||||
command, ok := validateBotComment(request.Comment.Body)
|
||||
if ok {
|
||||
log.Printf("got webhook request: contractor %s", command)
|
||||
|
||||
bot := NewBotHandler(giteaClient)
|
||||
output, err := bot.Handle(command)
|
||||
if err != nil {
|
||||
log.Printf("failed to run bot handler with error: %s", err.Error())
|
||||
}
|
||||
|
||||
parts := strings.Split(request.Repository.FullName, "/")
|
||||
|
||||
comment, err := bot.AppendComment(
|
||||
parts[0],
|
||||
parts[1],
|
||||
request.Issue.Number,
|
||||
output,
|
||||
)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := queue.Insert(MessageTypeRefreshRepository, RefreshRepositoryRequest{
|
||||
Repository: parts[1],
|
||||
Owner: parts[0],
|
||||
PullRequestID: request.Issue.Number,
|
||||
CommentID: comment.ID,
|
||||
CommentBody: comment.Body,
|
||||
ReportProgress: true,
|
||||
}); err != nil {
|
||||
ctx.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(204)
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
engine.Run("0.0.0.0:8080")
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func validateBotComment(s string) (request string, ok bool) {
|
||||
if after, ok := strings.CutPrefix(s, "/contractor"); ok {
|
||||
return strings.TrimSpace(after), true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
func RootCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{Use: "contractor"}
|
||||
|
||||
cmd.AddCommand(installCmd(), serverCmd())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
type BotHandler struct {
|
||||
giteaClient *GiteaClient
|
||||
}
|
||||
|
||||
func NewBotHandler(gitea *GiteaClient) *BotHandler {
|
||||
return &BotHandler{giteaClient: gitea}
|
||||
}
|
||||
|
||||
func (b *BotHandler) Handle(input string) (output string, err error) {
|
||||
innerHandle := func(input string) (output string, err error) {
|
||||
if strings.HasPrefix(input, "help") {
|
||||
return b.Help(), nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(input, "refresh") {
|
||||
return `
|
||||
<h3>Contractor triggered renovate refresh on this repository</h3>
|
||||
This comment will be updated with status
|
||||
|
||||
<!-- Status update start -->
|
||||
<!-- Status update end -->
|
||||
`, nil
|
||||
}
|
||||
|
||||
return b.Help(), errors.New("could not recognize command")
|
||||
}
|
||||
|
||||
output, err = innerHandle(input)
|
||||
output = fmt.Sprintf(
|
||||
"%s\n<small>This comment was generated by <a href='https://git.front.kjuulh.io/kjuulh/contractor'>Contractor</a></small>",
|
||||
output,
|
||||
)
|
||||
return output, err
|
||||
}
|
||||
|
||||
func (b *BotHandler) Help() string {
|
||||
return `<details open>
|
||||
<summary><h3>/contractor [command]</h3></summary>
|
||||
|
||||
<strong>Commands:</strong>
|
||||
|
||||
* /contractor help
|
||||
* triggers the help menu
|
||||
* /contractor refresh
|
||||
* triggers renovate to refresh the current pull request
|
||||
</details>`
|
||||
}
|
||||
|
||||
type AddCommentResponse struct {
|
||||
Body string `json:"body"`
|
||||
ID int `json:"id"`
|
||||
}
|
||||
|
||||
func (b *BotHandler) AppendComment(
|
||||
owner string,
|
||||
repository string,
|
||||
pullRequest int,
|
||||
comment string,
|
||||
) (*AddCommentResponse, error) {
|
||||
return b.giteaClient.AddComment(owner, repository, pullRequest, comment)
|
||||
}
|
||||
|
||||
type QueueMessage struct {
|
||||
Type string `json:"type"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type GoQueue struct {
|
||||
queue []*QueueMessage
|
||||
queueLock sync.Mutex
|
||||
subscribers map[string]map[string]func(ctx context.Context, item *QueueMessage) error
|
||||
subscribersLock sync.RWMutex
|
||||
}
|
||||
|
||||
func NewGoQueue() *GoQueue {
|
||||
return &GoQueue{
|
||||
queue: make([]*QueueMessage, 0),
|
||||
subscribers: make(
|
||||
map[string]map[string]func(ctx context.Context, item *QueueMessage) error,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (gq *GoQueue) Subscribe(
|
||||
messageType string,
|
||||
callback func(ctx context.Context, item *QueueMessage) error,
|
||||
) string {
|
||||
gq.subscribersLock.Lock()
|
||||
defer gq.subscribersLock.Unlock()
|
||||
|
||||
uid, err := uuid.NewUUID()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
id := uid.String()
|
||||
|
||||
_, ok := gq.subscribers[messageType]
|
||||
if !ok {
|
||||
messageTypeSubscribers := make(
|
||||
map[string]func(ctx context.Context, item *QueueMessage) error,
|
||||
)
|
||||
messageTypeSubscribers[id] = callback
|
||||
gq.subscribers[messageType] = messageTypeSubscribers
|
||||
} else {
|
||||
gq.subscribers[messageType][id] = callback
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func (gq *GoQueue) Unsubscribe(messageType string, id string) {
|
||||
gq.subscribersLock.Lock()
|
||||
defer gq.subscribersLock.Unlock()
|
||||
_, ok := gq.subscribers[messageType]
|
||||
if !ok {
|
||||
// No work to be done
|
||||
return
|
||||
} else {
|
||||
delete(gq.subscribers[messageType], id)
|
||||
}
|
||||
}
|
||||
|
||||
func (gq *GoQueue) Insert(messageType string, content any) error {
|
||||
gq.queueLock.Lock()
|
||||
defer gq.queueLock.Unlock()
|
||||
|
||||
contents, err := json.Marshal(content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gq.queue = append(gq.queue, &QueueMessage{
|
||||
Type: messageType,
|
||||
Content: string(contents),
|
||||
})
|
||||
|
||||
go func() {
|
||||
gq.handle(context.Background())
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gq *GoQueue) handle(ctx context.Context) {
|
||||
gq.queueLock.Lock()
|
||||
defer gq.queueLock.Unlock()
|
||||
|
||||
for {
|
||||
if len(gq.queue) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
item := gq.queue[0]
|
||||
gq.queue = gq.queue[1:]
|
||||
|
||||
gq.subscribersLock.RLock()
|
||||
defer gq.subscribersLock.RUnlock()
|
||||
|
||||
for id, callback := range gq.subscribers[item.Type] {
|
||||
log.Printf("sending message to %s", id)
|
||||
go callback(ctx, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type GiteaClient struct {
|
||||
url *string
|
||||
token *string
|
||||
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewGiteaClient(url, token *string) *GiteaClient {
|
||||
return &GiteaClient{
|
||||
url: url,
|
||||
token: token,
|
||||
client: http.DefaultClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (gc *GiteaClient) EditComment(
|
||||
ctx context.Context,
|
||||
doneRequest *RefreshDoneRepositoryRequest,
|
||||
) error {
|
||||
commentBody := html.UnescapeString(doneRequest.CommentBody)
|
||||
startCmnt := "<!-- Status update start -->"
|
||||
startIdx := strings.Index(commentBody, startCmnt)
|
||||
endIdx := strings.Index(commentBody, "<!-- Status update end -->")
|
||||
if startIdx >= 0 && endIdx >= 0 {
|
||||
log.Println("found comment to replace")
|
||||
|
||||
var content string
|
||||
|
||||
if doneRequest.Error != "" {
|
||||
content = fmt.Sprintf("<pre>ERROR: %s</pre><br>", doneRequest.Error)
|
||||
}
|
||||
if doneRequest.Status != "" {
|
||||
content = fmt.Sprintf("<p>%s</p>", doneRequest.Status)
|
||||
}
|
||||
|
||||
doneRequest.CommentBody = fmt.Sprintf(
|
||||
"%s<br><hr>%s<hr><br>%s",
|
||||
commentBody[:startIdx+len(startCmnt)],
|
||||
content,
|
||||
commentBody[endIdx:],
|
||||
)
|
||||
}
|
||||
|
||||
editComment := struct {
|
||||
Body string `json:"body"`
|
||||
}{
|
||||
Body: doneRequest.CommentBody,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(editComment)
|
||||
if err != nil {
|
||||
log.Println("failed to marshal request body: %w", err)
|
||||
return err
|
||||
}
|
||||
bodyReader := bytes.NewReader(body)
|
||||
|
||||
request, err := http.NewRequest(
|
||||
http.MethodPatch,
|
||||
fmt.Sprintf(
|
||||
"%s/repos/%s/%s/issues/comments/%d",
|
||||
strings.TrimSuffix(*gc.url, "/"),
|
||||
doneRequest.Owner,
|
||||
doneRequest.Repository,
|
||||
doneRequest.CommentID,
|
||||
),
|
||||
bodyReader,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("failed to form update comment request: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
request.Header.Add("Authorization", fmt.Sprintf("token %s", *gc.token))
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
|
||||
resp, err := gc.client.Do(request)
|
||||
if err != nil {
|
||||
log.Printf("failed to update comment: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode > 299 {
|
||||
log.Printf("failed to update comment with status code: %d", resp.StatusCode)
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Printf("failed to read body of error response: %s", err.Error())
|
||||
} else {
|
||||
log.Printf("request body: %s", string(respBody))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gc *GiteaClient) CreateWebhook(owner, repository string) error {
|
||||
createHookOptions := createHook{
|
||||
Active: true,
|
||||
AuthorizationHeader: "",
|
||||
BranchFilter: "*",
|
||||
Config: map[string]string{
|
||||
"url": "http://10.0.9.1:8080/gitea/webhook",
|
||||
"content_type": "json",
|
||||
},
|
||||
Events: []string{
|
||||
"pull_request_comment",
|
||||
},
|
||||
Type: "gitea",
|
||||
}
|
||||
|
||||
body, err := json.Marshal(createHookOptions)
|
||||
if err != nil {
|
||||
log.Println("failed to marshal request body: %w", err)
|
||||
return err
|
||||
}
|
||||
bodyReader := bytes.NewReader(body)
|
||||
request, err := http.NewRequest(
|
||||
http.MethodPost,
|
||||
fmt.Sprintf(
|
||||
"%s/repos/%s/%s/hooks",
|
||||
strings.TrimSuffix(*gc.url, "/"),
|
||||
owner,
|
||||
repository,
|
||||
),
|
||||
bodyReader,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("failed to form create hook request: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
request.Header.Add("Authorization", fmt.Sprintf("token %s", *gc.token))
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
|
||||
resp, err := gc.client.Do(request)
|
||||
if err != nil {
|
||||
log.Printf("failed to register hook: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode > 299 {
|
||||
log.Printf("failed to register with status code: %d", resp.StatusCode)
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Printf("failed to read body of error response: %s", err.Error())
|
||||
} else {
|
||||
log.Printf("request body: %s", string(respBody))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gc *GiteaClient) AddComment(
|
||||
owner, repository string,
|
||||
pullRequest int,
|
||||
comment string,
|
||||
) (*AddCommentResponse, error) {
|
||||
addComment := struct {
|
||||
Body string `json:"body"`
|
||||
}{
|
||||
Body: comment,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(addComment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bodyReader := bytes.NewReader(body)
|
||||
|
||||
request, err := http.NewRequest(
|
||||
http.MethodPost,
|
||||
fmt.Sprintf(
|
||||
"%s/repos/%s/%s/issues/%d/comments",
|
||||
strings.TrimSuffix(*gc.url, "/"),
|
||||
owner,
|
||||
repository,
|
||||
pullRequest,
|
||||
),
|
||||
bodyReader,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request.Header.Add("Authorization", fmt.Sprintf("token %s", *gc.token))
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
|
||||
resp, err := gc.client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode > 299 {
|
||||
log.Printf("failed to register with status code: %d", resp.StatusCode)
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
log.Printf("request body: %s", string(respBody))
|
||||
}
|
||||
}
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var response AddCommentResponse
|
||||
if err := json.Unmarshal(respBody, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
type RenovateClient struct {
|
||||
config string
|
||||
}
|
||||
|
||||
func NewRenovateClient(config string) *RenovateClient {
|
||||
return &RenovateClient{config: config}
|
||||
}
|
||||
|
||||
func (rc *RenovateClient) RefreshRepository(ctx context.Context, owner, repository string) error {
|
||||
client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stdout))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
envRenovateToken := os.Getenv("GITEA_RENOVATE_TOKEN")
|
||||
log.Println(envRenovateToken)
|
||||
|
||||
renovateToken := client.SetSecret("RENOVATE_TOKEN", envRenovateToken)
|
||||
githubComToken := client.SetSecret("GITHUB_COM_TOKEN", os.Getenv("GITHUB_COM_TOKEN"))
|
||||
renovateSecret := client.SetSecret("RENOVATE_SECRETS", os.Getenv("RENOVATE_SECRETS"))
|
||||
|
||||
output, err := client.Container().
|
||||
From("renovate/renovate:latest").
|
||||
WithNewFile("/opts/renovate/config.json", dagger.ContainerWithNewFileOpts{
|
||||
Contents: `{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"platform": "gitea",
|
||||
"endpoint": "https://git.front.kjuulh.io/api/v1/",
|
||||
"automerge": true,
|
||||
"automergeType": "pr",
|
||||
"extends": [
|
||||
"config:base"
|
||||
],
|
||||
"hostRules": [
|
||||
{
|
||||
"hostType": "docker",
|
||||
"matchHost": "harbor.front.kjuulh.io",
|
||||
"username": "service",
|
||||
"password": "{{ secrets.HARBOR_SERVER_PASSWORD }}"
|
||||
}
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchDatasources": ["docker"],
|
||||
"registryUrls": ["https://harbor.front.kjuulh.io/docker-proxy/library/"]
|
||||
},
|
||||
{
|
||||
"groupName": "all dependencies",
|
||||
"separateMajorMinor": false,
|
||||
"groupSlug": "all",
|
||||
"packageRules": [
|
||||
{
|
||||
"matchPackagePatterns": [
|
||||
"*"
|
||||
],
|
||||
"groupName": "all dependencies",
|
||||
"groupSlug": "all"
|
||||
}
|
||||
],
|
||||
"lockFileMaintenance": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}`,
|
||||
Permissions: 755,
|
||||
Owner: "root",
|
||||
}).
|
||||
WithSecretVariable("RENOVATE_TOKEN", renovateToken).
|
||||
WithSecretVariable("GITHUB_COM_TOKEN", githubComToken).
|
||||
WithSecretVariable("RENOVATE_SECRETS", renovateSecret).
|
||||
WithEnvVariable("LOG_LEVEL", "warn").
|
||||
WithEnvVariable("RENOVATE_CONFIG_FILE", "/opts/renovate/config.json").
|
||||
WithExec([]string{
|
||||
fmt.Sprintf("%s/%s", owner, repository),
|
||||
}).
|
||||
Sync(ctx)
|
||||
|
||||
stdout, outerr := output.Stdout(ctx)
|
||||
if outerr == nil {
|
||||
log.Printf("stdout: %s", stdout)
|
||||
}
|
||||
stderr, outerr := output.Stderr(ctx)
|
||||
if outerr == nil {
|
||||
log.Printf("stderr: %s", stderr)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("error: %w, \nstderr: %s\nstdout: %s", err, stderr, stdout)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
1
crates/contractor/.gitignore
vendored
Normal file
1
crates/contractor/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
25
crates/contractor/Cargo.toml
Normal file
25
crates/contractor/Cargo.toml
Normal file
@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "contractor"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
clap.workspace = true
|
||||
dotenv.workspace = true
|
||||
axum.workspace = true
|
||||
|
||||
serde = { version = "1.0.202", features = ["derive"] }
|
||||
sqlx = { version = "0.7.4", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "time"] }
|
||||
uuid = { version = "1.8.0", features = ["v4"] }
|
||||
tower-http = { version = "0.5.2", features = ["cors", "trace"] }
|
||||
futures = "0.3.30"
|
||||
reqwest = {version = "0.12.4", default-features = false, features = ["json", "rustls-tls"]}
|
||||
itertools = "0.13.0"
|
||||
regex = "1.10.4"
|
||||
serde_json = "1.0.117"
|
||||
dagger-sdk = "0.9.8"
|
||||
backon = "0.4.4"
|
@ -0,0 +1 @@
|
||||
-- Add migration script here
|
137
crates/contractor/src/api.rs
Normal file
137
crates/contractor/src/api.rs
Normal file
@ -0,0 +1,137 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::{
|
||||
extract::{MatchedPath, State},
|
||||
http::Request,
|
||||
response::IntoResponse,
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tower_http::trace::TraceLayer;
|
||||
|
||||
use crate::{
|
||||
services::{
|
||||
bot::{BotRequest, BotState},
|
||||
gitea::Repository,
|
||||
},
|
||||
SharedState,
|
||||
};
|
||||
|
||||
pub async fn serve_axum(state: &SharedState, host: &SocketAddr) -> Result<(), anyhow::Error> {
|
||||
tracing::info!("running webhook server");
|
||||
let app = Router::new()
|
||||
.route("/", get(root))
|
||||
.route("/webhooks/gitea", post(gitea_webhook))
|
||||
.with_state(state.to_owned())
|
||||
.layer(
|
||||
TraceLayer::new_for_http().make_span_with(|request: &Request<_>| {
|
||||
// Log the matched route's path (with placeholders not filled in).
|
||||
// Use request.uri() or OriginalUri if you want the real path.
|
||||
let matched_path = request
|
||||
.extensions()
|
||||
.get::<MatchedPath>()
|
||||
.map(MatchedPath::as_str);
|
||||
|
||||
tracing::info_span!(
|
||||
"http_request",
|
||||
method = ?request.method(),
|
||||
matched_path,
|
||||
some_other_field = tracing::field::Empty,
|
||||
)
|
||||
}), // ...
|
||||
);
|
||||
|
||||
tracing::info!("listening on {}", host);
|
||||
let listener = tokio::net::TcpListener::bind(host).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn root() -> &'static str {
|
||||
"Hello, contractor!"
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct GiteaWebhookComment {
|
||||
body: String,
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct GiteaWebhookRepository {
|
||||
full_name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum GiteaWebhook {
|
||||
Issue {
|
||||
comment: GiteaWebhookComment,
|
||||
repository: GiteaWebhookRepository,
|
||||
},
|
||||
}
|
||||
|
||||
pub enum ApiError {
|
||||
InternalError(anyhow::Error),
|
||||
}
|
||||
|
||||
impl IntoResponse for ApiError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
match self {
|
||||
ApiError::InternalError(e) => {
|
||||
tracing::error!("failed with internal error: {}", e);
|
||||
|
||||
(axum::http::StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
|
||||
}
|
||||
}
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn gitea_webhook(
|
||||
State(state): State<SharedState>,
|
||||
Json(json): Json<GiteaWebhook>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
tracing::info!(
|
||||
"called: {}",
|
||||
serde_json::to_string(&json)
|
||||
.context("failed to serialize webhook")
|
||||
.map_err(ApiError::InternalError)?
|
||||
);
|
||||
|
||||
let bot_req: BotRequest = json.try_into().map_err(ApiError::InternalError)?;
|
||||
|
||||
state
|
||||
.bot()
|
||||
.handle_request(bot_req)
|
||||
.await
|
||||
.map_err(ApiError::InternalError)?;
|
||||
|
||||
Ok("Hello, contractor!")
|
||||
}
|
||||
|
||||
impl TryFrom<GiteaWebhook> for BotRequest {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(value: GiteaWebhook) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
GiteaWebhook::Issue {
|
||||
comment,
|
||||
repository,
|
||||
} => {
|
||||
let (owner, name) = repository.full_name.split_once('/').ok_or(anyhow::anyhow!(
|
||||
"{} did not contain a valid owner/repository",
|
||||
&repository.full_name
|
||||
))?;
|
||||
|
||||
Ok(BotRequest {
|
||||
repo: Repository {
|
||||
owner: owner.into(),
|
||||
name: name.into(),
|
||||
},
|
||||
command: comment.body,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
97
crates/contractor/src/main.rs
Normal file
97
crates/contractor/src/main.rs
Normal file
@ -0,0 +1,97 @@
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use futures::{stream::FuturesUnordered, StreamExt};
|
||||
use tokio::task;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(author, version, about, long_about = None, subcommand_required = true)]
|
||||
struct Command {
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
Serve {
|
||||
#[arg(env = "SERVICE_HOST", long, default_value = "127.0.0.1:3000")]
|
||||
host: SocketAddr,
|
||||
},
|
||||
|
||||
Reconcile {
|
||||
#[arg(long)]
|
||||
user: Option<String>,
|
||||
#[arg(long)]
|
||||
org: Option<Vec<String>>,
|
||||
|
||||
#[arg(long, env = "CONTRACTOR_FILTER")]
|
||||
filter: Option<String>,
|
||||
|
||||
#[arg(long = "force-refresh", env = "CONTRACTOR_FORCE_REFRESH")]
|
||||
force_refresh: bool,
|
||||
},
|
||||
}
|
||||
|
||||
mod api;
|
||||
mod schedule;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
dotenv::dotenv().ok();
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let cli = Command::parse();
|
||||
|
||||
match cli.command {
|
||||
Some(Commands::Serve { host }) => {
|
||||
tracing::info!("Starting service");
|
||||
|
||||
let state = SharedState::from(Arc::new(State::new().await?));
|
||||
|
||||
let mut tasks = FuturesUnordered::new();
|
||||
|
||||
tasks.push({
|
||||
let state = state.clone();
|
||||
task::spawn(async move {
|
||||
serve_axum(&state, &host).await?;
|
||||
Ok::<(), anyhow::Error>(())
|
||||
})
|
||||
});
|
||||
|
||||
tasks.push(task::spawn(async move {
|
||||
serve_cron_jobs(&state).await?;
|
||||
Ok::<(), anyhow::Error>(())
|
||||
}));
|
||||
|
||||
while let Some(result) = tasks.next().await {
|
||||
result??
|
||||
}
|
||||
}
|
||||
Some(Commands::Reconcile {
|
||||
user,
|
||||
org,
|
||||
filter,
|
||||
force_refresh,
|
||||
}) => {
|
||||
tracing::info!("running reconcile");
|
||||
|
||||
let state = SharedState::from(Arc::new(State::new().await?));
|
||||
|
||||
state
|
||||
.reconciler()
|
||||
.reconcile(user, org, filter, force_refresh)
|
||||
.await?;
|
||||
|
||||
tracing::info!("done running reconcile");
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
mod state;
|
||||
pub use crate::state::{SharedState, State};
|
||||
use crate::{api::serve_axum, schedule::serve_cron_jobs, services::reconciler::ReconcilerState};
|
||||
|
||||
mod services;
|
16
crates/contractor/src/schedule.rs
Normal file
16
crates/contractor/src/schedule.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use crate::SharedState;
|
||||
|
||||
pub async fn serve_cron_jobs(state: &SharedState) -> Result<(), anyhow::Error> {
|
||||
let _state = state.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tracing::info!("running cronjobs");
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_secs(10_000)).await;
|
||||
}
|
||||
Ok::<(), anyhow::Error>(())
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(())
|
||||
}
|
5
crates/contractor/src/services.rs
Normal file
5
crates/contractor/src/services.rs
Normal file
@ -0,0 +1,5 @@
|
||||
pub mod bot;
|
||||
pub mod engines;
|
||||
pub mod gitea;
|
||||
pub mod reconciler;
|
||||
pub mod renovate;
|
84
crates/contractor/src/services/bot.rs
Normal file
84
crates/contractor/src/services/bot.rs
Normal file
@ -0,0 +1,84 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::{services::renovate::RenovateConfig, SharedState};
|
||||
|
||||
use super::{engines::dagger::Dagger, gitea::Repository};
|
||||
|
||||
pub struct Bot {
|
||||
command_name: String,
|
||||
|
||||
dagger: Dagger,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(author, version, about, long_about = None, subcommand_required = true)]
|
||||
struct BotCommand {
|
||||
#[command(subcommand)]
|
||||
command: Option<BotCommands>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum BotCommands {
|
||||
Refresh {
|
||||
#[arg(long)]
|
||||
all: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl Bot {
|
||||
pub fn new(dagger: Dagger) -> Self {
|
||||
Self {
|
||||
command_name: std::env::var("CONTRACTOR_COMMAND_NAME").unwrap_or("contractor".into()),
|
||||
|
||||
dagger,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_request(&self, req: impl Into<BotRequest>) -> anyhow::Result<()> {
|
||||
let req: BotRequest = req.into();
|
||||
|
||||
if !req.command.starts_with(&self.command_name) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let cmd = BotCommand::parse_from(req.command.split_whitespace());
|
||||
|
||||
match cmd.command {
|
||||
Some(BotCommands::Refresh { all }) => {
|
||||
tracing::info!("triggering refresh for: {}, all: {}", req.repo, all);
|
||||
|
||||
let dagger = self.dagger.clone();
|
||||
tokio::spawn(async move {
|
||||
match dagger
|
||||
.execute_renovate(&RenovateConfig {
|
||||
repo: format!("{}/{}", &req.repo.owner, &req.repo.name),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(e) => tracing::error!("failed to execute renovate: {}", e),
|
||||
};
|
||||
});
|
||||
}
|
||||
None => {
|
||||
// TODO: Send back the help menu
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BotRequest {
|
||||
pub repo: Repository,
|
||||
pub command: String,
|
||||
}
|
||||
|
||||
pub trait BotState {
|
||||
fn bot(&self) -> Bot;
|
||||
}
|
||||
impl BotState for SharedState {
|
||||
fn bot(&self) -> Bot {
|
||||
Bot::new(self.engine.clone())
|
||||
}
|
||||
}
|
1
crates/contractor/src/services/engines.rs
Normal file
1
crates/contractor/src/services/engines.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod dagger;
|
157
crates/contractor/src/services/engines/dagger.rs
Normal file
157
crates/contractor/src/services/engines/dagger.rs
Normal file
@ -0,0 +1,157 @@
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
|
||||
use dagger_sdk::ContainerWithNewFileOptsBuilder;
|
||||
use futures::Future;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
type DynDagger = Arc<dyn traits::Dagger + Send + Sync + 'static>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Dagger {
|
||||
dagger: DynDagger,
|
||||
}
|
||||
|
||||
impl Default for Dagger {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Dagger {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
dagger: Arc::new(DefaultDagger::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for Dagger {
|
||||
type Target = DynDagger;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.dagger
|
||||
}
|
||||
}
|
||||
|
||||
struct DefaultDagger {
|
||||
client: Arc<RwLock<Option<dagger_sdk::Query>>>,
|
||||
}
|
||||
|
||||
impl DefaultDagger {
|
||||
pub fn new() -> Self {
|
||||
let client = Arc::new(RwLock::new(None));
|
||||
|
||||
let host =
|
||||
std::env::var("CONTRACTOR_DOCKER_HOST").expect("CONTRACTOR_DOCKER_HOST to be set");
|
||||
|
||||
std::env::set_var("DOCKER_HOST", host);
|
||||
|
||||
tokio::spawn({
|
||||
let client = client.clone();
|
||||
|
||||
async move {
|
||||
let mut client = client.write().await;
|
||||
|
||||
match dagger_sdk::connect().await {
|
||||
Ok(o) => *client = Some(o),
|
||||
Err(e) => tracing::error!("failed to start dagger engine: {}", e),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
Self { client }
|
||||
}
|
||||
|
||||
pub async fn get_client(&self) -> dagger_sdk::Query {
|
||||
let client = self.client.clone().read().await.clone();
|
||||
|
||||
client.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl traits::Dagger for DefaultDagger {
|
||||
fn execute_renovate<'a>(
|
||||
&'a self,
|
||||
config: &'a crate::services::renovate::RenovateConfig,
|
||||
) -> std::pin::Pin<Box<dyn Future<Output = anyhow::Result<()>> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
let renovate_image = "renovate/renovate:37";
|
||||
|
||||
let client = self.get_client().await;
|
||||
|
||||
let github_com_token = client.set_secret(
|
||||
"GITHUB_COM_TOKEN",
|
||||
std::env::var("CONTRACTOR_GITHUB_COM_TOKEN")
|
||||
.expect("CONTRACTOR_GITHUB_COM_TOKEN to be set"),
|
||||
);
|
||||
|
||||
let renovate_secrets = client.set_secret(
|
||||
"RENOVATE_SECRETS",
|
||||
std::env::var("CONTRACTOR_RENOVATE_SECRETS")
|
||||
.expect("CONTRACTOR_RENOVATE_SECRETS to be set"),
|
||||
);
|
||||
|
||||
let renovate_token = client.set_secret(
|
||||
"RENOVATE_TOKEN",
|
||||
std::env::var("CONTRACTOR_RENOVATE_TOKEN")
|
||||
.expect("CONTRACTOR_RENOVATE_TOKEN to be set"),
|
||||
);
|
||||
|
||||
let renovate_file_url = std::env::var("CONTRACTOR_RENOVATE_CONFIG_URL")
|
||||
.expect("CONTRACTOR_RENOVATE_CONFIG_URL to be set");
|
||||
|
||||
let renovate_file = client.http(renovate_file_url).contents().await?;
|
||||
|
||||
let mut renovate_file_value: serde_json::Value = serde_json::from_str(&renovate_file)?;
|
||||
let obj = renovate_file_value
|
||||
.as_object_mut()
|
||||
.ok_or(anyhow::anyhow!("config is not a valid json object"))?;
|
||||
|
||||
let _ = obj.insert("autodiscover".into(), serde_json::Value::from_str("false")?);
|
||||
|
||||
let renovate_file = serde_json::to_string(&obj)?;
|
||||
|
||||
let output = client
|
||||
.container()
|
||||
.from(renovate_image)
|
||||
.with_secret_variable("GITHUB_COM_TOKEN", github_com_token)
|
||||
.with_secret_variable("RENOVATE_SECRETS", renovate_secrets)
|
||||
.with_secret_variable("RENOVATE_TOKEN", renovate_token)
|
||||
.with_env_variable("LOG_LEVEL", "info")
|
||||
.with_env_variable("RENOVATE_CONFIG_FILE", "/opt/renovate/config.json")
|
||||
.with_new_file_opts(
|
||||
"/opt/renovate/config.json",
|
||||
ContainerWithNewFileOptsBuilder::default()
|
||||
.contents(renovate_file.as_str())
|
||||
.permissions(0o644isize)
|
||||
.build()?,
|
||||
)
|
||||
.with_exec(vec![&config.repo])
|
||||
.stdout()
|
||||
.await?;
|
||||
|
||||
tracing::debug!(
|
||||
"renovate on: {} finished with output {}",
|
||||
&config.repo,
|
||||
&output
|
||||
);
|
||||
|
||||
Ok::<(), anyhow::Error>(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub mod traits {
|
||||
use std::pin::Pin;
|
||||
|
||||
use futures::Future;
|
||||
|
||||
use crate::services::renovate::RenovateConfig;
|
||||
|
||||
pub trait Dagger {
|
||||
fn execute_renovate<'a>(
|
||||
&'a self,
|
||||
config: &'a RenovateConfig,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<()>> + Send + 'a>>;
|
||||
}
|
||||
}
|
508
crates/contractor/src/services/gitea.rs
Normal file
508
crates/contractor/src/services/gitea.rs
Normal file
@ -0,0 +1,508 @@
|
||||
use std::{fmt::Display, ops::Deref, pin::Pin, sync::Arc};
|
||||
|
||||
type DynGiteaClient = Arc<dyn traits::GiteaClient + Send + Sync + 'static>;
|
||||
pub struct GiteaClient(DynGiteaClient);
|
||||
|
||||
impl GiteaClient {
|
||||
pub fn new() -> Self {
|
||||
Self(Arc::new(DefaultGiteaClient::default()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for GiteaClient {
|
||||
type Target = DynGiteaClient;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Repository {
|
||||
pub owner: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Display for Repository {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!("{}/{}", self.owner, self.name))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<GiteaRepository> for Repository {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: GiteaRepository) -> Result<Self, Self::Error> {
|
||||
let (owner, name) = value
|
||||
.full_name
|
||||
.split_once('/')
|
||||
.ok_or(anyhow::anyhow!(
|
||||
"name of repository is invalid, should contain a /"
|
||||
))
|
||||
.map_err(|e| {
|
||||
tracing::warn!("failed to parse repository: {}", e);
|
||||
|
||||
e
|
||||
})?;
|
||||
|
||||
Ok(Repository {
|
||||
owner: owner.into(),
|
||||
name: name.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct GiteaRepository {
|
||||
full_name: String,
|
||||
}
|
||||
|
||||
pub struct DefaultGiteaClient {
|
||||
url: String,
|
||||
token: String,
|
||||
|
||||
webhook_url: String,
|
||||
}
|
||||
|
||||
impl Default for DefaultGiteaClient {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
url: std::env::var("GITEA_URL")
|
||||
.context("GITEA_URL should be set")
|
||||
.map(|g| g.trim_end_matches('/').to_string())
|
||||
.unwrap(),
|
||||
token: std::env::var("GITEA_TOKEN")
|
||||
.context("GITEA_TOKEN should be set")
|
||||
.unwrap(),
|
||||
webhook_url: std::env::var("CONTRACTOR_URL")
|
||||
.context("CONTRACTOR_URL should be set")
|
||||
.map(|url| format!("{}/webhooks/gitea", url.trim_end_matches('/')))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct GiteaWebhook {
|
||||
id: isize,
|
||||
#[serde(rename = "type")]
|
||||
r#type: GiteaWebhookType,
|
||||
config: GiteaWebhookConfig,
|
||||
}
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct GiteaWebhookConfig {
|
||||
url: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub enum GiteaWebhookType {
|
||||
#[serde(rename = "gitea")]
|
||||
Gitea,
|
||||
Other(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct CreateGiteaWebhook {
|
||||
active: bool,
|
||||
authorization_header: Option<String>,
|
||||
branch_filter: Option<String>,
|
||||
config: CreateGiteaWebhookConfig,
|
||||
events: Vec<String>,
|
||||
#[serde(rename = "type")]
|
||||
r#type: GiteaWebhookType,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct CreateGiteaWebhookConfig {
|
||||
content_type: String,
|
||||
url: String,
|
||||
}
|
||||
|
||||
impl DefaultGiteaClient {
|
||||
async fn fetch_user_repos_page(
|
||||
&self,
|
||||
page: usize,
|
||||
) -> anyhow::Result<(Vec<Repository>, Vec<usize>)> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let url = format!("{}/api/v1/user/repos?page={page}&limit=50", self.url);
|
||||
|
||||
tracing::trace!("calling url: {}", &url);
|
||||
|
||||
let response = client
|
||||
.get(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", format!("token {}", self.token))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let mut pages = Vec::new();
|
||||
if page <= 1 {
|
||||
if let Some(link_header) = response.headers().get("link") {
|
||||
let link_str = link_header.to_str()?;
|
||||
pages = parse_link(page, link_str)?;
|
||||
}
|
||||
}
|
||||
|
||||
let repositories = response.json::<Vec<GiteaRepository>>().await?;
|
||||
|
||||
Ok((
|
||||
repositories
|
||||
.into_iter()
|
||||
.flat_map(Repository::try_from)
|
||||
.collect(),
|
||||
pages,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn fetch_user_repos(&self) -> anyhow::Result<Vec<Repository>> {
|
||||
let (repos, pages) = self.fetch_user_repos_page(1).await?;
|
||||
|
||||
let tasks = pages
|
||||
.into_iter()
|
||||
.map(|page| async move {
|
||||
let (new_repos, _) = self.fetch_user_repos_page(page).await?;
|
||||
|
||||
Ok::<Vec<Repository>, anyhow::Error>(new_repos)
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>();
|
||||
|
||||
let res: Result<Vec<Vec<Repository>>, anyhow::Error> = tasks.try_collect().await;
|
||||
let res = res?.into_iter().flatten();
|
||||
|
||||
Ok(repos.into_iter().chain(res).collect())
|
||||
}
|
||||
|
||||
async fn fetch_org_repos_page(
|
||||
&self,
|
||||
org: &str,
|
||||
page: usize,
|
||||
) -> anyhow::Result<(Vec<Repository>, Vec<usize>)> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let url = format!(
|
||||
"{}/api/v1/orgs/{}/repos?page={page}&limit=50",
|
||||
self.url, org
|
||||
);
|
||||
|
||||
tracing::trace!("calling url: {}", &url);
|
||||
|
||||
let response = client
|
||||
.get(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", format!("token {}", self.token))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let mut pages = Vec::new();
|
||||
if page <= 1 {
|
||||
if let Some(link_header) = response.headers().get("link") {
|
||||
let link_str = link_header.to_str()?;
|
||||
pages = parse_link(page, link_str)?;
|
||||
}
|
||||
}
|
||||
|
||||
let repositories = response.json::<Vec<GiteaRepository>>().await?;
|
||||
|
||||
Ok((
|
||||
repositories
|
||||
.into_iter()
|
||||
.flat_map(Repository::try_from)
|
||||
.collect(),
|
||||
pages,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn fetch_org_repos(&self, org: &str) -> anyhow::Result<Vec<Repository>> {
|
||||
let (repos, pages) = self.fetch_org_repos_page(org, 1).await?;
|
||||
|
||||
let tasks = pages
|
||||
.into_iter()
|
||||
.map(|page| async move {
|
||||
let (new_repos, _) = self.fetch_org_repos_page(org, page).await?;
|
||||
|
||||
Ok::<Vec<Repository>, anyhow::Error>(new_repos)
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>();
|
||||
|
||||
let res: Result<Vec<Vec<Repository>>, anyhow::Error> = tasks.try_collect().await;
|
||||
let res = res?.into_iter().flatten();
|
||||
|
||||
Ok(repos.into_iter().chain(res).collect())
|
||||
}
|
||||
|
||||
async fn fetch_renovate(&self, repo: &Repository) -> anyhow::Result<Option<()>> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let url = format!(
|
||||
"{}/api/v1/repos/{}/{}/contents/renovate.json",
|
||||
self.url, &repo.owner, &repo.name
|
||||
);
|
||||
|
||||
tracing::trace!("calling url: {}", &url);
|
||||
|
||||
let response = (|| async {
|
||||
client
|
||||
.get(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", format!("token {}", self.token))
|
||||
.send()
|
||||
.await
|
||||
})
|
||||
.retry(&ExponentialBuilder::default())
|
||||
.notify(|err, dur| {
|
||||
tracing::debug!("retrying job: {err}, in: {} seconds", dur.as_secs());
|
||||
})
|
||||
.await?;
|
||||
|
||||
match response.error_for_status() {
|
||||
Ok(_) => Ok(Some(())),
|
||||
Err(e) => match e.status() {
|
||||
Some(StatusCode::NOT_FOUND) => Ok(None),
|
||||
Some(status) => {
|
||||
tracing::warn!(
|
||||
"failed to call fetch renovate for: {}, with error: {}",
|
||||
&repo,
|
||||
status
|
||||
);
|
||||
anyhow::bail!(e)
|
||||
}
|
||||
_ => {
|
||||
anyhow::bail!(e)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_webhook(&self, repo: &Repository) -> anyhow::Result<Option<GiteaWebhook>> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let url = format!(
|
||||
"{}/api/v1/repos/{}/{}/hooks",
|
||||
self.url, &repo.owner, &repo.name
|
||||
);
|
||||
|
||||
tracing::trace!("calling url: {}", &url);
|
||||
|
||||
let response = (|| async {
|
||||
client
|
||||
.get(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", format!("token {}", self.token))
|
||||
.send()
|
||||
.await
|
||||
})
|
||||
.retry(&ExponentialBuilder::default())
|
||||
.notify(|err, dur| {
|
||||
tracing::debug!("retrying job: {err}, in: {} seconds", dur.as_secs());
|
||||
})
|
||||
.await?;
|
||||
|
||||
let webhooks = response.json::<Vec<GiteaWebhook>>().await?;
|
||||
|
||||
let valid_webhooks = webhooks
|
||||
.into_iter()
|
||||
.filter(|w| w.r#type == GiteaWebhookType::Gitea)
|
||||
.filter(|w| w.config.url.contains("contractor"))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(valid_webhooks.first().map(|f| f.to_owned()))
|
||||
}
|
||||
|
||||
async fn add_webhook(&self, repo: &Repository) -> anyhow::Result<()> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let url = format!(
|
||||
"{}/api/v1/repos/{}/{}/hooks",
|
||||
self.url, &repo.owner, &repo.name
|
||||
);
|
||||
|
||||
let val = self.create_webhook();
|
||||
|
||||
tracing::trace!(
|
||||
"calling url: {} with body {}",
|
||||
&url,
|
||||
serde_json::to_string(&val)?
|
||||
);
|
||||
|
||||
let response = (|| async {
|
||||
client
|
||||
.post(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json")
|
||||
.header("Authorization", format!("token {}", self.token))
|
||||
.json(&val)
|
||||
.send()
|
||||
.await
|
||||
})
|
||||
.retry(&ExponentialBuilder::default())
|
||||
.notify(|err, dur| {
|
||||
tracing::debug!("retrying job: {err}, in: {} seconds", dur.as_secs());
|
||||
})
|
||||
.await?;
|
||||
|
||||
if let Err(e) = response.error_for_status_ref() {
|
||||
if let Ok(ok) = response.text().await {
|
||||
anyhow::bail!("failed to create webhook: {}, body: {}", e, ok);
|
||||
}
|
||||
|
||||
anyhow::bail!("failed to create webhook: {}", e)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_webhook(&self) -> CreateGiteaWebhook {
|
||||
CreateGiteaWebhook {
|
||||
active: true,
|
||||
authorization_header: Some("something".into()),
|
||||
branch_filter: Some("*".into()),
|
||||
config: CreateGiteaWebhookConfig {
|
||||
content_type: "json".into(),
|
||||
url: format!("{}?type=contractor", self.webhook_url),
|
||||
},
|
||||
events: vec!["pull_request_comment".into(), "issue_comment".into()],
|
||||
r#type: GiteaWebhookType::Gitea,
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_webhook(&self, repo: &Repository, webhook: GiteaWebhook) -> anyhow::Result<()> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let url = format!(
|
||||
"{}/api/v1/repos/{}/{}/hooks/{}",
|
||||
self.url, &repo.owner, &repo.name, &webhook.id,
|
||||
);
|
||||
|
||||
let val = self.create_webhook();
|
||||
|
||||
tracing::trace!(
|
||||
"calling url: {} with body {}",
|
||||
&url,
|
||||
serde_json::to_string(&val)?
|
||||
);
|
||||
|
||||
let response = (|| async {
|
||||
client
|
||||
.patch(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json")
|
||||
.header("Authorization", format!("token {}", self.token))
|
||||
.json(&val)
|
||||
.send()
|
||||
.await
|
||||
})
|
||||
.retry(&ExponentialBuilder::default())
|
||||
.notify(|err, dur| {
|
||||
tracing::debug!("retrying job: {err}, in: {} seconds", dur.as_secs());
|
||||
})
|
||||
.await?;
|
||||
|
||||
if let Err(e) = response.error_for_status_ref() {
|
||||
if let Ok(ok) = response.text().await {
|
||||
anyhow::bail!("failed to create webhook: {}, body: {}", e, ok);
|
||||
}
|
||||
|
||||
anyhow::bail!("failed to create webhook: {}", e)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl traits::GiteaClient for DefaultGiteaClient {
|
||||
fn get_user_repositories<'a>(
|
||||
&'a self,
|
||||
user: &str,
|
||||
) -> Pin<Box<dyn futures::prelude::Future<Output = anyhow::Result<Vec<Repository>>> + Send + 'a>>
|
||||
{
|
||||
tracing::debug!("fetching gitea repositories for user: {user}");
|
||||
|
||||
Box::pin(async { self.fetch_user_repos().await })
|
||||
}
|
||||
|
||||
fn get_org_repositories<'a>(
|
||||
&'a self,
|
||||
org: &'a str,
|
||||
) -> Pin<Box<dyn futures::prelude::Future<Output = anyhow::Result<Vec<Repository>>> + Send + 'a>>
|
||||
{
|
||||
tracing::debug!("fetching gitea repositories for org: {org}");
|
||||
|
||||
Box::pin(async move { self.fetch_org_repos(org).await })
|
||||
}
|
||||
|
||||
fn renovate_enabled<'a>(
|
||||
&'a self,
|
||||
repo: &'a Repository,
|
||||
) -> Pin<Box<dyn futures::prelude::Future<Output = anyhow::Result<bool>> + Send + 'a>> {
|
||||
tracing::trace!("checking whether renovate is enabled for: {:?}", repo);
|
||||
|
||||
Box::pin(async { self.fetch_renovate(repo).await.map(|s| s.is_some()) })
|
||||
}
|
||||
|
||||
fn ensure_webhook<'a>(
|
||||
&'a self,
|
||||
repo: &'a Repository,
|
||||
force_refresh: bool,
|
||||
) -> Pin<Box<dyn futures::prelude::Future<Output = anyhow::Result<()>> + Send + 'a>> {
|
||||
tracing::trace!("ensuring webhook exists for repo: {}", repo);
|
||||
|
||||
Box::pin(async move {
|
||||
match (self.get_webhook(repo).await?, force_refresh) {
|
||||
(Some(_), false) => {
|
||||
tracing::trace!("webhook already found for {} skipping...", repo);
|
||||
}
|
||||
(Some(webhook), true) => {
|
||||
tracing::trace!("webhook already found for {} refreshing it", repo);
|
||||
self.update_webhook(repo, webhook).await?;
|
||||
}
|
||||
(None, _) => {
|
||||
tracing::trace!("webhook was not found for {} adding", repo);
|
||||
self.add_webhook(repo).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// <https://git.front.kjuulh.io/api/v1/user/repos?page=2>; rel="next",<https://git.front.kjuulh.io/api/v1/user/repos?page=9>; rel="last"
|
||||
fn parse_link(page: usize, link_str: &str) -> anyhow::Result<Vec<usize>> {
|
||||
let link_sections = link_str.split(',');
|
||||
|
||||
for link_section in link_sections {
|
||||
if let Some((link, rel)) = link_section.rsplit_once("; ") {
|
||||
if rel == r#"rel="last""# {
|
||||
let actual_link = &link[1..link.len() - 1];
|
||||
|
||||
let url = Url::parse(actual_link)?;
|
||||
|
||||
if let Some(page_num) = url
|
||||
.query_pairs()
|
||||
.into_iter()
|
||||
.find(|(name, _)| name == "page")
|
||||
.map(|(_, value)| value)
|
||||
{
|
||||
let page_num: usize = page_num.parse()?;
|
||||
|
||||
let page_numbers = (page + 1..page_num).collect::<Vec<usize>>();
|
||||
|
||||
return Ok(page_numbers);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Vec::default())
|
||||
}
|
||||
|
||||
mod extensions;
|
||||
pub mod traits;
|
||||
|
||||
use anyhow::Context;
|
||||
use backon::{ExponentialBuilder, Retryable};
|
||||
pub use extensions::*;
|
||||
use futures::{stream::FuturesUnordered, TryStreamExt};
|
||||
use reqwest::{StatusCode, Url};
|
||||
use serde::{Deserialize, Serialize};
|
11
crates/contractor/src/services/gitea/extensions.rs
Normal file
11
crates/contractor/src/services/gitea/extensions.rs
Normal file
@ -0,0 +1,11 @@
|
||||
use crate::SharedState;
|
||||
|
||||
use super::GiteaClient;
|
||||
|
||||
pub trait GiteaClientState {
|
||||
fn gitea_client(&self) -> GiteaClient {
|
||||
GiteaClient::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl GiteaClientState for SharedState {}
|
28
crates/contractor/src/services/gitea/traits.rs
Normal file
28
crates/contractor/src/services/gitea/traits.rs
Normal file
@ -0,0 +1,28 @@
|
||||
use std::pin::Pin;
|
||||
|
||||
use futures::Future;
|
||||
|
||||
use super::Repository;
|
||||
|
||||
pub trait GiteaClient {
|
||||
fn get_user_repositories<'a>(
|
||||
&'a self,
|
||||
user: &str,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<Vec<Repository>>> + Send + 'a>>;
|
||||
|
||||
fn get_org_repositories<'a>(
|
||||
&'a self,
|
||||
org: &'a str,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<Vec<Repository>>> + Send + 'a>>;
|
||||
|
||||
fn renovate_enabled<'a>(
|
||||
&'a self,
|
||||
repo: &'a Repository,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<bool>> + Send + 'a>>;
|
||||
|
||||
fn ensure_webhook<'a>(
|
||||
&'a self,
|
||||
repo: &'a Repository,
|
||||
force_refresh: bool,
|
||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<()>> + Send + 'a>>;
|
||||
}
|
153
crates/contractor/src/services/reconciler.rs
Normal file
153
crates/contractor/src/services/reconciler.rs
Normal file
@ -0,0 +1,153 @@
|
||||
use anyhow::Context;
|
||||
use futures::{stream::FuturesUnordered, StreamExt};
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::SharedState;
|
||||
|
||||
use super::gitea::{GiteaClient, GiteaClientState, Repository};
|
||||
|
||||
pub struct Reconciler {
|
||||
gitea_client: GiteaClient,
|
||||
}
|
||||
|
||||
impl Reconciler {
|
||||
pub fn new(gitea_client: GiteaClient) -> Self {
|
||||
Self { gitea_client }
|
||||
}
|
||||
|
||||
pub async fn reconcile(
|
||||
&self,
|
||||
user: Option<String>,
|
||||
orgs: Option<Vec<String>>,
|
||||
filter: Option<String>,
|
||||
force_refresh: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
let repos = self.get_repos(user, orgs).await?;
|
||||
tracing::debug!("found repositories: {}", repos.len());
|
||||
|
||||
let filtered_repos = match filter {
|
||||
Some(filter) => {
|
||||
let re = regex::Regex::new(&filter).context(
|
||||
"filter regex failed to compile, make sure it is valid against rust-lang/regex",
|
||||
)?;
|
||||
|
||||
repos
|
||||
.into_iter()
|
||||
.filter(|r| {
|
||||
if re.is_match(&r.to_string()) {
|
||||
true
|
||||
} else {
|
||||
tracing::trace!(
|
||||
filter = &filter,
|
||||
"repository: {}, didn't match filter",
|
||||
r.to_string(),
|
||||
);
|
||||
false
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
None => repos,
|
||||
};
|
||||
tracing::debug!("filtered repositories: {}", filtered_repos.len());
|
||||
|
||||
let renovate_enabled = self.get_renovate_enabled(&filtered_repos).await?;
|
||||
tracing::debug!(
|
||||
"found repositories with renovate enabled: {}",
|
||||
renovate_enabled.len()
|
||||
);
|
||||
|
||||
self.ensure_webhook(&renovate_enabled, force_refresh)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_repos(
|
||||
&self,
|
||||
user: Option<String>,
|
||||
orgs: Option<Vec<String>>,
|
||||
) -> anyhow::Result<Vec<Repository>> {
|
||||
let mut repos = Vec::new();
|
||||
|
||||
if let Some(user) = user {
|
||||
let mut r = self.gitea_client.get_user_repositories(&user).await?;
|
||||
|
||||
repos.append(&mut r);
|
||||
}
|
||||
|
||||
if let Some(orgs) = orgs {
|
||||
for org in orgs {
|
||||
let mut r = self.gitea_client.get_org_repositories(&org).await?;
|
||||
repos.append(&mut r);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(repos.into_iter().unique().collect())
|
||||
}
|
||||
|
||||
async fn get_renovate_enabled(&self, repos: &[Repository]) -> anyhow::Result<Vec<Repository>> {
|
||||
let mut futures = FuturesUnordered::new();
|
||||
|
||||
for repo in repos {
|
||||
futures.push(async move {
|
||||
let enabled = self.gitea_client.renovate_enabled(repo).await?;
|
||||
|
||||
if enabled {
|
||||
Ok::<Option<Repository>, anyhow::Error>(Some(repo.to_owned()))
|
||||
} else {
|
||||
tracing::trace!("repository: {:?}, doesn't have renovate enabled", repo);
|
||||
Ok(None)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let mut enabled = Vec::new();
|
||||
|
||||
while let Some(res) = futures.next().await {
|
||||
let res = res?;
|
||||
|
||||
if let Some(repo) = res {
|
||||
enabled.push(repo)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(enabled)
|
||||
}
|
||||
|
||||
async fn ensure_webhook(
|
||||
&self,
|
||||
repos: &[Repository],
|
||||
force_refresh: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
tracing::debug!("ensuring webhooks are setup for repos");
|
||||
|
||||
let mut tasks = FuturesUnordered::new();
|
||||
|
||||
for repo in repos {
|
||||
tasks.push(async move {
|
||||
self.gitea_client
|
||||
.ensure_webhook(repo, force_refresh)
|
||||
.await?;
|
||||
|
||||
Ok::<(), anyhow::Error>(())
|
||||
})
|
||||
}
|
||||
|
||||
while let Some(res) = tasks.next().await {
|
||||
res?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ReconcilerState {
|
||||
fn reconciler(&self) -> Reconciler;
|
||||
}
|
||||
|
||||
impl ReconcilerState for SharedState {
|
||||
fn reconciler(&self) -> Reconciler {
|
||||
Reconciler::new(self.gitea_client())
|
||||
}
|
||||
}
|
3
crates/contractor/src/services/renovate.rs
Normal file
3
crates/contractor/src/services/renovate.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub struct RenovateConfig {
|
||||
pub repo: String,
|
||||
}
|
46
crates/contractor/src/state.rs
Normal file
46
crates/contractor/src/state.rs
Normal file
@ -0,0 +1,46 @@
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
|
||||
use crate::services::engines::dagger::Dagger;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SharedState(Arc<State>);
|
||||
|
||||
impl From<Arc<State>> for SharedState {
|
||||
fn from(value: Arc<State>) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for SharedState {
|
||||
type Target = Arc<State>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
// pub db: Pool<Postgres>,
|
||||
pub engine: Dagger,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub async fn new() -> anyhow::Result<Self> {
|
||||
// let db = sqlx::PgPool::connect(
|
||||
// &std::env::var("DATABASE_URL").context("DATABASE_URL is not set")?,
|
||||
// )
|
||||
// .await?;
|
||||
|
||||
// sqlx::migrate!("migrations/crdb")
|
||||
// .set_locking(false)
|
||||
// .run(&db)
|
||||
// .await?;
|
||||
|
||||
// let _ = sqlx::query("SELECT 1;").fetch_one(&db).await?;
|
||||
|
||||
// Ok(Self { db })
|
||||
let engine = Dagger::new();
|
||||
|
||||
Ok(Self { engine })
|
||||
}
|
||||
}
|
32
cuddle.yaml
32
cuddle.yaml
@ -1,27 +1,21 @@
|
||||
# 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"
|
||||
base: "git@git.front.kjuulh.io:kjuulh/cuddle-rust-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
|
||||
clusters:
|
||||
clank-prod:
|
||||
replicas: "3"
|
||||
namespace: prod
|
||||
|
||||
|
||||
deployment:
|
||||
registry: git@git.front.kjuulh.io:kjuulh/clank-clusters
|
||||
env:
|
||||
prod:
|
||||
clusters:
|
||||
- clank-prod
|
||||
|
||||
scripts:
|
||||
"ci:main":
|
||||
type: shell
|
||||
"ci:pr":
|
||||
type: shell
|
||||
"ci:release":
|
||||
type: shell
|
||||
|
48
go.mod
48
go.mod
@ -1,48 +0,0 @@
|
||||
module git.front.kjuulh.io/kjuulh/contractor
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
dagger.io/dagger v0.8.1
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/spf13/cobra v1.7.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/99designs/gqlgen v0.17.31 // indirect
|
||||
github.com/Khan/genqlient v0.6.0 // indirect
|
||||
github.com/adrg/xdg v0.4.0 // indirect
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/iancoleman/strcase v0.3.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/vektah/gqlparser/v2 v2.5.6 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.11.0 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/net v0.12.0 // indirect
|
||||
golang.org/x/sync v0.3.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
golang.org/x/text v0.11.0 // indirect
|
||||
golang.org/x/tools v0.11.0 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
133
go.sum
133
go.sum
@ -1,133 +0,0 @@
|
||||
dagger.io/dagger v0.8.1 h1:jLNPGubxrLWUfsX+snjaw913B1lxVmWftzdVehB+RQU=
|
||||
dagger.io/dagger v0.8.1/go.mod h1:CZwYt0FfVsEEYTFytzf2ihESB2P4H1S3/UfnrVxjBsE=
|
||||
github.com/99designs/gqlgen v0.17.31 h1:VncSQ82VxieHkea8tz11p7h/zSbvHSxSDZfywqWt158=
|
||||
github.com/99designs/gqlgen v0.17.31/go.mod h1:i4rEatMrzzu6RXaHydq1nmEPZkb3bKQsnxNRHS4DQB4=
|
||||
github.com/Khan/genqlient v0.6.0 h1:Bwb1170ekuNIVIwTJEqvO8y7RxBxXu639VJOkKSrwAk=
|
||||
github.com/Khan/genqlient v0.6.0/go.mod h1:rvChwWVTqXhiapdhLDV4bp9tz/Xvtewwkon4DpWWCRM=
|
||||
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
|
||||
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
|
||||
github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
|
||||
github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
|
||||
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/vektah/gqlparser/v2 v2.5.6 h1:Ou14T0N1s191eRMZ1gARVqohcbe1e8FrcONScsq8cRU=
|
||||
github.com/vektah/gqlparser/v2 v2.5.6/go.mod h1:z8xXUff237NntSuH8mLFijZ+1tjV1swDbpDqjJmk6ME=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
|
||||
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8=
|
||||
golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
19
main.go
19
main.go
@ -1,19 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"git.front.kjuulh.io/kjuulh/contractor/cmd/contractor"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
log.Println("DEBUG: no .env file found")
|
||||
}
|
||||
|
||||
if err := contractor.RootCmd().Execute(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
#!/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"
|
@ -1,25 +0,0 @@
|
||||
#!/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"
|
@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
cargo run -p ci -- local docker-image --image kasperhermansen/cuddle-please --tag dev --bin-name cuddle-please
|
@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
cargo run -p ci -- local build-docs --mkdocs-image $MKDOCS_IMAGE --caddy-image $CADDY_IMAGE
|
@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
docker run --rm -it -p 8000:8000 -v ${PWD}:/docs ${MKDOCS_IMAGE}
|
@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
docker run --rm -it -p 8000:8000 -v ${PWD}:/docs ${MKDOCS_IMAGE}
|
@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
docker run --rm -it -v ${PWD}:/docs ${MKDOCS_IMAGE} new .
|
@ -1 +0,0 @@
|
||||
|
15
templates/docker-compose.yaml
Normal file
15
templates/docker-compose.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
version: "3"
|
||||
services:
|
||||
crdb:
|
||||
restart: 'always'
|
||||
image: 'cockroachdb/cockroach:v23.1.14'
|
||||
command: 'start-single-node --advertise-addr 0.0.0.0 --insecure'
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/health?ready=1"]
|
||||
interval: '10s'
|
||||
timeout: '30s'
|
||||
retries: 5
|
||||
start_period: '20s'
|
||||
ports:
|
||||
- 8080:8080
|
||||
- '26257:26257'
|
Loading…
Reference in New Issue
Block a user