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
+ />
+
+
+
+
+
+
+
+ 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
+}