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},
};
pub use self::jwt::{JwtClient, RequestUser, RequestUserExtension};
pub use self::jwt::{InnerRequestUser, JwtClient, RequestUser, RequestUserExtension};
pub use github::client::GithubOauthClient;
pub use google::client::GoogleOauthClient;

View File

@@ -1,5 +1,6 @@
pub mod aws;
pub enum InstanceCleanupData {
Aws { instance_id: String, region: String, }
}
pub enum BuildCleanupData {
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 async_trait::async_trait;
use futures::Future;
use monitor_helpers::{all_logs_success, monitor_timestamp};
use futures::future::join_all;
use monitor_helpers::monitor_timestamp;
use monitor_types::{
all_logs_success,
entities::{
build::{Build, BuildBuilderConfig},
builder::{AwsBuilder, BuilderConfig},
deployment::DockerContainerState,
update::{Log, Update, UpdateStatus, UpdateTarget},
Operation, PermissionLevel,
},
@@ -15,12 +17,15 @@ use monitor_types::{
requests::api::{CreateBuild, DeleteBuild, GetBuild, ListBuilds, RunBuild, UpdateBuild},
};
use mungos::mongodb::bson::{doc, to_bson};
use periphery_client::PeripheryClient;
use periphery_client::{
requests::{self, GetVersionResponse},
PeripheryClient,
};
use resolver_api::Resolve;
use crate::{
auth::RequestUser,
cloud::{aws::Ec2Instance, InstanceCleanupData},
auth::{InnerRequestUser, RequestUser},
cloud::{aws::Ec2Instance, BuildCleanupData},
helpers::empty_or_only_spaces,
state::State,
};
@@ -78,6 +83,7 @@ impl Resolve<CreateBuild, RequestUser> for State {
name,
created_at: start_ts,
updated_at: start_ts,
last_built_at: 0,
permissions: [(user.id.clone(), PermissionLevel::Update)]
.into_iter()
.collect(),
@@ -273,6 +279,8 @@ impl Resolve<RunBuild, RequestUser> for State {
};
update.id = self.add_update(update.clone()).await?;
// GET BUILDER PERIPHERY
let builder = self.get_build_builder(&build, &mut update).await;
if let Err(e) = &builder {
@@ -286,11 +294,63 @@ impl Resolve<RunBuild, RequestUser> for State {
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)
.await;
self.handle_post_build_redeploy(&build.id, &mut update)
.await;
update.finalize();
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 {
async fn get_build_builder(
&self,
build: &Build,
update: &mut Update,
) -> anyhow::Result<(PeripheryClient, Option<InstanceCleanupData>)> {
) -> anyhow::Result<(PeripheryClient, BuildCleanupData)> {
match &build.config.builder {
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 periphery = self.periphery_client(&server);
Ok((periphery, None))
Ok((
periphery,
BuildCleanupData::Server {
repo_name: build.name.clone(),
},
))
}
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?;
match builder.config {
BuilderConfig::AwsBuilder(config) => {
@@ -346,7 +420,9 @@ impl State {
build: &Build,
builder: AwsBuilder,
update: &mut Update,
) -> anyhow::Result<(PeripheryClient, Option<InstanceCleanupData>)> {
) -> anyhow::Result<(PeripheryClient, BuildCleanupData)> {
let start_create_ts = monitor_timestamp();
let instance_name = format!(
"BUILDER-{}-v{}",
build.name,
@@ -355,34 +431,71 @@ impl State {
let Ec2Instance { instance_id, ip } =
self.create_ec2_instance(&instance_name, &builder).await?;
update
.logs
.push(Log::simple("started builder instance", format!("")));
let readable_sec_group_ids = builder.security_group_ids.join(", ");
let AwsBuilder {
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?;
let periphery = PeripheryClient::new(format!("http://{ip}:8000"), &self.config.passkey);
Ok((
periphery,
InstanceCleanupData::Aws {
instance_id,
region: builder.region,
let start_connect_ts = monitor_timestamp();
let mut res = Ok(GetVersionResponse {
version: String::new(),
});
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(
&self,
cleanup_data: Option<InstanceCleanupData>,
update: &mut Update,
) {
if cleanup_data.is_none() {
return;
}
match cleanup_data.unwrap() {
InstanceCleanupData::Aws {
async fn cleanup_builder_instance(&self, cleanup_data: BuildCleanupData, update: &mut Update) {
match cleanup_data {
BuildCleanupData::Server { repo_name } => {}
BuildCleanupData::Aws {
instance_id,
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 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::{
entities::{
deployment::{Deployment, DockerContainerState},
@@ -11,7 +11,7 @@ use monitor_types::{
requests::api::{
CreateDeployment, DeleteDeployment, GetDeployment, ListDeployments, RenameDeployment,
UpdateDeployment,
},
}, all_logs_success,
};
use mungos::mongodb::bson::{doc, to_bson};
use periphery_client::requests;

View File

@@ -1,27 +1,9 @@
use async_timing_util::unix_timestamp_ms;
use monitor_types::entities::update::Log;
pub fn to_monitor_name(name: &str) -> String {
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 {
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))]
pub updated_at: I64,
#[serde(default)]
#[builder(setter(skip))]
pub last_built_at: I64,
pub config: BuildConfig,
}
@@ -161,6 +165,8 @@ pub enum BuildBuilderConfig {
impl Default for BuildBuilderConfig {
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 typeshare::typeshare;
use crate::optional_string;
pub mod build;
pub mod builder;
pub mod deployment;
@@ -93,6 +95,19 @@ pub struct CloneArgs {
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]
#[derive(
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 {
*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 anyhow::Context;
use monitor_helpers::{optional_string, to_monitor_name};
use monitor_types::entities::{
use monitor_helpers::to_monitor_name;
use monitor_types::{entities::{
build::{Build, BuildConfig},
update::Log,
EnvironmentVar, Version,
};
}, optional_string};
use crate::helpers::run_monitor_command;

View File

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

View File

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

View File

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