mirror of
https://github.com/GlueOps/autoglue.git
synced 2026-04-17 19:49:23 +02:00
Feat/cluster metadata (#836)
* feat: add cluster metadata key-value store
- Add ClusterMetadata model with ClusterID FK, key, value fields
- Add Metadata []ClusterMetadata relation to Cluster model
- Add CRUD handlers: List, Get, Create, Update, Delete cluster metadata
- Keys are forced to lowercase on create/update
- Values preserve case sensitivity
- Add metadata routes under /clusters/{clusterID}/metadata
- Include metadata in ClusterResponse DTO and clusterToDTO mapping
- Add Preload(Metadata) to all cluster queries
- Register ClusterMetadata in AutoMigrate
Closes: internal-GlueOps/issues#302
* feat: include cluster metadata in prepare payload
- Preload cluster Metadata in ClusterPrepareWorker
- Map cluster metadata into mapper.ClusterToDTO response payload
This ensures metadata key-value pairs are injected into the platform JSON payload used by prepare/bootstrap flows.
* feat: add cluster metadata UI section to configure dialog
* feat: simplify cluster metadata to map[string]string in response
* fix: address cluster metadata PR review feedback
Agent-Logs-Url: https://github.com/GlueOps/autoglue/sessions/f767d4b8-ecae-4cde-bb5c-f0845c5a7cdf
Co-authored-by: yesterdaysrebel <256862558+yesterdaysrebel@users.noreply.github.com>
* chore: finalize review feedback updates
Agent-Logs-Url: https://github.com/GlueOps/autoglue/sessions/f767d4b8-ecae-4cde-bb5c-f0845c5a7cdf
Co-authored-by: yesterdaysrebel <256862558+yesterdaysrebel@users.noreply.github.com>
* chore: revert unintended go.sum change
Agent-Logs-Url: https://github.com/GlueOps/autoglue/sessions/f767d4b8-ecae-4cde-bb5c-f0845c5a7cdf
Co-authored-by: yesterdaysrebel <256862558+yesterdaysrebel@users.noreply.github.com>
* fix: add missing go.sum entry for golang.org/x/tools (swag v2 transitive dep)
* feat: add cluster metadata listing and updating functionality
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: yesterdaysrebel <256862558+yesterdaysrebel@users.noreply.github.com>
This commit is contained in:
@@ -148,6 +148,10 @@ export const clustersApi = {
|
||||
|
||||
// --- metadata ---
|
||||
|
||||
listClusterMetadata: (clusterID: string) =>
|
||||
withRefresh(async () => {
|
||||
return await clusterMetadata.listClusterMetadata({ clusterID })
|
||||
}),
|
||||
createClusterMetadata: (clusterID: string, key: string, value: string) =>
|
||||
withRefresh(async () => {
|
||||
return await clusterMetadata.createClusterMetadata({
|
||||
@@ -155,7 +159,14 @@ export const clustersApi = {
|
||||
createClusterMetadataRequest: { key, value },
|
||||
})
|
||||
}),
|
||||
|
||||
updateClusterMetadata: (clusterID: string, metadataID: string, value: string) =>
|
||||
withRefresh(async () => {
|
||||
return await clusterMetadata.updateClusterMetadata({
|
||||
clusterID,
|
||||
metadataID,
|
||||
updateClusterMetadataRequest: { value },
|
||||
})
|
||||
}),
|
||||
// --- cluster runs / actions ---
|
||||
listClusterRuns: (clusterID: string) =>
|
||||
withRefresh(async () => {
|
||||
|
||||
@@ -5,10 +5,10 @@ import { dnsApi } from "@/api/dns";
|
||||
import { loadBalancersApi } from "@/api/loadbalancers";
|
||||
import { nodePoolsApi } from "@/api/node_pools";
|
||||
import { serversApi } from "@/api/servers";
|
||||
import type { DtoActionResponse, DtoClusterResponse, DtoClusterRunResponse, DtoDomainResponse, DtoLoadBalancerResponse, DtoNodePoolResponse, DtoRecordSetResponse, DtoServerResponse } from "@/sdk";
|
||||
import type { DtoActionResponse, DtoClusterMetadataResponse, DtoClusterResponse, DtoClusterRunResponse, DtoDomainResponse, DtoLoadBalancerResponse, DtoNodePoolResponse, DtoRecordSetResponse, DtoServerResponse } from "@/sdk";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { AlertCircle, CheckCircle2, CircleSlash2, FileCode2, Globe2, Key, Loader2, MapPin, Pencil, Plus, Search, Server, Wrench } from "lucide-react";
|
||||
import { AlertCircle, CheckCircle2, CircleSlash2, FileCode2, Globe2, Key, Loader2, MapPin, Pencil, Plus, Save, Search, Server, Wrench, X } from "lucide-react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -222,6 +222,8 @@ export const ClustersPage = () => {
|
||||
const [nodePoolId, setNodePoolId] = useState("")
|
||||
const [metadataKey, setMetadataKey] = useState("")
|
||||
const [metadataValue, setMetadataValue] = useState("")
|
||||
const [editingMetadataId, setEditingMetadataId] = useState<string | null>(null)
|
||||
const [editingMetadataValue, setEditingMetadataValue] = useState("")
|
||||
const [kubeconfigText, setKubeconfigText] = useState("")
|
||||
const [busyKey, setBusyKey] = useState<string | null>(null)
|
||||
|
||||
@@ -285,6 +287,13 @@ export const ClustersPage = () => {
|
||||
},
|
||||
})
|
||||
|
||||
const metadataQ = useQuery({
|
||||
queryKey: ["cluster-metadata", configCluster?.id],
|
||||
enabled: !!configCluster?.id,
|
||||
queryFn: async () =>
|
||||
asArray<DtoClusterMetadataResponse>(await clustersApi.listClusterMetadata(configCluster!.id!)),
|
||||
})
|
||||
|
||||
const actionLabelByTarget = useMemo(() => {
|
||||
const m = new Map<string, string>()
|
||||
;(actionsQ.data ?? []).forEach((a) => {
|
||||
@@ -412,6 +421,8 @@ export const ClustersPage = () => {
|
||||
useEffect(() => {
|
||||
setMetadataKey("")
|
||||
setMetadataValue("")
|
||||
setEditingMetadataId(null)
|
||||
setEditingMetadataValue("")
|
||||
|
||||
if (!configCluster) {
|
||||
setCaptainDomainId("")
|
||||
@@ -441,6 +452,7 @@ export const ClustersPage = () => {
|
||||
setConfigCluster(updated)
|
||||
await qc.invalidateQueries({ queryKey: ["clusters"] })
|
||||
await qc.invalidateQueries({ queryKey: ["cluster-runs", configCluster.id] })
|
||||
await qc.invalidateQueries({ queryKey: ["cluster-metadata", configCluster.id] })
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -673,6 +685,37 @@ export const ClustersPage = () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdateMetadata() {
|
||||
if (!configCluster?.id || !editingMetadataId) return
|
||||
if (!editingMetadataValue.trim()) return toast.error("Value is required")
|
||||
setBusyKey(`metadata:${editingMetadataId}`)
|
||||
try {
|
||||
await clustersApi.updateClusterMetadata(
|
||||
configCluster.id,
|
||||
editingMetadataId,
|
||||
editingMetadataValue.trim(),
|
||||
)
|
||||
toast.success("Metadata updated.")
|
||||
setEditingMetadataId(null)
|
||||
setEditingMetadataValue("")
|
||||
await refreshConfigCluster()
|
||||
} catch (err: any) {
|
||||
toast.error(err?.message ?? "Failed to update metadata.")
|
||||
} finally {
|
||||
setBusyKey(null)
|
||||
}
|
||||
}
|
||||
|
||||
function beginMetadataEdit(metadata: DtoClusterMetadataResponse) {
|
||||
if (!metadata.id) return
|
||||
setEditingMetadataId(metadata.id)
|
||||
setEditingMetadataValue(metadata.value ?? "")
|
||||
}
|
||||
|
||||
function cancelMetadataEdit() {
|
||||
setEditingMetadataId(null)
|
||||
setEditingMetadataValue("")
|
||||
}
|
||||
|
||||
|
||||
if (clustersQ.isLoading) return <div className="p-6">Loading clusters…</div>
|
||||
@@ -1552,18 +1595,60 @@ export const ClustersPage = () => {
|
||||
|
||||
<div className="mt-3 space-y-1">
|
||||
<Label className="text-xs">Stored Metadata</Label>
|
||||
{configCluster.metadata && Object.keys(configCluster.metadata).length > 0 ? (
|
||||
{metadataQ.isLoading ? (
|
||||
<p className="text-muted-foreground mt-1 text-xs">Loading metadata…</p>
|
||||
) : (metadataQ.data ?? []).length > 0 ? (
|
||||
<div className="divide-border mt-1 rounded-md border">
|
||||
{Object.entries(configCluster.metadata).map(([key, value]) => (
|
||||
{(metadataQ.data ?? []).map((metadata) => (
|
||||
<div
|
||||
key={key}
|
||||
key={metadata.id}
|
||||
className="flex items-center justify-between gap-3 px-3 py-2 text-xs"
|
||||
>
|
||||
<div className="flex flex-col min-w-0">
|
||||
<code className="font-mono font-medium">{key}</code>
|
||||
<code className="text-muted-foreground font-mono text-xs truncate">
|
||||
{value}
|
||||
</code>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
||||
<code className="font-mono font-medium">{metadata.key}</code>
|
||||
{editingMetadataId === metadata.id ? (
|
||||
<Input
|
||||
value={editingMetadataValue}
|
||||
onChange={(e) => setEditingMetadataValue(e.target.value)}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
) : (
|
||||
<code className="text-muted-foreground font-mono text-xs break-all">
|
||||
{metadata.value}
|
||||
</code>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{editingMetadataId === metadata.id ? (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleUpdateMetadata}
|
||||
disabled={
|
||||
isBusy(`metadata:${metadata.id}`) || !editingMetadataValue.trim()
|
||||
}
|
||||
>
|
||||
<Save className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={cancelMetadataEdit}
|
||||
disabled={isBusy(`metadata:${metadata.id}`)}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => beginMetadataEdit(metadata)}
|
||||
disabled={!!editingMetadataId}
|
||||
>
|
||||
<Pencil className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user