add status to build / repo

This commit is contained in:
mbecker20
2024-05-10 22:59:20 -07:00
parent ea440235c4
commit bba6c4d8b6
13 changed files with 294 additions and 56 deletions

View File

@@ -1,4 +1,4 @@
use std::{str::FromStr, time::Duration};
use std::time::Duration;
use anyhow::{anyhow, Context};
use async_trait::async_trait;
@@ -23,7 +23,7 @@ use monitor_client::{
};
use mungos::{
find::find_collect,
mongodb::bson::{doc, oid::ObjectId, to_bson},
mongodb::bson::{doc, to_bson},
};
use periphery_client::{
api::{self, GetVersionResponse},
@@ -223,7 +223,7 @@ impl Resolve<RunBuild, User> for State {
.await
.builds
.update_one(
doc! { "_id": ObjectId::from_str(&build.id)? },
doc! { "name": &build.name },
doc! {
"$set": {
"config.version": to_bson(&build.config.version)
@@ -352,11 +352,8 @@ async fn get_aws_builder(
) -> anyhow::Result<(PeripheryClient, BuildCleanupData)> {
let start_create_ts = monitor_timestamp();
let instance_name = format!(
"BUILDER-{}-v{}",
build.name,
build.config.version
);
let instance_name =
format!("BUILDER-{}-v{}", build.name, build.config.version);
let Ec2Instance { instance_id, ip } = launch_ec2_instance(
&instance_name,
AwsServerTemplateConfig::from_builder_config(&config),

View File

@@ -1,5 +1,3 @@
use std::str::FromStr;
use anyhow::anyhow;
use async_trait::async_trait;
use monitor_client::{
@@ -14,7 +12,7 @@ use monitor_client::{
Operation,
},
};
use mungos::mongodb::bson::{doc, oid::ObjectId};
use mungos::mongodb::bson::doc;
use periphery_client::api;
use resolver_api::Resolve;
use serror::serialize_error_pretty;
@@ -98,20 +96,7 @@ impl Resolve<CloneRepo, User> for State {
update.finalize();
if update.success {
let res = db_client().await
.repos
.update_one(
doc! { "_id": ObjectId::from_str(&repo.id)? },
doc! { "$set": { "info.last_pulled_at": monitor_timestamp() } },
None,
)
.await;
if let Err(e) = res {
warn!(
"failed to update repo last_pulled_at | repo id: {} | {e:#}",
repo.id
);
}
update_last_pulled(&repo.name).await;
}
update_update(update.clone()).await?;
@@ -168,7 +153,7 @@ impl Resolve<PullRepo, User> for State {
let logs = match periphery
.request(api::git::PullRepo {
name: repo.name,
name: repo.name.clone(),
branch: optional_string(&repo.config.branch),
on_pull: repo.config.on_pull.into_option(),
})
@@ -185,23 +170,27 @@ impl Resolve<PullRepo, User> for State {
update.finalize();
if update.success {
let res = db_client().await
.repos
.update_one(
doc! { "_id": ObjectId::from_str(&repo.id)? },
doc! { "$set": { "info.last_pulled_at": monitor_timestamp() } },
None,
)
.await;
if let Err(e) = res {
warn!(
"failed to update repo last_pulled_at | repo id: {} | {e:#}",
repo.id
);
}
update_last_pulled(&repo.name).await;
}
update_update(update.clone()).await?;
Ok(update)
}
}
async fn update_last_pulled(repo_name: &str) {
let res = db_client()
.await
.repos
.update_one(
doc! { "name": repo_name },
doc! { "$set": { "info.last_pulled_at": monitor_timestamp() } },
None,
)
.await;
if let Err(e) = res {
warn!(
"failed to update repo last_pulled_at | repo: {repo_name} | {e:#}",
);
}
}

View File

@@ -2,8 +2,10 @@ use std::{collections::HashSet, str::FromStr};
use anyhow::{anyhow, Context};
use monitor_client::entities::{
build::BuildStatus,
deployment::{Deployment, DockerContainerState},
permission::PermissionLevel,
repo::RepoStatus,
server::{Server, ServerStatus},
tag::Tag,
update::ResourceTargetVariant,
@@ -15,7 +17,10 @@ use mungos::{
mongodb::bson::{doc, oid::ObjectId, Document},
};
use crate::{resource, state::db_client};
use crate::{
resource,
state::{action_states, db_client},
};
#[instrument(level = "debug")]
pub async fn get_user(user_id: &str) -> anyhow::Result<User> {
@@ -198,3 +203,98 @@ pub fn id_or_name_filter(id_or_name: &str) -> Document {
Err(_) => doc! { "name": id_or_name },
}
}
pub async fn get_build_status(id: &String) -> BuildStatus {
async {
if action_states()
.build
.get(id)
.await
.map(|s| s.get().map(|s| s.building))
.transpose()?
.unwrap_or_default()
{
return Ok(BuildStatus::Building);
}
let status = db_client()
.await
.updates
.find_one(
doc! {
"target.type": "Build",
"target.id": id,
"operation": "RunBuild"
},
None,
)
.await?
.map(|u| {
if u.success {
BuildStatus::Ok
} else {
BuildStatus::Failed
}
})
.unwrap_or(BuildStatus::Ok);
anyhow::Ok(status)
}
.await
.inspect_err(|e| {
warn!("failed to get build status for {id} | {e:#}")
})
.unwrap_or(BuildStatus::Unknown)
}
pub async fn get_repo_status(id: &String) -> RepoStatus {
async {
if let Some(status) = action_states()
.repo
.get(id)
.await
.map(|s| {
s.get().map(|s| {
if s.cloning {
Some(RepoStatus::Cloning)
} else if s.pulling {
Some(RepoStatus::Pulling)
} else {
None
}
})
})
.transpose()?
.flatten()
{
return Ok(status);
}
let status = db_client()
.await
.updates
.find_one(
doc! {
"target.type": "Repo",
"target.id": id,
"$or": [
{ "operation": "CloneRepo" },
{ "operation": "PullRepo" },
],
},
None,
)
.await?
.map(|u| {
if u.success {
RepoStatus::Ok
} else {
RepoStatus::Failed
}
})
.unwrap_or(RepoStatus::Ok);
anyhow::Ok(status)
}
.await
.inspect_err(|e| {
warn!("failed to get repo status for {id} | {e:#}")
})
.unwrap_or(RepoStatus::Unknown)
}

View File

@@ -14,7 +14,7 @@ use monitor_client::entities::{
use mungos::mongodb::Collection;
use crate::{
helpers::empty_or_only_spaces,
helpers::{empty_or_only_spaces, query::get_build_status},
state::{action_states, db_client},
};
@@ -38,6 +38,7 @@ impl super::MonitorResource for Build {
async fn to_list_item(
build: Resource<Self::Config, Self::Info>,
) -> anyhow::Result<Self::ListItem> {
let status = get_build_status(&build.id).await;
Ok(BuildListItem {
name: build.name,
id: build.id,
@@ -48,6 +49,7 @@ impl super::MonitorResource for Build {
version: build.config.version,
repo: build.config.repo,
branch: build.config.branch,
status,
},
})
}

View File

@@ -16,7 +16,7 @@ use periphery_client::api::git::DeleteRepo;
use serror::serialize_error_pretty;
use crate::{
helpers::periphery_client,
helpers::{periphery_client, query::get_repo_status},
state::{action_states, db_client},
};
@@ -42,6 +42,7 @@ impl super::MonitorResource for Repo {
async fn to_list_item(
repo: Resource<Self::Config, Self::Info>,
) -> anyhow::Result<Self::ListItem> {
let status = get_repo_status(&repo.id).await;
Ok(RepoListItem {
name: repo.name,
id: repo.id,
@@ -51,6 +52,7 @@ impl super::MonitorResource for Repo {
last_pulled_at: repo.info.last_pulled_at,
repo: repo.config.repo,
branch: repo.config.branch,
status,
},
})
}

View File

@@ -3,6 +3,7 @@ use derive_default_builder::DefaultBuilder;
use mungos::mongodb::bson::{doc, Document};
use partial_derive2::Partial;
use serde::{Deserialize, Serialize};
use strum::Display;
use typeshare::typeshare;
use crate::entities::I64;
@@ -19,7 +20,7 @@ pub type Build = Resource<BuildConfig, BuildInfo>;
pub type BuildListItem = ResourceListItem<BuildListItemInfo>;
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuildListItemInfo {
/// Unix timestamp in milliseconds of last build
pub last_built_at: I64,
@@ -29,6 +30,24 @@ pub struct BuildListItemInfo {
pub repo: String,
/// The branch of the repo
pub branch: String,
/// Status for the build
pub status: BuildStatus,
}
#[typeshare]
#[derive(
Debug, Clone, Copy, Default, Serialize, Deserialize, Display,
)]
pub enum BuildStatus {
/// Last build successful (or never built)
Ok,
/// Last build failed
Failed,
/// Currently building
Building,
/// Other case
#[default]
Unknown,
}
#[typeshare]

View File

@@ -3,6 +3,7 @@ use derive_default_builder::DefaultBuilder;
use mungos::mongodb::bson::{doc, Document};
use partial_derive2::Partial;
use serde::{Deserialize, Serialize};
use strum::Display;
use typeshare::typeshare;
use crate::entities::I64;
@@ -18,9 +19,32 @@ pub type RepoListItem = ResourceListItem<RepoListItemInfo>;
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct RepoListItemInfo {
/// Repo last cloned / pulled timestamp in ms.
pub last_pulled_at: I64,
/// The configured github repo
pub repo: String,
/// The configured branch
pub branch: String,
/// The repo status
pub status: RepoStatus,
}
#[typeshare]
#[derive(
Debug, Clone, Copy, Default, Serialize, Deserialize, Display,
)]
pub enum RepoStatus {
/// Last clone / pull successful (or never cloned)
Ok,
/// Last clone / pull failed
Failed,
/// Currently cloning
Cloning,
/// Currently pullling
Pulling,
/// Other case
#[default]
Unknown,
}
#[typeshare]
@@ -29,6 +53,7 @@ pub type Repo = Resource<RepoConfig, RepoInfo>;
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct RepoInfo {
/// When repo was last pulled
pub last_pulled_at: I64,
}

View File

@@ -329,6 +329,17 @@ export type Build = Resource<BuildConfig, BuildInfo>;
export type GetBuildResponse = Build;
export enum BuildStatus {
/** Last build successful (or never built) */
Ok = "Ok",
/** Last build failed */
Failed = "Failed",
/** Currently building */
Building = "Building",
/** Other case */
Unknown = "Unknown",
}
export interface BuildListItemInfo {
/** Unix timestamp in milliseconds of last build */
last_built_at: I64;
@@ -338,6 +349,8 @@ export interface BuildListItemInfo {
repo: string;
/** The branch of the repo */
branch: string;
/** Status for the build */
status: BuildStatus;
}
export type BuildListItem = ResourceListItem<BuildListItemInfo>;
@@ -679,6 +692,7 @@ export interface RepoConfig {
}
export interface RepoInfo {
/** When repo was last pulled */
last_pulled_at: I64;
}
@@ -686,10 +700,28 @@ export type Repo = Resource<RepoConfig, RepoInfo>;
export type GetRepoResponse = Repo;
export enum RepoStatus {
/** Last clone / pull successful (or never cloned) */
Ok = "Ok",
/** Last clone / pull failed */
Failed = "Failed",
/** Currently cloning */
Cloning = "Cloning",
/** Currently pullling */
Pulling = "Pulling",
/** Other case */
Unknown = "Unknown",
}
export interface RepoListItemInfo {
/** Repo last cloned / pulled timestamp in ms. */
last_pulled_at: I64;
/** The configured github repo */
repo: string;
/** The configured branch */
branch: string;
/** The repo status */
status: RepoStatus;
}
export type RepoListItem = ResourceListItem<RepoListItemInfo>;

View File

@@ -9,6 +9,9 @@ import { DeleteResource, NewResource } from "../common";
import { DeploymentTable } from "../deployment/table";
import { RunBuild } from "./actions";
import { IconStrictId } from "./icon";
import { bg_color_class_by_intention, build_status_intention } from "@lib/color";
import { Card, CardHeader } from "@ui/card";
import { cn } from "@lib/utils";
const useBuild = (id?: string) =>
useRead("ListBuilds", {}).data?.find((d) => d.id === id);
@@ -28,7 +31,21 @@ export const BuildComponents: RequiredResourceComponents = {
else return <Hammer className="w-4 h-4" />;
},
Status: {},
Status: {
Status: ({ id }) => {
let status = useBuild(id)?.info.status;
const color = bg_color_class_by_intention(
build_status_intention(status)
);
return (
<Card className={cn("w-fit", color)}>
<CardHeader className="py-0 px-2">
{status}
</CardHeader>
</Card>
);
},
},
Info: {
Repo: ({ id }) => {

View File

@@ -1,8 +1,9 @@
import { TagsWithBadge } from "@components/tags";
import { useRead, useTagsFilter } from "@lib/hooks";
import { DataTable, SortableHeader } from "@ui/data-table";
import { fmt_date_with_minutes, fmt_version } from "@lib/formatting";
import { fmt_version } from "@lib/formatting";
import { ResourceLink } from "../common";
import { BuildComponents } from ".";
export const BuildTable = ({ search }: { search?: string }) => {
const builds = useRead("ListBuilds", {}).data;
@@ -39,17 +40,13 @@ export const BuildTable = ({ search }: { search?: string }) => {
),
},
{
accessorKey: "info.last_built_at",
accessorKey: "info.status",
header: ({ column }) => (
<SortableHeader column={column} title="Last Built" />
<SortableHeader column={column} title="Status" />
),
cell: ({ row }) => (
<BuildComponents.Status.Status id={row.original.id} />
),
accessorFn: ({ info: { last_built_at } }) => {
if (last_built_at > 0) {
return fmt_date_with_minutes(new Date(last_built_at));
} else {
return "never";
}
},
},
{
header: "Tags",

View File

@@ -7,6 +7,8 @@ import { RepoConfig } from "./config";
import { CloneRepo, PullRepo } from "./actions";
import { DeleteResource, NewResource } from "../common";
import { RepoTable } from "./table";
import { bg_color_class_by_intention, repo_status_intention } from "@lib/color";
import { cn } from "@lib/utils";
const useRepo = (id?: string) =>
useRead("ListRepos", {}).data?.find((d) => d.id === id);
@@ -40,7 +42,17 @@ export const RepoComponents: RequiredResourceComponents = {
Icon: () => <GitBranch className="w-4 h-4" />,
Status: {},
Status: {
Status: ({ id }) => {
let status = useRepo(id)?.info.status;
const color = bg_color_class_by_intention(repo_status_intention(status));
return (
<Card className={cn("w-fit", color)}>
<CardHeader className="py-0 px-2">{status}</CardHeader>
</Card>
);
},
},
Info: {
Repo: ({ id }) => {

View File

@@ -2,6 +2,7 @@ import { useRead, useTagsFilter } from "@lib/hooks";
import { DataTable, SortableHeader } from "@ui/data-table";
import { ResourceLink } from "../common";
import { TagsWithBadge } from "@components/tags";
import { RepoComponents } from ".";
export const RepoTable = ({ search }: { search?: string }) => {
const tags = useTagsFilter();
@@ -39,6 +40,15 @@ export const RepoTable = ({ search }: { search?: string }) => {
<SortableHeader column={column} title="Branch" />
),
},
{
accessorKey: "info.status",
header: ({ column }) => (
<SortableHeader column={column} title="Status" />
),
cell: ({ row }) => (
<RepoComponents.Status.Status id={row.original.id} />
),
},
{
header: "Tags",
cell: ({ row }) => {

View File

@@ -118,6 +118,42 @@ export const deployment_state_intention: (
}
};
export const build_status_intention = (status?: Types.BuildStatus) => {
switch (status) {
case undefined:
return "None";
case Types.BuildStatus.Unknown:
return "Unknown";
case Types.BuildStatus.Ok:
return "Good";
case Types.BuildStatus.Building:
return "Warning";
case Types.BuildStatus.Failed:
return "Critical";
default:
return "None";
}
}
export const repo_status_intention = (status?: Types.RepoStatus) => {
switch (status) {
case undefined:
return "None";
case Types.RepoStatus.Unknown:
return "Unknown";
case Types.RepoStatus.Ok:
return "Good";
case Types.RepoStatus.Cloning:
return "Warning";
case Types.RepoStatus.Pulling:
return "Warning";
case Types.RepoStatus.Failed:
return "Critical";
default:
return "None";
}
};
export const alert_level_intention: (
level: Types.SeverityLevel
) => ColorIntention = (level) => {