From db28bc042573228a38521211168cc0e4dd386771 Mon Sep 17 00:00:00 2001 From: loveuer Date: Wed, 12 Nov 2025 21:06:14 +0800 Subject: [PATCH] feat: add k8s cluster resource management module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add K8s module with kubeconfig storage in cluster_config table - Implement resource list APIs for 8 kinds: namespace, deployment, statefulset, service, configmap, pod, pv, pvc - Add K8sResourceList page with sidebar navigation and resource tables - Support YAML upload/input dialog for resource creation via dynamic client - Add kubeconfig settings drawer - Increase main content width from lg to xl for better table display - Add navigation link for cluster resources in top bar 🤖 Generated with [Qoder][https://qoder.com] --- dev.sh | 7 +- frontend/src/App.tsx | 5 +- frontend/src/pages/K8sResourceList.tsx | 484 ++++++++++++++++++++++++ frontend/vite.config.ts | 2 +- go.mod | 37 +- go.sum | 132 ++++++- internal/api/api.go | 25 ++ internal/model/model.go | 11 + internal/module/k8s/handler.config.go | 54 +++ internal/module/k8s/handler.resource.go | 301 +++++++++++++++ internal/module/k8s/k8s.go | 21 + 11 files changed, 1069 insertions(+), 10 deletions(-) create mode 100644 frontend/src/pages/K8sResourceList.tsx create mode 100644 internal/module/k8s/handler.config.go create mode 100644 internal/module/k8s/handler.resource.go create mode 100644 internal/module/k8s/k8s.go diff --git a/dev.sh b/dev.sh index ffc7836..ac293fa 100755 --- a/dev.sh +++ b/dev.sh @@ -47,6 +47,11 @@ if ! command -v go &> /dev/null; then exit 1 fi +if ! go build -o /dev/null .;then + echo -e "${RED}Error: backend can't compile.${NC}" + exit 1 +fi + echo -e "${GREEN}Starting backend (Go)...${NC}" go run main.go & BACKEND_PID=$! @@ -66,4 +71,4 @@ echo -e "${YELLOW}Frontend PID: $FRONTEND_PID${NC}" echo -e "${YELLOW}Press Ctrl+C to stop both services${NC}" # Wait for both processes -wait $BACKEND_PID $FRONTEND_PID \ No newline at end of file +wait $BACKEND_PID $FRONTEND_PID diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b4b1a3c..5c92410 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,6 +2,7 @@ import { Container, Typography, Box, AppBar, Toolbar, Button, Stack } from '@mui import { Routes, Route, Link } from 'react-router-dom' import { useAppStore } from './stores/appStore' import RegistryImageList from './pages/RegistryImageList' +import K8sResourceList from './pages/K8sResourceList' function App() { const { count, increment, decrement, reset } = useAppStore() @@ -15,11 +16,13 @@ function App() { + - + } /> + } /> diff --git a/frontend/src/pages/K8sResourceList.tsx b/frontend/src/pages/K8sResourceList.tsx new file mode 100644 index 0000000..2e91c0d --- /dev/null +++ b/frontend/src/pages/K8sResourceList.tsx @@ -0,0 +1,484 @@ +import { useState, useEffect, useRef } from 'react' +import { + Box, + Typography, + Drawer, + List, + ListItem, + ListItemButton, + ListItemText, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + CircularProgress, + Alert, + IconButton, + TextField, + Button, + Stack, + Divider, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Snackbar, +} from '@mui/material' +import SettingsIcon from '@mui/icons-material/Settings' +import CloseIcon from '@mui/icons-material/Close' +import AddIcon from '@mui/icons-material/Add' +import UploadFileIcon from '@mui/icons-material/UploadFile' + +const KINDS = [ + { key: 'namespace', label: 'Namespace', endpoint: '/api/v1/k8s/namespace/list' }, + { key: 'deployment', label: 'Deployment', endpoint: '/api/v1/k8s/deployment/list' }, + { key: 'statefulset', label: 'StatefulSet', endpoint: '/api/v1/k8s/statefulset/list' }, + { key: 'service', label: 'Service', endpoint: '/api/v1/k8s/service/list' }, + { key: 'configmap', label: 'ConfigMap', endpoint: '/api/v1/k8s/configmap/list' }, + { key: 'pod', label: 'Pod', endpoint: '/api/v1/k8s/pod/list' }, + { key: 'pv', label: 'PersistentVolume', endpoint: '/api/v1/k8s/pv/list' }, + { key: 'pvc', label: 'PersistentVolumeClaim', endpoint: '/api/v1/k8s/pvc/list' }, +] + +const DRAWER_WIDTH = 240 + +export default function K8sResourceList() { + const [selectedKind, setSelectedKind] = useState(KINDS[0]) + const [resources, setResources] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [settingsOpen, setSettingsOpen] = useState(false) + const [kubeconfig, setKubeconfig] = useState('') + const [kubeconfigError, setKubeconfigError] = useState(false) + const [namespace, setNamespace] = useState('') + const [createDialogOpen, setCreateDialogOpen] = useState(false) + const [yamlContent, setYamlContent] = useState('') + const [applyLoading, setApplyLoading] = useState(false) + const [snackbar, setSnackbar] = useState<{ open: boolean; message: string; severity: 'success' | 'error' }>({ + open: false, + message: '', + severity: 'success', + }) + const fileInputRef = useRef(null) + + useEffect(() => { + fetchKubeconfig() + }, []) + + useEffect(() => { + if (kubeconfig) { + fetchResources() + } + }, [selectedKind, namespace]) + + const fetchKubeconfig = async () => { + try { + const res = await fetch('/api/v1/k8s/config') + const result = await res.json() + setKubeconfig(result.data?.kubeconfig || '') + if (!result.data?.kubeconfig) { + setKubeconfigError(true) + } + } catch (e: any) { + console.error('Failed to fetch kubeconfig:', e) + } + } + + const saveKubeconfig = async () => { + try { + const res = await fetch('/api/v1/k8s/config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ kubeconfig }), + }) + if (!res.ok) throw new Error('Failed to save kubeconfig') + setKubeconfigError(false) + setSettingsOpen(false) + fetchResources() + } catch (e: any) { + setError(e.message) + } + } + + const handleFileUpload = (event: React.ChangeEvent) => { + const file = event.target.files?.[0] + if (file) { + const reader = new FileReader() + reader.onload = (e) => { + const content = e.target?.result as string + setYamlContent(content) + } + reader.readAsText(file) + } + } + + const handleApplyYaml = async () => { + if (!yamlContent.trim()) { + setSnackbar({ open: true, message: 'YAML 内容不能为空', severity: 'error' }) + return + } + + setApplyLoading(true) + try { + const res = await fetch('/api/v1/k8s/resource/apply', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ yaml: yamlContent }), + }) + + const result = await res.json() + + if (!res.ok) { + throw new Error(result.err || 'Failed to apply resource') + } + + setSnackbar({ open: true, message: '资源应用成功', severity: 'success' }) + setCreateDialogOpen(false) + setYamlContent('') + fetchResources() + } catch (e: any) { + setSnackbar({ open: true, message: `应用失败: ${e.message}`, severity: 'error' }) + } finally { + setApplyLoading(false) + } + } + + const fetchResources = async () => { + if (!kubeconfig) { + setKubeconfigError(true) + return + } + + setLoading(true) + setError(null) + try { + const url = new URL(selectedKind.endpoint, window.location.origin) + if (namespace && selectedKind.key !== 'namespace' && selectedKind.key !== 'pv') { + url.searchParams.set('namespace', namespace) + } + + const res = await fetch(url.toString()) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + const result = await res.json() + setResources(result.data?.items || []) + } catch (e: any) { + setError(e.message) + } finally { + setLoading(false) + } + } + + const getResourceColumns = () => { + switch (selectedKind.key) { + case 'namespace': + return ['Name', 'Status', 'Age'] + case 'deployment': + case 'statefulset': + return ['Name', 'Namespace', 'Replicas', 'Age'] + case 'service': + return ['Name', 'Namespace', 'Type', 'Cluster IP', 'External IP', 'Ports', 'Age'] + case 'configmap': + return ['Name', 'Namespace', 'Data Keys', 'Age'] + case 'pod': + return ['Name', 'Namespace', 'Status', 'IP', 'Node', 'Age'] + case 'pv': + return ['Name', 'Capacity', 'Access Modes', 'Status', 'Claim', 'Age'] + case 'pvc': + return ['Name', 'Namespace', 'Status', 'Volume', 'Capacity', 'Access Modes', 'Age'] + default: + return ['Name'] + } + } + + const renderResourceRow = (resource: any) => { + const metadata = resource.metadata || {} + const spec = resource.spec || {} + const status = resource.status || {} + + const getAge = (timestamp: string) => { + if (!timestamp) return '-' + const diff = Date.now() - new Date(timestamp).getTime() + const days = Math.floor(diff / (1000 * 60 * 60 * 24)) + const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)) + if (days > 0) return `${days}d` + return `${hours}h` + } + + switch (selectedKind.key) { + case 'namespace': + return ( + + {metadata.name || '-'} + {status.phase || '-'} + {getAge(metadata.creationTimestamp)} + + ) + case 'deployment': + case 'statefulset': + return ( + + {metadata.name || '-'} + {metadata.namespace || '-'} + {`${status.readyReplicas || 0}/${spec.replicas || 0}`} + {getAge(metadata.creationTimestamp)} + + ) + case 'service': + return ( + + {metadata.name || '-'} + {metadata.namespace || '-'} + {spec.type || '-'} + {spec.clusterIP || '-'} + {spec.externalIPs?.join(', ') || status.loadBalancer?.ingress?.map((i: any) => i.ip || i.hostname).join(', ') || '-'} + {spec.ports?.map((p: any) => `${p.port}/${p.protocol}`).join(', ') || '-'} + {getAge(metadata.creationTimestamp)} + + ) + case 'configmap': + return ( + + {metadata.name || '-'} + {metadata.namespace || '-'} + {Object.keys(resource.data || {}).length} + {getAge(metadata.creationTimestamp)} + + ) + case 'pod': + return ( + + {metadata.name || '-'} + {metadata.namespace || '-'} + {status.phase || '-'} + {status.podIP || '-'} + {spec.nodeName || '-'} + {getAge(metadata.creationTimestamp)} + + ) + case 'pv': + return ( + + {metadata.name || '-'} + {spec.capacity?.storage || '-'} + {spec.accessModes?.join(', ') || '-'} + {status.phase || '-'} + {spec.claimRef?.name ? `${spec.claimRef.namespace}/${spec.claimRef.name}` : '-'} + {getAge(metadata.creationTimestamp)} + + ) + case 'pvc': + return ( + + {metadata.name || '-'} + {metadata.namespace || '-'} + {status.phase || '-'} + {spec.volumeName || '-'} + {status.capacity?.storage || '-'} + {spec.accessModes?.join(', ') || '-'} + {getAge(metadata.creationTimestamp)} + + ) + default: + return ( + + {metadata.name || '-'} + + ) + } + } + + return ( + + + + {KINDS.map((kind) => ( + + setSelectedKind(kind)} + > + + + + ))} + + + + + + {selectedKind.label} + + setCreateDialogOpen(true)} sx={{ mr: 1 }}> + + + setSettingsOpen(true)}> + + + + + + {selectedKind.key !== 'namespace' && selectedKind.key !== 'pv' && ( + + setNamespace(e.target.value)} + size="small" + sx={{ width: 300 }} + /> + + )} + + {kubeconfigError && ( + + 请先在右侧设置中配置 Kubeconfig + + )} + + {error && {error}} + + {loading && ( + + + + )} + + {!loading && !error && ( + + + + + {getResourceColumns().map((col) => ( + {col} + ))} + + + + {resources.length === 0 && ( + + + 暂无数据 + + + )} + {resources.map((resource) => renderResourceRow(resource))} + +
+
+ )} +
+ + setSettingsOpen(false)} + sx={{ '& .MuiDrawer-paper': { width: 500 } }} + > + + + 集群配置 + setSettingsOpen(false)}> + + + + + + setKubeconfig(e.target.value)} + placeholder="粘贴 kubeconfig 内容..." + fullWidth + /> + + + + + + setCreateDialogOpen(false)} + maxWidth="md" + fullWidth + > + + + 创建资源 + setCreateDialogOpen(false)}> + + + + + + + + + + + setYamlContent(e.target.value)} + placeholder="粘贴或编辑 YAML 内容..." + fullWidth + variant="outlined" + /> + + + + + + + + + setSnackbar({ ...snackbar, open: false })} + anchorOrigin={{ vertical: 'top', horizontal: 'center' }} + > + setSnackbar({ ...snackbar, open: false })}> + {snackbar.message} + + +
+ ) +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 72ae0a3..f75f616 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ port: 3000, proxy: { '/api': { - target: 'http://localhost:9119', + target: 'http://127.0.0.1:9119', changeOrigin: true, // Removed rewrite so /api prefix is preserved for backend route /api/v1/... }, diff --git a/go.mod b/go.mod index 9916b9f..d613c56 100644 --- a/go.mod +++ b/go.mod @@ -10,27 +10,46 @@ require ( github.com/jedib0t/go-pretty/v6 v6.7.1 github.com/spf13/cobra v1.10.1 golang.org/x/crypto v0.43.0 + golang.org/x/net v0.46.0 gorm.io/gorm v1.31.1 + k8s.io/apimachinery v0.34.1 + k8s.io/client-go v0.34.1 + sigs.k8s.io/yaml v1.6.0 ) require ( github.com/andybalholm/brotli v1.2.0 // indirect github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/cli v28.2.2+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.3 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/gofiber/utils/v2 v2.0.0-rc.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gnostic-models v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -42,12 +61,28 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.65.0 // indirect github.com/vbatts/tar-split v0.12.1 // indirect - golang.org/x/net v0.46.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.37.0 // indirect + golang.org/x/term v0.36.0 // indirect golang.org/x/text v0.30.0 // indirect + golang.org/x/time v0.9.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.34.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect modernc.org/libc v1.22.5 // indirect modernc.org/mathutil v1.5.0 // indirect modernc.org/memory v1.5.0 // indirect modernc.org/sqlite v1.23.1 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect ) diff --git a/go.sum b/go.sum index d3bc5a3..c2e611b 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,7 @@ github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUS github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -14,24 +15,43 @@ github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqI github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gofiber/fiber/v3 v3.0.0-beta.2 h1:mVVgt8PTaHGup3NGl/+7U7nEoZaXJ5OComV4E+HpAao= github.com/gofiber/fiber/v3 v3.0.0-beta.2/go.mod h1:w7sdfTY0okjZ1oVH6rSOGvuACUIt0By1iK0HKUb3uqM= github.com/gofiber/utils/v2 v2.0.0-rc.1 h1:b77K5Rk9+Pjdxz4HlwEBnS7u5nikhx7armQB8xPds4s= github.com/gofiber/utils/v2 v2.0.0-rc.1/go.mod h1:Y1g08g7gvST49bbjHJ1AVqcsmg93912R/tbKWhn6V3E= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU= github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y= -github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q= -github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -42,8 +62,23 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -52,6 +87,18 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -66,6 +113,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shamaton/msgpack/v2 v2.2.3 h1:uDOHmxQySlvlUYfQwdjxyybAOzjlQsD1Vjy+4jmO9NM= github.com/shamaton/msgpack/v2 v2.2.3/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI= @@ -76,7 +125,15 @@ github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4 github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -89,23 +146,66 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -113,6 +213,18 @@ gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= +k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= +k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= @@ -121,3 +233,11 @@ modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/api/api.go b/internal/api/api.go index 70b1a72..9bc1a78 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -8,6 +8,7 @@ import ( "gitea.loveuer.com/loveuer/cluster/internal/middleware" "gitea.loveuer.com/loveuer/cluster/internal/model" + "gitea.loveuer.com/loveuer/cluster/internal/module/k8s" "gitea.loveuer.com/loveuer/cluster/internal/module/registry" "gitea.loveuer.com/loveuer/cluster/pkg/store" "github.com/gofiber/fiber/v3" @@ -36,6 +37,11 @@ func Init(ctx context.Context, address string, db *gorm.DB, store store.Store) ( log.Printf("Warning: failed to migrate RegistryConfig: %v", err) } + // Initialize k8s module + if err := k8s.Init(ctx, db, store); err != nil { + log.Printf("Warning: failed to initialize k8s module: %v", err) + } + // oci image apis { app.All("/v2/*", registry.Registry(ctx, db, store)) @@ -53,6 +59,25 @@ func Init(ctx context.Context, address string, db *gorm.DB, store store.Store) ( registryAPI.Post("/config", registry.RegistryConfigSet(ctx, db, store)) } + // k8s cluster apis + { + k8sAPI := app.Group("/api/v1/k8s") + // cluster config + k8sAPI.Get("/config", k8s.ClusterConfigGet(ctx, db, store)) + k8sAPI.Post("/config", k8s.ClusterConfigSet(ctx, db, store)) + // resource operations + k8sAPI.Post("/resource/apply", k8s.K8sResourceApply(ctx, db, store)) + // resource list + k8sAPI.Get("/namespace/list", k8s.K8sNamespaceList(ctx, db, store)) + k8sAPI.Get("/deployment/list", k8s.K8sDeploymentList(ctx, db, store)) + k8sAPI.Get("/statefulset/list", k8s.K8sStatefulSetList(ctx, db, store)) + k8sAPI.Get("/configmap/list", k8s.K8sConfigMapList(ctx, db, store)) + k8sAPI.Get("/pod/list", k8s.K8sPodList(ctx, db, store)) + k8sAPI.Get("/pv/list", k8s.K8sPVList(ctx, db, store)) + k8sAPI.Get("/pvc/list", k8s.K8sPVCList(ctx, db, store)) + k8sAPI.Get("/service/list", k8s.K8sServiceList(ctx, db, store)) + } + ln, err = net.Listen("tcp", address) if err != nil { return fn, fmt.Errorf("failed to listen on %s: %w", address, err) diff --git a/internal/model/model.go b/internal/model/model.go index 29ad4b8..aaab619 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -79,3 +79,14 @@ type RegistryConfig struct { Key string `gorm:"uniqueIndex;not null" json:"key"` // ???? key Value string `gorm:"type:text" json:"value"` // ???? value } + +// ClusterConfig k8s cluster configuration +type ClusterConfig struct { + ID uint `gorm:"primarykey" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + Key string `gorm:"uniqueIndex;not null" json:"key"` + Value string `gorm:"type:text" json:"value"` +} diff --git a/internal/module/k8s/handler.config.go b/internal/module/k8s/handler.config.go new file mode 100644 index 0000000..df7c6cf --- /dev/null +++ b/internal/module/k8s/handler.config.go @@ -0,0 +1,54 @@ +package k8s + +import ( + "context" + "encoding/json" + + "gitea.loveuer.com/loveuer/cluster/internal/model" + "gitea.loveuer.com/loveuer/cluster/pkg/resp" + "gitea.loveuer.com/loveuer/cluster/pkg/store" + "github.com/gofiber/fiber/v3" + "gorm.io/gorm" +) + +func ClusterConfigGet(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler { + return func(c fiber.Ctx) error { + var config model.ClusterConfig + + if err := db.Where("key = ?", "kubeconfig").First(&config).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return resp.R200(c, map[string]any{ + "kubeconfig": "", + }) + } + return resp.R500(c, "", nil, err) + } + + return resp.R200(c, map[string]any{ + "kubeconfig": config.Value, + }) + } +} + +func ClusterConfigSet(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler { + return func(c fiber.Ctx) error { + var req struct { + Kubeconfig string `json:"kubeconfig"` + } + + if err := json.Unmarshal(c.Body(), &req); err != nil { + return resp.R400(c, "", nil, err) + } + + config := model.ClusterConfig{ + Key: "kubeconfig", + Value: req.Kubeconfig, + } + + if err := db.Where("key = ?", "kubeconfig").Assign(config).FirstOrCreate(&config).Error; err != nil { + return resp.R500(c, "", nil, err) + } + + return resp.R200(c, nil) + } +} diff --git a/internal/module/k8s/handler.resource.go b/internal/module/k8s/handler.resource.go new file mode 100644 index 0000000..60de334 --- /dev/null +++ b/internal/module/k8s/handler.resource.go @@ -0,0 +1,301 @@ +package k8s + +import ( + "context" + "encoding/json" + "fmt" + + "gitea.loveuer.com/loveuer/cluster/internal/model" + "gitea.loveuer.com/loveuer/cluster/pkg/resp" + "gitea.loveuer.com/loveuer/cluster/pkg/store" + "github.com/gofiber/fiber/v3" + "gorm.io/gorm" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" + "sigs.k8s.io/yaml" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func getK8sClient(db *gorm.DB) (*kubernetes.Clientset, error) { + var config model.ClusterConfig + + if err := db.Where("key = ?", "kubeconfig").First(&config).Error; err != nil { + return nil, fmt.Errorf("kubeconfig not found: %w", err) + } + + if config.Value == "" { + return nil, fmt.Errorf("kubeconfig is empty") + } + + clientConfig, err := clientcmd.RESTConfigFromKubeConfig([]byte(config.Value)) + if err != nil { + return nil, fmt.Errorf("failed to parse kubeconfig: %w", err) + } + + clientset, err := kubernetes.NewForConfig(clientConfig) + if err != nil { + return nil, fmt.Errorf("failed to create k8s client: %w", err) + } + + return clientset, nil +} + +func K8sNamespaceList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler { + return func(c fiber.Ctx) error { + clientset, err := getK8sClient(db) + if err != nil { + return resp.R500(c, "", nil, err) + } + + namespaces, err := clientset.CoreV1().Namespaces().List(c.Context(), metav1.ListOptions{}) + if err != nil { + return resp.R500(c, "", nil, err) + } + + return resp.R200(c, map[string]any{ + "items": namespaces.Items, + }) + } +} + +func K8sDeploymentList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler { + return func(c fiber.Ctx) error { + namespace := c.Query("namespace", "") + + clientset, err := getK8sClient(db) + if err != nil { + return resp.R500(c, "", nil, err) + } + + deployments, err := clientset.AppsV1().Deployments(namespace).List(c.Context(), metav1.ListOptions{}) + if err != nil { + return resp.R500(c, "", nil, err) + } + + return resp.R200(c, map[string]any{ + "items": deployments.Items, + }) + } +} + +func K8sStatefulSetList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler { + return func(c fiber.Ctx) error { + namespace := c.Query("namespace", "") + + clientset, err := getK8sClient(db) + if err != nil { + return resp.R500(c, "", nil, err) + } + + statefulsets, err := clientset.AppsV1().StatefulSets(namespace).List(c.Context(), metav1.ListOptions{}) + if err != nil { + return resp.R500(c, "", nil, err) + } + + return resp.R200(c, map[string]any{ + "items": statefulsets.Items, + }) + } +} + +func K8sConfigMapList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler { + return func(c fiber.Ctx) error { + namespace := c.Query("namespace", "") + + clientset, err := getK8sClient(db) + if err != nil { + return resp.R500(c, "", nil, err) + } + + configmaps, err := clientset.CoreV1().ConfigMaps(namespace).List(c.Context(), metav1.ListOptions{}) + if err != nil { + return resp.R500(c, "", nil, err) + } + + return resp.R200(c, map[string]any{ + "items": configmaps.Items, + }) + } +} + +func K8sPodList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler { + return func(c fiber.Ctx) error { + namespace := c.Query("namespace", "") + + clientset, err := getK8sClient(db) + if err != nil { + return resp.R500(c, "", nil, err) + } + + pods, err := clientset.CoreV1().Pods(namespace).List(c.Context(), metav1.ListOptions{}) + if err != nil { + return resp.R500(c, "", nil, err) + } + + return resp.R200(c, map[string]any{ + "items": pods.Items, + }) + } +} + +func K8sPVList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler { + return func(c fiber.Ctx) error { + clientset, err := getK8sClient(db) + if err != nil { + return resp.R500(c, "", nil, err) + } + + pvs, err := clientset.CoreV1().PersistentVolumes().List(c.Context(), metav1.ListOptions{}) + if err != nil { + return resp.R500(c, "", nil, err) + } + + return resp.R200(c, map[string]any{ + "items": pvs.Items, + }) + } +} + +func K8sPVCList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler { + return func(c fiber.Ctx) error { + namespace := c.Query("namespace", "") + + clientset, err := getK8sClient(db) + if err != nil { + return resp.R500(c, "", nil, err) + } + + pvcs, err := clientset.CoreV1().PersistentVolumeClaims(namespace).List(c.Context(), metav1.ListOptions{}) + if err != nil { + return resp.R500(c, "", nil, err) + } + + return resp.R200(c, map[string]any{ + "items": pvcs.Items, + }) + } +} + +func K8sServiceList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler { + return func(c fiber.Ctx) error { + namespace := c.Query("namespace", "") + + clientset, err := getK8sClient(db) + if err != nil { + return resp.R500(c, "", nil, err) + } + + services, err := clientset.CoreV1().Services(namespace).List(c.Context(), metav1.ListOptions{}) + if err != nil { + return resp.R500(c, "", nil, err) + } + + return resp.R200(c, map[string]any{ + "items": services.Items, + }) + } +} + +func K8sResourceApply(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler { + return func(c fiber.Ctx) error { + var req struct { + Yaml string `json:"yaml"` + } + + if err := json.Unmarshal(c.Body(), &req); err != nil { + return resp.R400(c, "", nil, err) + } + + if req.Yaml == "" { + return resp.R400(c, "", nil, fmt.Errorf("yaml content is empty")) + } + + var config model.ClusterConfig + if err := db.Where("key = ?", "kubeconfig").First(&config).Error; err != nil { + return resp.R500(c, "", nil, fmt.Errorf("kubeconfig not found: %w", err)) + } + + if config.Value == "" { + return resp.R500(c, "", nil, fmt.Errorf("kubeconfig is empty")) + } + + clientConfig, err := clientcmd.RESTConfigFromKubeConfig([]byte(config.Value)) + if err != nil { + return resp.R500(c, "", nil, fmt.Errorf("failed to parse kubeconfig: %w", err)) + } + + dynamicClient, err := dynamic.NewForConfig(clientConfig) + if err != nil { + return resp.R500(c, "", nil, fmt.Errorf("failed to create dynamic client: %w", err)) + } + + var obj unstructured.Unstructured + if err := yaml.Unmarshal([]byte(req.Yaml), &obj); err != nil { + return resp.R400(c, "", nil, fmt.Errorf("failed to parse yaml: %w", err)) + } + + gvk := obj.GroupVersionKind() + namespace := obj.GetNamespace() + name := obj.GetName() + + if name == "" { + return resp.R400(c, "", nil, fmt.Errorf("resource name is required")) + } + + gvr := schema.GroupVersionResource{ + Group: gvk.Group, + Version: gvk.Version, + Resource: getResourceName(gvk.Kind), + } + + var result *unstructured.Unstructured + if namespace != "" { + result, err = dynamicClient.Resource(gvr).Namespace(namespace).Create(c.Context(), &obj, metav1.CreateOptions{}) + } else { + result, err = dynamicClient.Resource(gvr).Create(c.Context(), &obj, metav1.CreateOptions{}) + } + + if err != nil { + return resp.R500(c, "", nil, fmt.Errorf("failed to apply resource: %w", err)) + } + + return resp.R200(c, map[string]any{ + "name": result.GetName(), + "namespace": result.GetNamespace(), + "kind": result.GetKind(), + }) + } +} + +func getResourceName(kind string) string { + kindToResource := map[string]string{ + "Namespace": "namespaces", + "Deployment": "deployments", + "StatefulSet": "statefulsets", + "Service": "services", + "ConfigMap": "configmaps", + "Pod": "pods", + "PersistentVolume": "persistentvolumes", + "PersistentVolumeClaim": "persistentvolumeclaims", + "Secret": "secrets", + "Ingress": "ingresses", + "DaemonSet": "daemonsets", + "Job": "jobs", + "CronJob": "cronjobs", + "ReplicaSet": "replicasets", + "ServiceAccount": "serviceaccounts", + "Role": "roles", + "RoleBinding": "rolebindings", + "ClusterRole": "clusterroles", + "ClusterRoleBinding": "clusterrolebindings", + } + + if resource, ok := kindToResource[kind]; ok { + return resource + } + + return kind + "s" +} diff --git a/internal/module/k8s/k8s.go b/internal/module/k8s/k8s.go new file mode 100644 index 0000000..e600734 --- /dev/null +++ b/internal/module/k8s/k8s.go @@ -0,0 +1,21 @@ +package k8s + +import ( + "context" + "log" + + "gitea.loveuer.com/loveuer/cluster/internal/model" + "gitea.loveuer.com/loveuer/cluster/pkg/store" + "gorm.io/gorm" +) + +func Init(ctx context.Context, db *gorm.DB, store store.Store) error { + if err := db.AutoMigrate( + &model.ClusterConfig{}, + ); err != nil { + log.Fatalf("failed to migrate k8s database: %v", err) + return err + } + + return nil +}