implement build request

This commit is contained in:
mbecker20
2023-06-24 00:42:14 +00:00
parent 2d4c682fac
commit 402259ef11
12 changed files with 275 additions and 66 deletions

View File

@@ -20,7 +20,7 @@ use crate::{
state::{State, StateExtension}, state::{State, StateExtension},
}; };
pub use self::jwt::{JwtClient, RequestUser, RequestUserExtension}; pub use self::jwt::{InnerRequestUser, JwtClient, RequestUser, RequestUserExtension};
pub use github::client::GithubOauthClient; pub use github::client::GithubOauthClient;
pub use google::client::GoogleOauthClient; pub use google::client::GoogleOauthClient;

View File

@@ -1,5 +1,6 @@
pub mod aws; pub mod aws;
pub enum InstanceCleanupData { pub enum BuildCleanupData {
Aws { instance_id: String, region: String, } Server { repo_name: String },
} Aws { instance_id: String, region: String },
}

View File

@@ -1,13 +1,15 @@
use std::pin::Pin; use std::time::Duration;
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
use async_trait::async_trait; use async_trait::async_trait;
use futures::Future; use futures::future::join_all;
use monitor_helpers::{all_logs_success, monitor_timestamp}; use monitor_helpers::monitor_timestamp;
use monitor_types::{ use monitor_types::{
all_logs_success,
entities::{ entities::{
build::{Build, BuildBuilderConfig}, build::{Build, BuildBuilderConfig},
builder::{AwsBuilder, BuilderConfig}, builder::{AwsBuilder, BuilderConfig},
deployment::DockerContainerState,
update::{Log, Update, UpdateStatus, UpdateTarget}, update::{Log, Update, UpdateStatus, UpdateTarget},
Operation, PermissionLevel, Operation, PermissionLevel,
}, },
@@ -15,12 +17,15 @@ use monitor_types::{
requests::api::{CreateBuild, DeleteBuild, GetBuild, ListBuilds, RunBuild, UpdateBuild}, requests::api::{CreateBuild, DeleteBuild, GetBuild, ListBuilds, RunBuild, UpdateBuild},
}; };
use mungos::mongodb::bson::{doc, to_bson}; use mungos::mongodb::bson::{doc, to_bson};
use periphery_client::PeripheryClient; use periphery_client::{
requests::{self, GetVersionResponse},
PeripheryClient,
};
use resolver_api::Resolve; use resolver_api::Resolve;
use crate::{ use crate::{
auth::RequestUser, auth::{InnerRequestUser, RequestUser},
cloud::{aws::Ec2Instance, InstanceCleanupData}, cloud::{aws::Ec2Instance, BuildCleanupData},
helpers::empty_or_only_spaces, helpers::empty_or_only_spaces,
state::State, state::State,
}; };
@@ -78,6 +83,7 @@ impl Resolve<CreateBuild, RequestUser> for State {
name, name,
created_at: start_ts, created_at: start_ts,
updated_at: start_ts, updated_at: start_ts,
last_built_at: 0,
permissions: [(user.id.clone(), PermissionLevel::Update)] permissions: [(user.id.clone(), PermissionLevel::Update)]
.into_iter() .into_iter()
.collect(), .collect(),
@@ -273,6 +279,8 @@ impl Resolve<RunBuild, RequestUser> for State {
}; };
update.id = self.add_update(update.clone()).await?; update.id = self.add_update(update.clone()).await?;
// GET BUILDER PERIPHERY
let builder = self.get_build_builder(&build, &mut update).await; let builder = self.get_build_builder(&build, &mut update).await;
if let Err(e) = &builder { if let Err(e) = &builder {
@@ -286,11 +294,63 @@ impl Resolve<RunBuild, RequestUser> for State {
let (periphery, cleanup_data) = builder.unwrap(); let (periphery, cleanup_data) = builder.unwrap();
// ... // CLONE REPO
let clone_success = match periphery
.request(requests::CloneRepo {
args: (&build).into(),
})
.await
{
Ok(clone_logs) => {
let success = all_logs_success(&clone_logs);
update.logs.extend(clone_logs);
success
}
Err(e) => {
update
.logs
.push(Log::error("clone repo", format!("{e:#?}")));
false
}
};
if clone_success {
match periphery
.request(requests::Build {
build: build.clone(),
})
.await
.context("failed at call to periphery to build")
{
Ok(logs) => update.logs.extend(logs),
Err(e) => update.logs.push(Log::error("build", format!("{e:#?}"))),
};
}
if all_logs_success(&update.logs) {
let _ = self
.db
.builds
.update_one::<Build>(
&build.id,
mungos::Update::Set(doc! {
"version": to_bson(&build.config.version)
.context("failed at converting version to bson")?,
"last_built_at": monitor_timestamp(),
}),
)
.await;
}
// CLEANUP AND FINALIZE UPDATE
self.cleanup_builder_instance(cleanup_data, &mut update) self.cleanup_builder_instance(cleanup_data, &mut update)
.await; .await;
self.handle_post_build_redeploy(&build.id, &mut update)
.await;
update.finalize(); update.finalize();
self.update_update(update.clone()).await?; self.update_update(update.clone()).await?;
@@ -318,19 +378,33 @@ impl Resolve<RunBuild, RequestUser> for State {
} }
} }
const BUILDER_POLL_RATE_SECS: u64 = 2;
const BUILDER_POLL_MAX_TRIES: usize = 30;
impl State { impl State {
async fn get_build_builder( async fn get_build_builder(
&self, &self,
build: &Build, build: &Build,
update: &mut Update, update: &mut Update,
) -> anyhow::Result<(PeripheryClient, Option<InstanceCleanupData>)> { ) -> anyhow::Result<(PeripheryClient, BuildCleanupData)> {
match &build.config.builder { match &build.config.builder {
BuildBuilderConfig::Server { server_id } => { BuildBuilderConfig::Server { server_id } => {
if server_id.is_empty() {
return Err(anyhow!("build has not configured a builder"));
}
let server = self.get_server(server_id).await?; let server = self.get_server(server_id).await?;
let periphery = self.periphery_client(&server); let periphery = self.periphery_client(&server);
Ok((periphery, None)) Ok((
periphery,
BuildCleanupData::Server {
repo_name: build.name.clone(),
},
))
} }
BuildBuilderConfig::Builder { builder_id } => { BuildBuilderConfig::Builder { builder_id } => {
if builder_id.is_empty() {
return Err(anyhow!("build has not configured a builder"));
}
let builder = self.get_builder(builder_id).await?; let builder = self.get_builder(builder_id).await?;
match builder.config { match builder.config {
BuilderConfig::AwsBuilder(config) => { BuilderConfig::AwsBuilder(config) => {
@@ -346,7 +420,9 @@ impl State {
build: &Build, build: &Build,
builder: AwsBuilder, builder: AwsBuilder,
update: &mut Update, update: &mut Update,
) -> anyhow::Result<(PeripheryClient, Option<InstanceCleanupData>)> { ) -> anyhow::Result<(PeripheryClient, BuildCleanupData)> {
let start_create_ts = monitor_timestamp();
let instance_name = format!( let instance_name = format!(
"BUILDER-{}-v{}", "BUILDER-{}-v{}",
build.name, build.name,
@@ -355,34 +431,71 @@ impl State {
let Ec2Instance { instance_id, ip } = let Ec2Instance { instance_id, ip } =
self.create_ec2_instance(&instance_name, &builder).await?; self.create_ec2_instance(&instance_name, &builder).await?;
update let readable_sec_group_ids = builder.security_group_ids.join(", ");
.logs let AwsBuilder {
.push(Log::simple("started builder instance", format!(""))); ami_id,
instance_type,
volume_gb,
subnet_id,
..
} = builder;
let log = Log {
stage: "start build instance".to_string(),
success: true,
stdout: format!("instance id: {instance_id}\nami id: {ami_id}\ninstance type: {instance_type}\nvolume size: {volume_gb} GB\nsubnet id: {subnet_id}\nsecurity groups: {readable_sec_group_ids}"),
start_ts: start_create_ts,
end_ts: monitor_timestamp(),
..Default::default()
};
update.logs.push(log);
self.update_update(update.clone()).await?; self.update_update(update.clone()).await?;
let periphery = PeripheryClient::new(format!("http://{ip}:8000"), &self.config.passkey); let periphery = PeripheryClient::new(format!("http://{ip}:8000"), &self.config.passkey);
Ok(( let start_connect_ts = monitor_timestamp();
periphery, let mut res = Ok(GetVersionResponse {
InstanceCleanupData::Aws { version: String::new(),
instance_id, });
region: builder.region, for _ in 0..BUILDER_POLL_MAX_TRIES {
let version = periphery
.request(requests::GetVersion {})
.await
.context("failed to reach periphery client on builder");
if let Ok(GetVersionResponse { version }) = &version {
let connect_log = Log {
stage: "build instance connected".to_string(),
success: true,
stdout: format!(
"established contact with periphery on builder\nperiphery version: v{}",
version
),
start_ts: start_connect_ts,
end_ts: monitor_timestamp(),
..Default::default()
};
update.logs.push(connect_log);
self.update_update(update.clone()).await?;
return Ok((
periphery,
BuildCleanupData::Aws {
instance_id,
region: builder.region,
},
));
} }
.into(), res = version;
)) tokio::time::sleep(Duration::from_secs(BUILDER_POLL_RATE_SECS)).await;
}
Err(anyhow!("{:#?}", res.err().unwrap()))
} }
async fn cleanup_builder_instance( async fn cleanup_builder_instance(&self, cleanup_data: BuildCleanupData, update: &mut Update) {
&self, match cleanup_data {
cleanup_data: Option<InstanceCleanupData>, BuildCleanupData::Server { repo_name } => {}
update: &mut Update, BuildCleanupData::Aws {
) {
if cleanup_data.is_none() {
return;
}
match cleanup_data.unwrap() {
InstanceCleanupData::Aws {
instance_id, instance_id,
region, region,
} => { } => {
@@ -401,4 +514,81 @@ impl State {
} }
} }
} }
async fn handle_post_build_redeploy(&self, build_id: &str, update: &mut Update) {
let redeploy_deployments = self
.db
.deployments
.get_some(
doc! { "build_id": build_id, "redeploy_on_build": true },
None,
)
.await;
if let Ok(deployments) = redeploy_deployments {
let futures = deployments.into_iter().map(|d| async move {
let request_user: RequestUser = InnerRequestUser {
id: "auto redeploy".to_string(),
is_admin: true,
..Default::default()
}
.into();
let state = self.get_deployment_state(&d).await.unwrap_or_default();
if state == DockerContainerState::Running {
// Some((
// d.id.clone(),
// self.deploy_container(
// &d.id,
// &RequestUser {
// id: "auto redeploy".to_string(),
// is_admin: true,
// ..Default::default()
// },
// None,
// None,
// )
// .await,
// ))
Some(())
} else {
None
}
});
let redeploy_results = join_all(futures).await;
let mut redeploys = Vec::<String>::new();
let mut redeploy_failures = Vec::<String>::new();
// for res in redeploy_results {
// if res.is_none() {
// continue;
// }
// let (id, res) = res.unwrap();
// match res {
// Ok(_) => redeploys.push(id),
// Err(e) => redeploy_failures.push(format!("{id}: {e:#?}")),
// }
// }
if !redeploys.is_empty() {
update.logs.push(Log::simple(
"redeploy",
format!("redeployed deployments: {}", redeploys.join(", ")),
))
}
if !redeploy_failures.is_empty() {
update.logs.push(Log::simple(
"redeploy failures",
redeploy_failures.join("\n"),
))
}
} else if let Err(e) = redeploy_deployments {
update.logs.push(Log::simple(
"redeploys failed",
format!("failed to get deployments to redeploy: {e:#?}"),
))
}
}
} }

View File

@@ -1,6 +1,6 @@
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
use async_trait::async_trait; use async_trait::async_trait;
use monitor_helpers::{all_logs_success, monitor_timestamp, to_monitor_name}; use monitor_helpers::{monitor_timestamp, to_monitor_name};
use monitor_types::{ use monitor_types::{
entities::{ entities::{
deployment::{Deployment, DockerContainerState}, deployment::{Deployment, DockerContainerState},
@@ -11,7 +11,7 @@ use monitor_types::{
requests::api::{ requests::api::{
CreateDeployment, DeleteDeployment, GetDeployment, ListDeployments, RenameDeployment, CreateDeployment, DeleteDeployment, GetDeployment, ListDeployments, RenameDeployment,
UpdateDeployment, UpdateDeployment,
}, }, all_logs_success,
}; };
use mungos::mongodb::bson::{doc, to_bson}; use mungos::mongodb::bson::{doc, to_bson};
use periphery_client::requests; use periphery_client::requests;

View File

@@ -1,27 +1,9 @@
use async_timing_util::unix_timestamp_ms; use async_timing_util::unix_timestamp_ms;
use monitor_types::entities::update::Log;
pub fn to_monitor_name(name: &str) -> String { pub fn to_monitor_name(name: &str) -> String {
name.to_lowercase().replace(' ', "_") name.to_lowercase().replace(' ', "_")
} }
pub fn optional_string(string: &str) -> Option<String> {
if string.is_empty() {
None
} else {
Some(string.to_string())
}
}
pub fn monitor_timestamp() -> i64 { pub fn monitor_timestamp() -> i64 {
unix_timestamp_ms() as i64 unix_timestamp_ms() as i64
} }
pub fn all_logs_success(logs: &Vec<Log>) -> bool {
for log in logs {
if !log.success {
return false;
}
}
true
}

View File

@@ -40,6 +40,10 @@ pub struct Build {
#[builder(setter(skip))] #[builder(setter(skip))]
pub updated_at: I64, pub updated_at: I64,
#[serde(default)]
#[builder(setter(skip))]
pub last_built_at: I64,
pub config: BuildConfig, pub config: BuildConfig,
} }
@@ -161,6 +165,8 @@ pub enum BuildBuilderConfig {
impl Default for BuildBuilderConfig { impl Default for BuildBuilderConfig {
fn default() -> Self { fn default() -> Self {
Self::Server { server_id: Default::default() } Self::Server {
server_id: Default::default(),
}
} }
} }

View File

@@ -6,6 +6,8 @@ use serde::{Deserialize, Serialize};
use strum_macros::{Display, EnumString}; use strum_macros::{Display, EnumString};
use typeshare::typeshare; use typeshare::typeshare;
use crate::optional_string;
pub mod build; pub mod build;
pub mod builder; pub mod builder;
pub mod deployment; pub mod deployment;
@@ -93,6 +95,19 @@ pub struct CloneArgs {
pub github_account: Option<String>, pub github_account: Option<String>,
} }
impl From<&self::build::Build> for CloneArgs {
fn from(build: &self::build::Build) -> CloneArgs {
CloneArgs {
name: build.name.clone(),
repo: optional_string(&build.config.repo),
branch: optional_string(&build.config.branch),
on_clone: build.config.pre_build.clone().into(),
on_pull: None,
github_account: optional_string(&build.config.github_account),
}
}
}
#[typeshare] #[typeshare]
#[derive( #[derive(
Serialize, Deserialize, Debug, Display, EnumString, PartialEq, Hash, Eq, Clone, Copy, Default, Serialize, Deserialize, Debug, Display, EnumString, PartialEq, Hash, Eq, Clone, Copy, Default,

View File

@@ -13,3 +13,20 @@ pub type MongoDocument = mungos::mongodb::bson::Document;
fn i64_is_zero(n: &I64) -> bool { fn i64_is_zero(n: &I64) -> bool {
*n == 0 *n == 0
} }
pub fn all_logs_success(logs: &Vec<entities::update::Log>) -> bool {
for log in logs {
if !log.success {
return false;
}
}
true
}
pub fn optional_string(string: &str) -> Option<String> {
if string.is_empty() {
None
} else {
Some(string.to_string())
}
}

View File

@@ -1,12 +1,12 @@
use std::{collections::HashMap, path::PathBuf}; use std::{collections::HashMap, path::PathBuf};
use anyhow::Context; use anyhow::Context;
use monitor_helpers::{optional_string, to_monitor_name}; use monitor_helpers::to_monitor_name;
use monitor_types::entities::{ use monitor_types::{entities::{
build::{Build, BuildConfig}, build::{Build, BuildConfig},
update::Log, update::Log,
EnvironmentVar, Version, EnvironmentVar, Version,
}; }, optional_string};
use crate::helpers::run_monitor_command; use crate::helpers::run_monitor_command;

View File

@@ -1,15 +1,15 @@
use std::collections::HashMap; use std::collections::HashMap;
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
use monitor_helpers::{optional_string, to_monitor_name}; use monitor_helpers::to_monitor_name;
use monitor_types::entities::{ use monitor_types::{entities::{
deployment::{ deployment::{
Conversion, Deployment, DeploymentConfig, DockerContainerStats, RestartMode, Conversion, Deployment, DeploymentConfig, DockerContainerStats, RestartMode,
TerminationSignal, TerminationSignal,
}, },
update::Log, update::Log,
EnvironmentVar, EnvironmentVar,
}; }, optional_string};
use run_command::async_run_command; use run_command::async_run_command;
use crate::helpers::{docker::parse_extra_args, run_monitor_command}; use crate::helpers::{docker::parse_extra_args, run_monitor_command};

View File

@@ -1,6 +1,5 @@
use async_trait::async_trait; use async_trait::async_trait;
use monitor_helpers::optional_string; use monitor_types::{entities::{server::docker_image::ImageSummary, update::Log}, optional_string};
use monitor_types::entities::{server::docker_image::ImageSummary, update::Log};
use resolver_api::{derive::Request, Resolve}; use resolver_api::{derive::Request, Resolve};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View File

@@ -1,9 +1,8 @@
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
use monitor_helpers::optional_string; use monitor_types::{entities::{
use monitor_types::entities::{
deployment::{BasicContainerInfo, Deployment, DockerContainerStats, TerminationSignal}, deployment::{BasicContainerInfo, Deployment, DockerContainerStats, TerminationSignal},
update::Log, update::Log,
}; }, optional_string};
use resolver_api::{derive::Request, Resolve}; use resolver_api::{derive::Request, Resolve};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};