mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-06 00:02:13 +00:00
fix: bound the credential maintenance pass and gate the gitlab picker on a license
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C1xHmkxuxYb1GYvth1BS75
This commit is contained in:
co-authored by
Claude Opus 5
parent
3ac9fd23e3
commit
1b21708261
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT ws.workspace_id, ws.git_sync\n FROM workspace_settings ws\n JOIN workspace w ON w.id = ws.workspace_id\n WHERE NOT w.deleted\n AND ws.git_sync IS NOT NULL\n AND jsonb_typeof(ws.git_sync->'repositories') = 'array'",
|
||||
"query": "SELECT ws.workspace_id, ws.git_sync\n FROM workspace_settings ws\n JOIN workspace w ON w.id = ws.workspace_id\n WHERE NOT w.deleted\n AND ws.git_sync IS NOT NULL\n AND jsonb_typeof(ws.git_sync->'repositories') = 'array'\n ORDER BY (\n SELECT min((elem->'credential'->>'checked_at')::bigint)\n FROM jsonb_array_elements(ws.git_sync->'repositories') AS elem\n ) ASC NULLS FIRST",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -22,5 +22,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "ddd41de79b23b0436bbc7997751c90d7d82bd20ea30f1dc08f2e2066fb8d6f4b"
|
||||
"hash": "2184f787e6dffc76bfc28f2f2cefc244505996be71d9caec9c6d69e32a9c3d2c"
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
03ef62314a7aa2a1ff4da23459a8fbf3753f495b
|
||||
e38325c9bfd083b3b3308ee5ca96edbcbe308ae3
|
||||
|
||||
+31
-1
@@ -4618,6 +4618,17 @@ const AUTO_PULL_POLL_SLACK_S: i64 = 30;
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
const GIT_CREDENTIAL_LOCK_ID: i64 = 737_483_923;
|
||||
|
||||
/// Wall-clock budget for one credential maintenance pass, spent between
|
||||
/// repositories rather than inside one.
|
||||
///
|
||||
/// `monitor_db` cancels every future in its `join!` at 600s, and an unreachable
|
||||
/// GitLab costs a repository up to the client's 20s timeout, so enough of them
|
||||
/// would take the whole pass — and the other maintenance futures with it. A
|
||||
/// rotation must never be cancelled between GitLab issuing a token and Windmill
|
||||
/// storing it, so the pass stops at a repository boundary instead and the
|
||||
/// least-recently-checked ordering brings the rest along on the next tick.
|
||||
const GIT_CREDENTIAL_PASS_BUDGET: Duration = Duration::from_secs(180);
|
||||
|
||||
/// Refresh every git-sync repository's credential status and rotate the ones near
|
||||
/// expiry, so a token dies visibly (and usually not at all) rather than taking
|
||||
/// sync down on its expiry date.
|
||||
@@ -4670,17 +4681,27 @@ async fn maintain_git_credentials_inner(db: &Pool<Postgres>) -> error::Result<()
|
||||
|
||||
// Same deleted/archived exclusion as the auto-pull poller: a dead workspace's
|
||||
// settings row survives, and rotating a token for one would be pure damage.
|
||||
// Least-recently-checked first, so a pass that runs out of budget resumes
|
||||
// where it stopped instead of re-checking the same head of the list forever.
|
||||
// A repository with no recorded credential sorts first and costs only a
|
||||
// database round trip: it has no token to introspect.
|
||||
let rows = sqlx::query!(
|
||||
r#"SELECT ws.workspace_id, ws.git_sync
|
||||
FROM workspace_settings ws
|
||||
JOIN workspace w ON w.id = ws.workspace_id
|
||||
WHERE NOT w.deleted
|
||||
AND ws.git_sync IS NOT NULL
|
||||
AND jsonb_typeof(ws.git_sync->'repositories') = 'array'"#
|
||||
AND jsonb_typeof(ws.git_sync->'repositories') = 'array'
|
||||
ORDER BY (
|
||||
SELECT min((elem->'credential'->>'checked_at')::bigint)
|
||||
FROM jsonb_array_elements(ws.git_sync->'repositories') AS elem
|
||||
) ASC NULLS FIRST"#
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await?;
|
||||
|
||||
let started = Instant::now();
|
||||
let mut skipped = 0usize;
|
||||
for row in rows {
|
||||
let Some(git_sync) = row.git_sync else {
|
||||
continue;
|
||||
@@ -4697,6 +4718,10 @@ async fn maintain_git_credentials_inner(db: &Pool<Postgres>) -> error::Result<()
|
||||
};
|
||||
|
||||
for repo in &settings.repositories {
|
||||
if started.elapsed() >= GIT_CREDENTIAL_PASS_BUDGET {
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
let path = &repo.git_repo_resource_path;
|
||||
// This refreshes and records the status on every repository it looks at,
|
||||
// rotating only the ones near expiry, so it is the whole maintenance pass
|
||||
@@ -4715,6 +4740,11 @@ async fn maintain_git_credentials_inner(db: &Pool<Postgres>) -> error::Result<()
|
||||
}
|
||||
}
|
||||
}
|
||||
if skipped > 0 {
|
||||
tracing::info!(
|
||||
"git credentials: pass budget reached, {skipped} repositories deferred to the next pass"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { workspaceStore, userStore } from '$lib/stores'
|
||||
import { workspaceStore, userStore, enterpriseLicense } from '$lib/stores'
|
||||
import { GitSyncService, VariableService, type GitlabProject } from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import Popover from './meltComponents/Popover.svelte'
|
||||
@@ -27,9 +27,12 @@
|
||||
let applying = $state(false)
|
||||
let listError: string | undefined = $state(undefined)
|
||||
|
||||
// The project listing is served by an enterprise-only route, so on a build
|
||||
// without it the button would open a form whose first request 404s.
|
||||
let show = $derived(
|
||||
resourceType === 'git_repository' &&
|
||||
!!$workspaceStore &&
|
||||
!!$enterpriseLicense &&
|
||||
($userStore?.is_admin || $userStore?.is_super_admin)
|
||||
)
|
||||
|
||||
|
||||
@@ -617,9 +617,12 @@
|
||||
<div class="text-xs text-secondary">
|
||||
{#if credentialDaysLeft === undefined}
|
||||
Repository token does not expire.
|
||||
{:else if repo.credential.rotatable}
|
||||
{:else if repo.credential.rotatable && $enterpriseLicense}
|
||||
Repository token expires on {repo.credential.expires_at}, and Windmill renews it
|
||||
automatically.
|
||||
{:else if repo.credential.rotatable}
|
||||
Repository token expires on {repo.credential.expires_at}. Renewing it automatically
|
||||
requires an enterprise license.
|
||||
{:else}
|
||||
Repository token expires on {repo.credential.expires_at}. Give it the api or self_rotate
|
||||
scope to let Windmill renew it automatically.
|
||||
|
||||
Reference in New Issue
Block a user