Skip to content

Commit aa8a203

Browse files
committed
feat: usable
1 parent 1a09ff6 commit aa8a203

7 files changed

Lines changed: 297 additions & 3 deletions

File tree

src/endpoint/mod.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
//! The API endpoints.
22
3-
use axum::{Router, routing::get};
3+
use axum::{
4+
Router,
5+
routing::{get, post},
6+
};
47
use tower_http::trace::TraceLayer;
58

9+
use crate::middleware;
10+
611
pub mod health;
12+
pub mod root;
713

814
/// Routes an [`Router`] with the endpoints defined by this module.
915
pub fn route_from(mut app: Router) -> Router {
@@ -17,5 +23,9 @@ fn route_gets(app: Router) -> Router {
1723
}
1824

1925
fn route_posts(app: Router) -> Router {
20-
app
26+
app.route(
27+
"/",
28+
post(root::post)
29+
.route_layer(middleware::auth::layers::KESSOKU_PRIVATE_CI_AUTHORIZATION.to_owned()),
30+
)
2131
}

src/endpoint/root.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,118 @@
11
//! Endpoint root.
2+
3+
use api_framework::{
4+
framework::{
5+
StateError, StateResult,
6+
queued_async::{QueuedAsyncFramework, QueuedAsyncFrameworkContext},
7+
},
8+
static_lazy_lock,
9+
};
10+
11+
use axum::{Json, http::StatusCode, response::IntoResponse};
12+
use config_file::FromConfigFile;
13+
use serde::Deserialize;
14+
15+
use crate::{
16+
config::{
17+
Config,
18+
services::{ServiceConfig, ServicesConfig},
19+
},
20+
env::DOCKER_WORKSPACE_DIR,
21+
transactions,
22+
};
23+
24+
static_lazy_lock! {
25+
QUEUED_ASYNC: QueuedAsyncFramework<String> = QueuedAsyncFramework::new();
26+
}
27+
28+
/// The payload of the post.
29+
#[derive(Debug, Clone, Deserialize)]
30+
pub struct PostPayload {
31+
/// The label of the service to update.
32+
pub service_label: String,
33+
}
34+
35+
/// The client posted an update request.
36+
/// Responds with [`StatusCode::OK`] right after the deployment is triggered.
37+
///
38+
/// See: [`PostPayload`], [`post_transaction`]
39+
pub async fn post(Json(payload): Json<PostPayload>) -> impl IntoResponse {
40+
let services = match ServicesConfig::from_config_file(ServicesConfig::file().path()) {
41+
Ok(s) => s,
42+
Err(e) => {
43+
tracing::error!("failed to read services config: {e:?}");
44+
return (
45+
StatusCode::INTERNAL_SERVER_ERROR,
46+
format!("failed to read services config: {e:?}"),
47+
)
48+
.into_response();
49+
}
50+
};
51+
52+
let service = match services
53+
.services
54+
.iter()
55+
.cloned()
56+
.find(|s| s.service_label == payload.service_label)
57+
{
58+
Some(s) => s,
59+
None => {
60+
return (
61+
StatusCode::BAD_REQUEST,
62+
format!("service with label '{}' not found", payload.service_label),
63+
)
64+
.into_response();
65+
}
66+
};
67+
68+
tokio::spawn(QUEUED_ASYNC.run(payload.service_label.clone(), move |cx| {
69+
Box::pin(post_transaction(cx, payload.clone(), service.clone()))
70+
}));
71+
72+
StatusCode::OK.into_response()
73+
}
74+
75+
async fn post_transaction(
76+
cx: QueuedAsyncFrameworkContext,
77+
_payload: PostPayload,
78+
service: ServiceConfig,
79+
) -> StateResult<()> {
80+
async fn inner(cx: &QueuedAsyncFrameworkContext, service: &ServiceConfig) -> StateResult<()> {
81+
// cd to workspace
82+
transactions::sys::cd(&*DOCKER_WORKSPACE_DIR)
83+
.await
84+
.map_err(|_| StateError::Retry)?;
85+
86+
// login to docker
87+
transactions::docker::login()
88+
.await
89+
.map_err(|_| StateError::Retry)?;
90+
91+
cx.check(())?;
92+
93+
// pull image
94+
transactions::docker::pull_image(&service.image)
95+
.await
96+
.map_err(|_| StateError::Retry)?;
97+
98+
cx.check(())?;
99+
100+
// up container
101+
transactions::docker::compose_up(&service.container_name)
102+
.await
103+
.map_err(|_| StateError::Retry)?;
104+
105+
Ok(())
106+
}
107+
108+
match inner(&cx, &service).await {
109+
Ok(_) => {
110+
tracing::info!("successfully updated service {}", service.service_label);
111+
Ok(())
112+
}
113+
err => {
114+
tracing::error!("failed to update service {}", service.service_label);
115+
err
116+
}
117+
}
118+
}

src/env.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use std::{env, path::PathBuf};
44

5-
use api_framework::{env::parse_env, static_lazy_lock};
5+
use api_framework::{parse_env, static_lazy_lock};
66

77
/// Sets up environment variables.
88
pub fn setup() {
@@ -52,3 +52,13 @@ static_lazy_lock! {
5252
/// The Docker workspace directory. Defaults to `/workspace` if not specified.
5353
pub DOCKER_WORKSPACE_DIR: PathBuf = parse_env!("DOCKER_WORKSPACE_DIR" => |s| Ok(PathBuf::from(s))).unwrap_or(PathBuf::from("/workspace"));
5454
}
55+
56+
static_lazy_lock! {
57+
/// The Docker username.
58+
pub DOCKER_USERNAME: String = env::var("DOCKER_USERNAME").expect("DOCKER_USERNAME not set in environment");
59+
}
60+
61+
static_lazy_lock! {
62+
/// The Docker password.
63+
pub DOCKER_PASSWORD: String = env::var("DOCKER_PASSWORD").expect("DOCKER_PASSWORD not set in environment");
64+
}

src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ use axum::Router;
1414
use tokio::net::TcpListener;
1515
use tracing::{info, trace};
1616

17+
mod transactions;
18+
1719
pub mod config;
1820
pub mod env;
1921
pub mod trace;

src/transactions/docker.rs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
use anyhow::Result;
2+
3+
use crate::env::{DOCKER_PASSWORD, DOCKER_USERNAME};
4+
5+
pub async fn login() -> Result<()> {
6+
match tokio::process::Command::new("docker")
7+
.arg("login")
8+
.arg("-u")
9+
.arg(&*DOCKER_USERNAME)
10+
.arg("-p")
11+
.arg(&*DOCKER_PASSWORD)
12+
.output()
13+
.await
14+
{
15+
Ok(output) => {
16+
if output.status.success() {
17+
tracing::info!(
18+
"successfully logged in to docker with username {}",
19+
&*DOCKER_USERNAME
20+
);
21+
Ok(())
22+
} else {
23+
tracing::error!(
24+
"failed to login to docker with username {}: {}",
25+
&*DOCKER_USERNAME,
26+
String::from_utf8_lossy(&output.stderr)
27+
);
28+
Err(anyhow::anyhow!(
29+
"failed to login to docker with username {}",
30+
&*DOCKER_USERNAME
31+
))
32+
}
33+
}
34+
Err(e) => {
35+
tracing::error!(
36+
"command failed to execute: docker login with username {}: {e:?}",
37+
&*DOCKER_USERNAME
38+
);
39+
Err(anyhow::anyhow!(
40+
"command failed to execute: docker login with username {}",
41+
&*DOCKER_USERNAME
42+
))
43+
}
44+
}
45+
}
46+
47+
pub async fn logout() -> Result<()> {
48+
match tokio::process::Command::new("docker")
49+
.arg("logout")
50+
.output()
51+
.await
52+
{
53+
Ok(output) => {
54+
if output.status.success() {
55+
tracing::info!("successfully logged out from docker");
56+
Ok(())
57+
} else {
58+
tracing::error!(
59+
"failed to logout from docker: {}",
60+
String::from_utf8_lossy(&output.stderr)
61+
);
62+
Err(anyhow::anyhow!("failed to logout from docker"))
63+
}
64+
}
65+
Err(e) => {
66+
tracing::error!("command failed to execute: docker logout: {e:?}");
67+
Err(anyhow::anyhow!("command failed to execute: docker logout"))
68+
}
69+
}
70+
}
71+
72+
pub async fn pull_image(image: &str) -> Result<()> {
73+
match tokio::process::Command::new("docker")
74+
.arg("pull")
75+
.arg(image)
76+
.output()
77+
.await
78+
{
79+
Ok(output) => {
80+
if output.status.success() {
81+
tracing::info!("successfully pulled image {image}");
82+
Ok(())
83+
} else {
84+
tracing::error!(
85+
"failed to pull image {image}: {}",
86+
String::from_utf8_lossy(&output.stderr)
87+
);
88+
Err(anyhow::anyhow!("failed to pull image {image}"))
89+
}
90+
}
91+
Err(e) => {
92+
tracing::error!("command failed to execute: pulling image {image}: {e:?}");
93+
Err(anyhow::anyhow!(
94+
"command failed to execute: pulling image {image}"
95+
))
96+
}
97+
}
98+
}
99+
100+
pub async fn compose_up(container_name: &str) -> Result<()> {
101+
match tokio::process::Command::new("docker")
102+
.arg("compose")
103+
.arg("up")
104+
.arg("-d")
105+
.arg(container_name)
106+
.output()
107+
.await
108+
{
109+
Ok(output) => {
110+
if output.status.success() {
111+
tracing::info!("successfully upped container {container_name}");
112+
Ok(())
113+
} else {
114+
tracing::error!(
115+
"failed to up container {container_name}: {}",
116+
String::from_utf8_lossy(&output.stderr)
117+
);
118+
Err(anyhow::anyhow!("failed to up container {container_name}"))
119+
}
120+
}
121+
Err(e) => {
122+
tracing::error!("command failed to execute: upping container {container_name}: {e:?}");
123+
Err(anyhow::anyhow!(
124+
"command failed to execute: upping container {container_name}"
125+
))
126+
}
127+
}
128+
}

src/transactions/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub mod docker;
2+
pub mod sys;

src/transactions/sys.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use std::path::Path;
2+
3+
use anyhow::Result;
4+
5+
pub async fn cd(path: &Path) -> Result<()> {
6+
match tokio::process::Command::new("cd").arg(path).output().await {
7+
Ok(output) => {
8+
if output.status.success() {
9+
tracing::info!("successfully changed directory to {:?}", path);
10+
Ok(())
11+
} else {
12+
tracing::error!(
13+
"failed to change directory to {:?}: {}",
14+
path,
15+
String::from_utf8_lossy(&output.stderr)
16+
);
17+
Err(anyhow::anyhow!("failed to change directory to {:?}", path))
18+
}
19+
}
20+
Err(e) => {
21+
tracing::error!("command failed to execute: cd {:?}: {e:?}", path);
22+
Err(anyhow::anyhow!("command failed to execute: cd {:?}", path))
23+
}
24+
}
25+
}

0 commit comments

Comments
 (0)