diff --git a/bin/core/src/api/read/builder.rs b/bin/core/src/api/read/builder.rs index 16491151b..64975087c 100644 --- a/bin/core/src/api/read/builder.rs +++ b/bin/core/src/api/read/builder.rs @@ -110,7 +110,7 @@ impl Resolve for State { let res = self .resolve( read::GetAvailableAccounts { - server: config.server_id, + server: Some(config.server_id), }, user, ) diff --git a/bin/core/src/api/read/server.rs b/bin/core/src/api/read/server.rs index ca4efb670..f1ed0bc1b 100644 --- a/bin/core/src/api/read/server.rs +++ b/bin/core/src/api/read/server.rs @@ -389,18 +389,24 @@ impl Resolve for State { GetAvailableAccounts { server }: GetAvailableAccounts, user: User, ) -> anyhow::Result { - let server = resource::get_check_permissions::( - &server, - &user, - PermissionLevel::Read, - ) - .await?; + let (github, docker) = match server { + Some(server) => { + let server = resource::get_check_permissions::( + &server, + &user, + PermissionLevel::Read, + ) + .await?; - let GetAccountsResponse { github, docker } = - periphery_client(&server)? - .request(api::GetAccounts {}) - .await - .context("failed to get accounts from periphery")?; + let GetAccountsResponse { github, docker } = + periphery_client(&server)? + .request(api::GetAccounts {}) + .await + .context("failed to get accounts from periphery")?; + (github, docker) + } + None => Default::default(), + }; let mut github_set = HashSet::::new(); diff --git a/bin/core/src/api/write/mod.rs b/bin/core/src/api/write/mod.rs index 9fa8535cd..df4ef1fb1 100644 --- a/bin/core/src/api/write/mod.rs +++ b/bin/core/src/api/write/mod.rs @@ -33,7 +33,7 @@ mod variable; #[resolver_target(State)] #[resolver_args(User)] #[serde(tag = "type", content = "params")] -enum WriteRequest { +pub enum WriteRequest { // ==== SERVICE USER ==== CreateServiceUser(CreateServiceUser), UpdateServiceUserDescription(UpdateServiceUserDescription), diff --git a/bin/core/src/listener/github.rs b/bin/core/src/listener/github.rs index b26c40d17..4a3d8086f 100644 --- a/bin/core/src/listener/github.rs +++ b/bin/core/src/listener/github.rs @@ -5,9 +5,10 @@ use axum::{extract::Path, http::HeaderMap, routing::post, Router}; use hex::ToHex; use hmac::{Hmac, Mac}; use monitor_client::{ - api::execute, + api::{execute, write::RefreshResourceSyncPending}, entities::{ - build::Build, procedure::Procedure, repo::Repo, user::github_user, + build::Build, procedure::Procedure, repo::Repo, + sync::ResourceSync, user::github_user, }, }; use resolver_api::Resolve; @@ -117,6 +118,50 @@ pub fn router() -> Router { }, ) ) + .route( + "/sync/:id/refresh", + post( + |Path(Id { id }), headers: HeaderMap, body: String| async move { + tokio::spawn(async move { + let span = info_span!("sync_refresh_webhook", id); + async { + let res = handle_sync_refresh_webhook( + id.clone(), + headers, + body + ).await; + if let Err(e) = res { + warn!("failed to run sync webook for sync {id} | {e:#}"); + } + } + .instrument(span) + .await + }); + }, + ) + ) + .route( + "/sync/:id/sync", + post( + |Path(Id { id }), headers: HeaderMap, body: String| async move { + tokio::spawn(async move { + let span = info_span!("sync_execute_webhook", id); + async { + let res = handle_sync_execute_webhook( + id.clone(), + headers, + body + ).await; + if let Err(e) = res { + warn!("failed to run sync webook for sync {id} | {e:#}"); + } + } + .instrument(span) + .await + }); + }, + ) + ) } async fn handle_build_webhook( @@ -253,6 +298,66 @@ async fn handle_procedure_webhook( Ok(()) } +async fn handle_sync_refresh_webhook( + sync_id: String, + headers: HeaderMap, + body: String, +) -> anyhow::Result<()> { + // Acquire and hold lock to make a task queue for + // subsequent listener calls on same resource. + // It would fail if we let it go through from action state busy. + let lock = sync_locks().get_or_insert_default(&sync_id).await; + let _lock = lock.lock().await; + + verify_gh_signature(headers, &body).await?; + let request_branch = extract_branch(&body)?; + let sync = resource::get::(&sync_id).await?; + if !sync.config.webhook_enabled { + return Err(anyhow!("sync does not have webhook enabled")); + } + if request_branch != sync.config.branch { + return Err(anyhow!("request branch does not match expected")); + } + let user = github_user().to_owned(); + State + .resolve(RefreshResourceSyncPending { sync: sync_id }, user) + .await?; + Ok(()) +} + +async fn handle_sync_execute_webhook( + sync_id: String, + headers: HeaderMap, + body: String, +) -> anyhow::Result<()> { + // Acquire and hold lock to make a task queue for + // subsequent listener calls on same resource. + // It would fail if we let it go through from action state busy. + let lock = sync_locks().get_or_insert_default(&sync_id).await; + let _lock = lock.lock().await; + + verify_gh_signature(headers, &body).await?; + let request_branch = extract_branch(&body)?; + let sync = resource::get::(&sync_id).await?; + if !sync.config.webhook_enabled { + return Err(anyhow!("sync does not have webhook enabled")); + } + if request_branch != sync.config.branch { + return Err(anyhow!("request branch does not match expected")); + } + let user = github_user().to_owned(); + let req = + crate::api::execute::ExecuteRequest::RunSync(execute::RunSync { + sync: sync_id, + }); + let update = init_execution_update(&req, &user).await?; + let crate::api::execute::ExecuteRequest::RunSync(req) = req else { + unreachable!() + }; + State.resolve(req, (user, update)).await?; + Ok(()) +} + #[instrument(skip_all)] async fn verify_gh_signature( headers: HeaderMap, @@ -313,3 +418,8 @@ fn procedure_locks() -> &'static ListenerLockCache { static BUILD_LOCKS: OnceLock = OnceLock::new(); BUILD_LOCKS.get_or_init(Default::default) } + +fn sync_locks() -> &'static ListenerLockCache { + static SYNC_LOCKS: OnceLock = OnceLock::new(); + SYNC_LOCKS.get_or_init(Default::default) +} diff --git a/client/core/rs/src/api/read/server.rs b/client/core/rs/src/api/read/server.rs index 590c6f1b6..0c5aa9fc4 100644 --- a/client/core/rs/src/api/read/server.rs +++ b/client/core/rs/src/api/read/server.rs @@ -320,7 +320,9 @@ pub struct GetServersSummaryResponse { // /// Get the usernames for the available github / docker accounts -/// on the target server. +/// on the target server, or only available globally if no server +/// is provided. +/// /// Response: [GetAvailableAccountsResponse]. #[typeshare] #[derive( @@ -331,7 +333,7 @@ pub struct GetServersSummaryResponse { pub struct GetAvailableAccounts { /// Id or name #[serde(alias = "id", alias = "name")] - pub server: String, + pub server: Option, } /// Response for [GetAvailableAccounts]. diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index 50f9775bc..c499b7360 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -1197,6 +1197,10 @@ export interface PendingUpdates { server_template_updates?: string; /** Readable log of any pending resource sync updates */ resource_sync_updates?: string; + /** Readable log of any pending variable updates */ + variable_updates?: string; + /** Readable log of any pending user group updates */ + user_group_updates?: string; } export interface ResourceSyncInfo { @@ -2566,12 +2570,14 @@ export interface GetServersSummaryResponse { /** * Get the usernames for the available github / docker accounts - * on the target server. + * on the target server, or only available globally if no server + * is provided. + * * Response: [GetAvailableAccountsResponse]. */ export interface GetAvailableAccounts { /** Id or name */ - server: string; + server?: string; } /** Response for [GetAvailableAccounts]. */ diff --git a/frontend/src/components/config/index.tsx b/frontend/src/components/config/index.tsx index 4655e37f2..fa18431f7 100644 --- a/frontend/src/components/config/index.tsx +++ b/frontend/src/components/config/index.tsx @@ -82,7 +82,7 @@ export const ConfigLayout = < ); }; -type PrimitiveConfigArgs = { placeholder: string }; +type PrimitiveConfigArgs = { placeholder?: string; label?: string }; type ConfigComponent = { label: string; @@ -258,7 +258,7 @@ export const ConfigAgain = < case "string": return ( set({ [key]: value } as Partial)} @@ -270,7 +270,7 @@ export const ConfigAgain = < return ( set({ [key]: Number(value) } as Partial) @@ -283,14 +283,14 @@ export const ConfigAgain = < return ( set({ [key]: value } as Partial)} disabled={disabled} /> ); default: - return
{key.toString()}
; + return
{args?.label ?? key.toString()}
; } } else if (component === false) { return ; diff --git a/frontend/src/components/config/util.tsx b/frontend/src/components/config/util.tsx index f0fbc30d0..ed299d2a0 100644 --- a/frontend/src/components/config/util.tsx +++ b/frontend/src/components/config/util.tsx @@ -194,18 +194,18 @@ export const AccountSelector = ({ placeholder, }: { disabled: boolean; - id: string | undefined; - type: "Server" | "Builder"; + id?: string; + type: "Server" | "None" | "Builder"; account_type: keyof Types.GetBuilderAvailableAccountsResponse; selected: string | undefined; onSelect: (id: string) => void; placeholder: string; }) => { const [request, params] = - type === "Server" - ? ["GetAvailableAccounts", { server: id! }] + type === "Server" || type === "None" + ? ["GetAvailableAccounts", { server: id }] : ["GetBuilderAvailableAccounts", { builder: id }]; - const accounts = useRead(request as any, params, { enabled: !!id }).data; + const accounts = useRead(request as any, params).data; return (