420 lines
11 KiB
Plaintext
420 lines
11 KiB
Plaintext
# Workspace Locking via Gitea Issues
|
|
#
|
|
# Distributed locking mechanism using Gitea issues
|
|
#
|
|
# Version: 1.0.0
|
|
|
|
use api_client.nu *
|
|
|
|
# Lock label constants
|
|
const LOCK_LABEL_PREFIX = "workspace-lock"
|
|
const READ_LOCK_LABEL = "read-lock"
|
|
const WRITE_LOCK_LABEL = "write-lock"
|
|
const DEPLOY_LOCK_LABEL = "deploy-lock"
|
|
|
|
# Get lock repository
|
|
def get-lock-repo [] -> record {
|
|
let config = get-gitea-config
|
|
let org = $config.repositories.workspaces_org
|
|
|
|
# Use special locks repository
|
|
{org: $org, repo: "workspace-locks"}
|
|
}
|
|
|
|
# Ensure locks repository exists
|
|
def ensure-lock-repo [] -> nothing {
|
|
let lock_repo = get-lock-repo
|
|
|
|
try {
|
|
get-repository $lock_repo.org $lock_repo.repo
|
|
} catch {
|
|
# Create locks repository
|
|
create-repository $lock_repo.org $lock_repo.repo "Workspace locking system" true false
|
|
print $"✓ Created locks repository: ($lock_repo.org)/($lock_repo.repo)"
|
|
}
|
|
}
|
|
|
|
# Format lock issue title
|
|
def format-lock-title [
|
|
workspace_name: string
|
|
lock_type: string
|
|
user: string
|
|
] -> string {
|
|
$"[LOCK:($lock_type)] ($workspace_name) by ($user)"
|
|
}
|
|
|
|
# Format lock issue body
|
|
def format-lock-body [
|
|
workspace_name: string
|
|
lock_type: string
|
|
user: string
|
|
operation?: string
|
|
expiry?: string
|
|
] -> string {
|
|
let timestamp = date now | format date "%Y-%m-%dT%H:%M:%SZ"
|
|
|
|
let body = [
|
|
"## Workspace Lock",
|
|
"",
|
|
$"- **Lock Type**: ($lock_type)",
|
|
$"- **Workspace**: ($workspace_name)",
|
|
$"- **User**: ($user)",
|
|
$"- **Timestamp**: ($timestamp)",
|
|
]
|
|
|
|
let body_with_operation = if ($operation | is-not-empty) {
|
|
$body | append [$"- **Operation**: ($operation)"]
|
|
} else {
|
|
$body
|
|
}
|
|
|
|
let body_with_expiry = if ($expiry | is-not-empty) {
|
|
$body_with_operation | append [$"- **Expiry**: ($expiry)"]
|
|
} else {
|
|
$body_with_operation
|
|
}
|
|
|
|
$body_with_expiry | str join "\n"
|
|
}
|
|
|
|
# Get lock labels
|
|
def get-lock-labels [
|
|
lock_type: string
|
|
] -> list {
|
|
let type_label = match $lock_type {
|
|
"read" => $READ_LOCK_LABEL,
|
|
"write" => $WRITE_LOCK_LABEL,
|
|
"deploy" => $DEPLOY_LOCK_LABEL,
|
|
_ => $lock_type
|
|
}
|
|
|
|
[$LOCK_LABEL_PREFIX, $type_label]
|
|
}
|
|
|
|
# Acquire workspace lock
|
|
export def acquire-workspace-lock [
|
|
workspace_name: string
|
|
lock_type: string # read, write, deploy
|
|
operation?: string
|
|
expiry?: string
|
|
--user: string = ""
|
|
] -> record {
|
|
# Get current user if not specified
|
|
let lock_user = if ($user | is-empty) {
|
|
(get-current-user).login
|
|
} else {
|
|
$user
|
|
}
|
|
|
|
# Ensure locks repository exists
|
|
ensure-lock-repo
|
|
|
|
let lock_repo = get-lock-repo
|
|
|
|
# Check for conflicting locks
|
|
let conflicts = check-lock-conflicts $workspace_name $lock_type
|
|
|
|
if ($conflicts | length) > 0 {
|
|
error make {
|
|
msg: $"Workspace ($workspace_name) is locked"
|
|
help: $"Conflicting locks: (($conflicts | each {|c| $"#($c.number)"} | str join ", "))"
|
|
}
|
|
}
|
|
|
|
# Create lock issue
|
|
let title = format-lock-title $workspace_name $lock_type $lock_user
|
|
let body = format-lock-body $workspace_name $lock_type $lock_user $operation $expiry
|
|
let labels = get-lock-labels $lock_type
|
|
|
|
let issue = create-issue $lock_repo.org $lock_repo.repo $title $body $labels
|
|
|
|
print $"✓ Workspace lock acquired: #($issue.number)"
|
|
|
|
{
|
|
lock_id: $issue.number
|
|
workspace: $workspace_name
|
|
lock_type: $lock_type
|
|
user: $lock_user
|
|
timestamp: (date now | format date "%Y-%m-%dT%H:%M:%SZ")
|
|
operation: $operation
|
|
expiry: $expiry
|
|
}
|
|
}
|
|
|
|
# Release workspace lock
|
|
export def release-workspace-lock [
|
|
workspace_name: string
|
|
lock_id: int
|
|
] -> bool {
|
|
let lock_repo = get-lock-repo
|
|
|
|
# Get lock issue
|
|
let issue = get-issue $lock_repo.org $lock_repo.repo $lock_id
|
|
|
|
# Verify it's a lock for this workspace
|
|
if not ($issue.title | str contains $workspace_name) {
|
|
error make {
|
|
msg: $"Lock #($lock_id) is not for workspace ($workspace_name)"
|
|
}
|
|
}
|
|
|
|
# Close issue
|
|
close-issue $lock_repo.org $lock_repo.repo $lock_id
|
|
|
|
print $"✓ Workspace lock released: #($lock_id)"
|
|
true
|
|
}
|
|
|
|
# Check lock conflicts
|
|
def check-lock-conflicts [
|
|
workspace_name: string
|
|
lock_type: string
|
|
] -> list {
|
|
let lock_repo = get-lock-repo
|
|
|
|
# Get all open locks for workspace
|
|
let all_locks = list-workspace-locks $workspace_name
|
|
|
|
# Check for conflicts
|
|
if $lock_type == "write" or $lock_type == "deploy" {
|
|
# Write/deploy locks conflict with any other lock
|
|
$all_locks
|
|
} else if $lock_type == "read" {
|
|
# Read locks only conflict with write/deploy locks
|
|
$all_locks | filter {|lock|
|
|
$lock.lock_type == "write" or $lock.lock_type == "deploy"
|
|
}
|
|
} else {
|
|
[]
|
|
}
|
|
}
|
|
|
|
# Check if workspace is locked
|
|
export def is-workspace-locked [
|
|
workspace_name: string
|
|
lock_type: string
|
|
] -> bool {
|
|
let conflicts = check-lock-conflicts $workspace_name $lock_type
|
|
($conflicts | length) > 0
|
|
}
|
|
|
|
# List active locks for workspace
|
|
export def list-workspace-locks [
|
|
workspace_name: string
|
|
] -> list {
|
|
let lock_repo = get-lock-repo
|
|
|
|
# Get all open issues with lock label
|
|
let issues = list-issues $lock_repo.org $lock_repo.repo "open" $LOCK_LABEL_PREFIX
|
|
|
|
# Filter for this workspace
|
|
$issues
|
|
| filter {|issue| $issue.title | str contains $workspace_name}
|
|
| each {|issue|
|
|
# Parse lock info from issue
|
|
let lock_type = if ($issue.title | str contains "LOCK:write") {
|
|
"write"
|
|
} else if ($issue.title | str contains "LOCK:read") {
|
|
"read"
|
|
} else if ($issue.title | str contains "LOCK:deploy") {
|
|
"deploy"
|
|
} else {
|
|
"unknown"
|
|
}
|
|
|
|
# Extract user from title
|
|
let title_parts = $issue.title | split row " by "
|
|
let user = if ($title_parts | length) > 1 {
|
|
$title_parts.1
|
|
} else {
|
|
"unknown"
|
|
}
|
|
|
|
{
|
|
number: $issue.number
|
|
workspace: $workspace_name
|
|
lock_type: $lock_type
|
|
user: $user
|
|
created_at: $issue.created_at
|
|
title: $issue.title
|
|
url: $issue.html_url
|
|
}
|
|
}
|
|
}
|
|
|
|
# List all active locks
|
|
export def list-all-locks [] -> list {
|
|
let lock_repo = get-lock-repo
|
|
|
|
# Get all open issues with lock label
|
|
let issues = list-issues $lock_repo.org $lock_repo.repo "open" $LOCK_LABEL_PREFIX
|
|
|
|
$issues | each {|issue|
|
|
# Parse lock info from issue
|
|
let lock_type = if ($issue.title | str contains "LOCK:write") {
|
|
"write"
|
|
} else if ($issue.title | str contains "LOCK:read") {
|
|
"read"
|
|
} else if ($issue.title | str contains "LOCK:deploy") {
|
|
"deploy"
|
|
} else {
|
|
"unknown"
|
|
}
|
|
|
|
# Extract workspace and user from title
|
|
let title_parts = $issue.title | parse "[LOCK:{type}] {workspace} by {user}"
|
|
let parsed = if ($title_parts | length) > 0 {
|
|
$title_parts.0
|
|
} else {
|
|
{workspace: "unknown", user: "unknown"}
|
|
}
|
|
|
|
{
|
|
number: $issue.number
|
|
workspace: $parsed.workspace
|
|
lock_type: $lock_type
|
|
user: $parsed.user
|
|
created_at: $issue.created_at
|
|
title: $issue.title
|
|
url: $issue.html_url
|
|
}
|
|
}
|
|
}
|
|
|
|
# Force release lock (admin only)
|
|
export def force-release-lock [
|
|
workspace_name: string
|
|
lock_id: int
|
|
--reason: string = "Forced unlock"
|
|
] -> bool {
|
|
let lock_repo = get-lock-repo
|
|
|
|
# Get lock issue
|
|
let issue = get-issue $lock_repo.org $lock_repo.repo $lock_id
|
|
|
|
# Add comment about forced unlock
|
|
let current_user = (get-current-user).login
|
|
let comment_body = $"🔓 **Forced unlock by ($current_user)**\n\nReason: ($reason)"
|
|
|
|
# Note: Gitea API doesn't have a direct comment creation endpoint in basic API
|
|
# For now, just close the issue
|
|
close-issue $lock_repo.org $lock_repo.repo $lock_id
|
|
|
|
print $"⚠️ Force released lock #($lock_id) for workspace ($workspace_name)"
|
|
true
|
|
}
|
|
|
|
# Get lock info
|
|
export def get-lock-info [
|
|
workspace_name: string
|
|
lock_id: int
|
|
] -> record {
|
|
let lock_repo = get-lock-repo
|
|
|
|
let issue = get-issue $lock_repo.org $lock_repo.repo $lock_id
|
|
|
|
# Verify it's a lock for this workspace
|
|
if not ($issue.title | str contains $workspace_name) {
|
|
error make {
|
|
msg: $"Lock #($lock_id) is not for workspace ($workspace_name)"
|
|
}
|
|
}
|
|
|
|
# Parse lock type
|
|
let lock_type = if ($issue.title | str contains "LOCK:write") {
|
|
"write"
|
|
} else if ($issue.title | str contains "LOCK:read") {
|
|
"read"
|
|
} else if ($issue.title | str contains "LOCK:deploy") {
|
|
"deploy"
|
|
} else {
|
|
"unknown"
|
|
}
|
|
|
|
# Extract user from title
|
|
let title_parts = $issue.title | split row " by "
|
|
let user = if ($title_parts | length) > 1 {
|
|
$title_parts.1
|
|
} else {
|
|
"unknown"
|
|
}
|
|
|
|
{
|
|
lock_id: $issue.number
|
|
workspace: $workspace_name
|
|
lock_type: $lock_type
|
|
user: $user
|
|
created_at: $issue.created_at
|
|
updated_at: $issue.updated_at
|
|
state: $issue.state
|
|
title: $issue.title
|
|
body: $issue.body
|
|
url: $issue.html_url
|
|
labels: $issue.labels
|
|
}
|
|
}
|
|
|
|
# Cleanup expired locks
|
|
export def cleanup-expired-locks [] -> list {
|
|
let lock_repo = get-lock-repo
|
|
let now = date now
|
|
|
|
let all_locks = list-all-locks
|
|
|
|
# Find expired locks (based on expiry in body)
|
|
let expired = $all_locks | each {|lock|
|
|
let info = get-lock-info $lock.workspace $lock.number
|
|
|
|
# Parse expiry from body
|
|
let expiry_line = $info.body | lines | filter {|line| $line | str contains "Expiry:"}
|
|
if ($expiry_line | length) > 0 {
|
|
let expiry_str = $expiry_line.0 | str replace "- **Expiry**: " "" | str trim
|
|
let expiry = try {
|
|
$expiry_str | into datetime
|
|
} catch {
|
|
null
|
|
}
|
|
|
|
if ($expiry | is-not-empty) and ($expiry < $now) {
|
|
$lock
|
|
} else {
|
|
null
|
|
}
|
|
} else {
|
|
null
|
|
}
|
|
} | filter {|x| $x != null}
|
|
|
|
# Close expired locks
|
|
$expired | each {|lock|
|
|
close-issue $lock_repo.org $lock_repo.repo $lock.number
|
|
print $"✓ Closed expired lock: #($lock.number) for ($lock.workspace)"
|
|
$lock
|
|
}
|
|
}
|
|
|
|
# Auto-lock wrapper for operations
|
|
export def with-workspace-lock [
|
|
workspace_name: string
|
|
lock_type: string
|
|
operation: string
|
|
command: closure
|
|
] -> any {
|
|
# Acquire lock
|
|
let lock = acquire-workspace-lock $workspace_name $lock_type $operation
|
|
|
|
# Execute command
|
|
let result = try {
|
|
do $command
|
|
} catch {|err|
|
|
# Release lock on error
|
|
release-workspace-lock $workspace_name $lock.lock_id
|
|
error make $err
|
|
}
|
|
|
|
# Release lock
|
|
release-workspace-lock $workspace_name $lock.lock_id
|
|
|
|
$result
|
|
}
|