Compare commits
2 Commits
db28bc0425
...
7d2e2ab842
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d2e2ab842 | ||
|
|
54ed79cea3 |
@@ -26,11 +26,15 @@ import {
|
|||||||
DialogContent,
|
DialogContent,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
Snackbar,
|
Snackbar,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import SettingsIcon from '@mui/icons-material/Settings'
|
import SettingsIcon from '@mui/icons-material/Settings'
|
||||||
import CloseIcon from '@mui/icons-material/Close'
|
import CloseIcon from '@mui/icons-material/Close'
|
||||||
import AddIcon from '@mui/icons-material/Add'
|
import AddIcon from '@mui/icons-material/Add'
|
||||||
import UploadFileIcon from '@mui/icons-material/UploadFile'
|
import UploadFileIcon from '@mui/icons-material/UploadFile'
|
||||||
|
import VisibilityIcon from '@mui/icons-material/Visibility'
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete'
|
||||||
|
|
||||||
const KINDS = [
|
const KINDS = [
|
||||||
{ key: 'namespace', label: 'Namespace', endpoint: '/api/v1/k8s/namespace/list' },
|
{ key: 'namespace', label: 'Namespace', endpoint: '/api/v1/k8s/namespace/list' },
|
||||||
@@ -54,6 +58,8 @@ export default function K8sResourceList() {
|
|||||||
const [kubeconfig, setKubeconfig] = useState('')
|
const [kubeconfig, setKubeconfig] = useState('')
|
||||||
const [kubeconfigError, setKubeconfigError] = useState(false)
|
const [kubeconfigError, setKubeconfigError] = useState(false)
|
||||||
const [namespace, setNamespace] = useState('')
|
const [namespace, setNamespace] = useState('')
|
||||||
|
const [namespaces, setNamespaces] = useState<string[]>([])
|
||||||
|
const [nameFilter, setNameFilter] = useState('')
|
||||||
const [createDialogOpen, setCreateDialogOpen] = useState(false)
|
const [createDialogOpen, setCreateDialogOpen] = useState(false)
|
||||||
const [yamlContent, setYamlContent] = useState('')
|
const [yamlContent, setYamlContent] = useState('')
|
||||||
const [applyLoading, setApplyLoading] = useState(false)
|
const [applyLoading, setApplyLoading] = useState(false)
|
||||||
@@ -63,6 +69,13 @@ export default function K8sResourceList() {
|
|||||||
severity: 'success',
|
severity: 'success',
|
||||||
})
|
})
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const [logsDialogOpen, setLogsDialogOpen] = useState(false)
|
||||||
|
const [logs, setLogs] = useState<string[]>([])
|
||||||
|
const [selectedPod, setSelectedPod] = useState<{ name: string; namespace: string } | null>(null)
|
||||||
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
||||||
|
const [deleteTarget, setDeleteTarget] = useState<{ name: string; namespace: string } | null>(null)
|
||||||
|
const [deleting, setDeleting] = useState(false)
|
||||||
|
const logsEndRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchKubeconfig()
|
fetchKubeconfig()
|
||||||
@@ -71,8 +84,11 @@ export default function K8sResourceList() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (kubeconfig) {
|
if (kubeconfig) {
|
||||||
fetchResources()
|
fetchResources()
|
||||||
|
if (selectedKind.key !== 'namespace' && selectedKind.key !== 'pv') {
|
||||||
|
fetchNamespaces()
|
||||||
}
|
}
|
||||||
}, [selectedKind, namespace])
|
}
|
||||||
|
}, [selectedKind, namespace, nameFilter])
|
||||||
|
|
||||||
const fetchKubeconfig = async () => {
|
const fetchKubeconfig = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -146,6 +162,60 @@ export default function K8sResourceList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleViewLogs = (podName: string, podNamespace: string) => {
|
||||||
|
setSelectedPod({ name: podName, namespace: podNamespace })
|
||||||
|
setLogs([])
|
||||||
|
setLogsDialogOpen(true)
|
||||||
|
|
||||||
|
const eventSource = new EventSource(
|
||||||
|
`/api/v1/k8s/pod/logs?name=${encodeURIComponent(podName)}&namespace=${encodeURIComponent(podNamespace)}&tail=1000&follow=true`
|
||||||
|
)
|
||||||
|
|
||||||
|
eventSource.onmessage = (event) => {
|
||||||
|
setLogs((prev) => [...prev, event.data])
|
||||||
|
setTimeout(() => logsEndRef.current?.scrollIntoView({ behavior: 'smooth' }), 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
eventSource.onerror = () => {
|
||||||
|
eventSource.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => eventSource.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeletePod = async () => {
|
||||||
|
if (!deleteTarget) return
|
||||||
|
|
||||||
|
setDeleting(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/v1/k8s/pod/delete', {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name: deleteTarget.name, namespace: deleteTarget.namespace }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await res.json()
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(result.err || 'Failed to delete pod')
|
||||||
|
}
|
||||||
|
|
||||||
|
setSnackbar({ open: true, message: 'Pod 删除成功', severity: 'success' })
|
||||||
|
setDeleteDialogOpen(false)
|
||||||
|
setDeleteTarget(null)
|
||||||
|
fetchResources()
|
||||||
|
} catch (e: any) {
|
||||||
|
setSnackbar({ open: true, message: `删除失败: ${e.message}`, severity: 'error' })
|
||||||
|
} finally {
|
||||||
|
setDeleting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openDeleteDialog = (podName: string, podNamespace: string) => {
|
||||||
|
setDeleteTarget({ name: podName, namespace: podNamespace })
|
||||||
|
setDeleteDialogOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
const fetchResources = async () => {
|
const fetchResources = async () => {
|
||||||
if (!kubeconfig) {
|
if (!kubeconfig) {
|
||||||
setKubeconfigError(true)
|
setKubeconfigError(true)
|
||||||
@@ -159,6 +229,9 @@ export default function K8sResourceList() {
|
|||||||
if (namespace && selectedKind.key !== 'namespace' && selectedKind.key !== 'pv') {
|
if (namespace && selectedKind.key !== 'namespace' && selectedKind.key !== 'pv') {
|
||||||
url.searchParams.set('namespace', namespace)
|
url.searchParams.set('namespace', namespace)
|
||||||
}
|
}
|
||||||
|
if (nameFilter) {
|
||||||
|
url.searchParams.set('name', nameFilter)
|
||||||
|
}
|
||||||
|
|
||||||
const res = await fetch(url.toString())
|
const res = await fetch(url.toString())
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
@@ -171,6 +244,18 @@ export default function K8sResourceList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchNamespaces = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/v1/k8s/namespace/list')
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
|
const result = await res.json()
|
||||||
|
const namespaceList = result.data?.items?.map((ns: any) => ns.metadata.name) || []
|
||||||
|
setNamespaces(namespaceList)
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('Failed to fetch namespaces:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getResourceColumns = () => {
|
const getResourceColumns = () => {
|
||||||
switch (selectedKind.key) {
|
switch (selectedKind.key) {
|
||||||
case 'namespace':
|
case 'namespace':
|
||||||
@@ -183,7 +268,7 @@ export default function K8sResourceList() {
|
|||||||
case 'configmap':
|
case 'configmap':
|
||||||
return ['Name', 'Namespace', 'Data Keys', 'Age']
|
return ['Name', 'Namespace', 'Data Keys', 'Age']
|
||||||
case 'pod':
|
case 'pod':
|
||||||
return ['Name', 'Namespace', 'Status', 'IP', 'Node', 'Age']
|
return ['Name', 'Namespace', 'Status', 'IP', 'Node', 'Age', 'Actions']
|
||||||
case 'pv':
|
case 'pv':
|
||||||
return ['Name', 'Capacity', 'Access Modes', 'Status', 'Claim', 'Age']
|
return ['Name', 'Capacity', 'Access Modes', 'Status', 'Claim', 'Age']
|
||||||
case 'pvc':
|
case 'pvc':
|
||||||
@@ -256,6 +341,23 @@ export default function K8sResourceList() {
|
|||||||
<TableCell>{status.podIP || '-'}</TableCell>
|
<TableCell>{status.podIP || '-'}</TableCell>
|
||||||
<TableCell>{spec.nodeName || '-'}</TableCell>
|
<TableCell>{spec.nodeName || '-'}</TableCell>
|
||||||
<TableCell>{getAge(metadata.creationTimestamp)}</TableCell>
|
<TableCell>{getAge(metadata.creationTimestamp)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => handleViewLogs(metadata.name, metadata.namespace)}
|
||||||
|
title="查看日志"
|
||||||
|
>
|
||||||
|
<VisibilityIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
onClick={() => openDeleteDialog(metadata.name, metadata.namespace)}
|
||||||
|
title="删除"
|
||||||
|
>
|
||||||
|
<DeleteIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)
|
)
|
||||||
case 'pv':
|
case 'pv':
|
||||||
@@ -332,13 +434,35 @@ export default function K8sResourceList() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{selectedKind.key !== 'namespace' && selectedKind.key !== 'pv' && (
|
{selectedKind.key !== 'namespace' && selectedKind.key !== 'pv' && (
|
||||||
<Box sx={{ mb: 2 }}>
|
<Box sx={{ mb: 2, display: 'flex', gap: 2 }}>
|
||||||
<TextField
|
<TextField
|
||||||
placeholder="Namespace (空则查询所有)"
|
select
|
||||||
|
label="Namespace"
|
||||||
value={namespace}
|
value={namespace}
|
||||||
onChange={(e) => setNamespace(e.target.value)}
|
onChange={(e) => setNamespace(e.target.value)}
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ width: 300 }}
|
sx={{ width: 200 }}
|
||||||
|
SelectProps={{
|
||||||
|
displayEmpty: true,
|
||||||
|
}}
|
||||||
|
InputLabelProps={{ shrink: true }}
|
||||||
|
>
|
||||||
|
<MenuItem value="">
|
||||||
|
<em>所有命名空间</em>
|
||||||
|
</MenuItem>
|
||||||
|
{namespaces.map((ns) => (
|
||||||
|
<MenuItem key={ns} value={ns}>
|
||||||
|
{ns}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
label="名称过滤"
|
||||||
|
placeholder="按名称过滤"
|
||||||
|
value={nameFilter}
|
||||||
|
onChange={(e) => setNameFilter(e.target.value)}
|
||||||
|
size="small"
|
||||||
|
sx={{ width: 200 }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
@@ -469,6 +593,65 @@ export default function K8sResourceList() {
|
|||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={logsDialogOpen}
|
||||||
|
onClose={() => setLogsDialogOpen(false)}
|
||||||
|
maxWidth="lg"
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<DialogTitle>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Typography variant="h6">
|
||||||
|
Pod 日志: {selectedPod?.name} ({selectedPod?.namespace})
|
||||||
|
</Typography>
|
||||||
|
<IconButton onClick={() => setLogsDialogOpen(false)}>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Paper
|
||||||
|
sx={{
|
||||||
|
p: 2,
|
||||||
|
bgcolor: '#1e1e1e',
|
||||||
|
color: '#d4d4d4',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
maxHeight: '60vh',
|
||||||
|
overflow: 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{logs.length === 0 && <Typography>等待日志...</Typography>}
|
||||||
|
{logs.map((log, index) => (
|
||||||
|
<Box key={index} sx={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
|
||||||
|
{log}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
<div ref={logsEndRef} />
|
||||||
|
</Paper>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={deleteDialogOpen} onClose={() => setDeleteDialogOpen(false)}>
|
||||||
|
<DialogTitle>确认删除</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Typography>
|
||||||
|
确定要删除 Pod <strong>{deleteTarget?.name}</strong> (namespace: {deleteTarget?.namespace}) 吗?
|
||||||
|
</Typography>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setDeleteDialogOpen(false)}>取消</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="error"
|
||||||
|
onClick={handleDeletePod}
|
||||||
|
disabled={deleting}
|
||||||
|
>
|
||||||
|
{deleting ? <CircularProgress size={24} /> : '删除'}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<Snackbar
|
<Snackbar
|
||||||
open={snackbar.open}
|
open={snackbar.open}
|
||||||
autoHideDuration={6000}
|
autoHideDuration={6000}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export default defineConfig({
|
|||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://127.0.0.1:9119',
|
target: 'http://127.0.0.1:9119',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
|
ws: true,
|
||||||
|
timeout: 0,
|
||||||
// Removed rewrite so /api prefix is preserved for backend route /api/v1/...
|
// Removed rewrite so /api prefix is preserved for backend route /api/v1/...
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ func Init(ctx context.Context, address string, db *gorm.DB, store store.Store) (
|
|||||||
k8sAPI.Get("/statefulset/list", k8s.K8sStatefulSetList(ctx, db, store))
|
k8sAPI.Get("/statefulset/list", k8s.K8sStatefulSetList(ctx, db, store))
|
||||||
k8sAPI.Get("/configmap/list", k8s.K8sConfigMapList(ctx, db, store))
|
k8sAPI.Get("/configmap/list", k8s.K8sConfigMapList(ctx, db, store))
|
||||||
k8sAPI.Get("/pod/list", k8s.K8sPodList(ctx, db, store))
|
k8sAPI.Get("/pod/list", k8s.K8sPodList(ctx, db, store))
|
||||||
|
k8sAPI.Get("/pod/logs", k8s.K8sPodLogs(ctx, db, store))
|
||||||
|
k8sAPI.Delete("/pod/delete", k8s.K8sPodDelete(ctx, db, store))
|
||||||
k8sAPI.Get("/pv/list", k8s.K8sPVList(ctx, db, store))
|
k8sAPI.Get("/pv/list", k8s.K8sPVList(ctx, db, store))
|
||||||
k8sAPI.Get("/pvc/list", k8s.K8sPVCList(ctx, db, store))
|
k8sAPI.Get("/pvc/list", k8s.K8sPVCList(ctx, db, store))
|
||||||
k8sAPI.Get("/service/list", k8s.K8sServiceList(ctx, db, store))
|
k8sAPI.Get("/service/list", k8s.K8sServiceList(ctx, db, store))
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
package k8s
|
package k8s
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gitea.loveuer.com/loveuer/cluster/internal/model"
|
"gitea.loveuer.com/loveuer/cluster/internal/model"
|
||||||
"gitea.loveuer.com/loveuer/cluster/pkg/resp"
|
"gitea.loveuer.com/loveuer/cluster/pkg/resp"
|
||||||
"gitea.loveuer.com/loveuer/cluster/pkg/store"
|
"gitea.loveuer.com/loveuer/cluster/pkg/store"
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/client-go/dynamic"
|
"k8s.io/client-go/dynamic"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
"k8s.io/client-go/tools/clientcmd"
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
"sigs.k8s.io/yaml"
|
"sigs.k8s.io/yaml"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
@@ -35,6 +41,8 @@ func getK8sClient(db *gorm.DB) (*kubernetes.Clientset, error) {
|
|||||||
return nil, fmt.Errorf("failed to parse kubeconfig: %w", err)
|
return nil, fmt.Errorf("failed to parse kubeconfig: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clientConfig.Timeout = 0
|
||||||
|
|
||||||
clientset, err := kubernetes.NewForConfig(clientConfig)
|
clientset, err := kubernetes.NewForConfig(clientConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create k8s client: %w", err)
|
return nil, fmt.Errorf("failed to create k8s client: %w", err)
|
||||||
@@ -43,6 +51,29 @@ func getK8sClient(db *gorm.DB) (*kubernetes.Clientset, error) {
|
|||||||
return clientset, nil
|
return clientset, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getK8sConfig(db *gorm.DB) (*rest.Config, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable HTTP/2 to avoid stream closing issues
|
||||||
|
clientConfig.TLSClientConfig.NextProtos = []string{"http/1.1"}
|
||||||
|
clientConfig.Timeout = 0
|
||||||
|
|
||||||
|
return clientConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
func K8sNamespaceList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler {
|
func K8sNamespaceList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler {
|
||||||
return func(c fiber.Ctx) error {
|
return func(c fiber.Ctx) error {
|
||||||
clientset, err := getK8sClient(db)
|
clientset, err := getK8sClient(db)
|
||||||
@@ -64,6 +95,7 @@ func K8sNamespaceList(ctx context.Context, db *gorm.DB, store store.Store) fiber
|
|||||||
func K8sDeploymentList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler {
|
func K8sDeploymentList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler {
|
||||||
return func(c fiber.Ctx) error {
|
return func(c fiber.Ctx) error {
|
||||||
namespace := c.Query("namespace", "")
|
namespace := c.Query("namespace", "")
|
||||||
|
name := c.Query("name", "")
|
||||||
|
|
||||||
clientset, err := getK8sClient(db)
|
clientset, err := getK8sClient(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -75,8 +107,20 @@ func K8sDeploymentList(ctx context.Context, db *gorm.DB, store store.Store) fibe
|
|||||||
return resp.R500(c, "", nil, err)
|
return resp.R500(c, "", nil, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter by name if provided
|
||||||
|
var filtered []appsv1.Deployment
|
||||||
|
if name != "" {
|
||||||
|
for _, deployment := range deployments.Items {
|
||||||
|
if strings.Contains(deployment.Name, name) {
|
||||||
|
filtered = append(filtered, deployment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filtered = deployments.Items
|
||||||
|
}
|
||||||
|
|
||||||
return resp.R200(c, map[string]any{
|
return resp.R200(c, map[string]any{
|
||||||
"items": deployments.Items,
|
"items": filtered,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,6 +128,7 @@ func K8sDeploymentList(ctx context.Context, db *gorm.DB, store store.Store) fibe
|
|||||||
func K8sStatefulSetList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler {
|
func K8sStatefulSetList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler {
|
||||||
return func(c fiber.Ctx) error {
|
return func(c fiber.Ctx) error {
|
||||||
namespace := c.Query("namespace", "")
|
namespace := c.Query("namespace", "")
|
||||||
|
name := c.Query("name", "")
|
||||||
|
|
||||||
clientset, err := getK8sClient(db)
|
clientset, err := getK8sClient(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -95,8 +140,20 @@ func K8sStatefulSetList(ctx context.Context, db *gorm.DB, store store.Store) fib
|
|||||||
return resp.R500(c, "", nil, err)
|
return resp.R500(c, "", nil, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter by name if provided
|
||||||
|
var filtered []appsv1.StatefulSet
|
||||||
|
if name != "" {
|
||||||
|
for _, statefulset := range statefulsets.Items {
|
||||||
|
if strings.Contains(statefulset.Name, name) {
|
||||||
|
filtered = append(filtered, statefulset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filtered = statefulsets.Items
|
||||||
|
}
|
||||||
|
|
||||||
return resp.R200(c, map[string]any{
|
return resp.R200(c, map[string]any{
|
||||||
"items": statefulsets.Items,
|
"items": filtered,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,6 +161,7 @@ func K8sStatefulSetList(ctx context.Context, db *gorm.DB, store store.Store) fib
|
|||||||
func K8sConfigMapList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler {
|
func K8sConfigMapList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler {
|
||||||
return func(c fiber.Ctx) error {
|
return func(c fiber.Ctx) error {
|
||||||
namespace := c.Query("namespace", "")
|
namespace := c.Query("namespace", "")
|
||||||
|
name := c.Query("name", "")
|
||||||
|
|
||||||
clientset, err := getK8sClient(db)
|
clientset, err := getK8sClient(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -115,8 +173,20 @@ func K8sConfigMapList(ctx context.Context, db *gorm.DB, store store.Store) fiber
|
|||||||
return resp.R500(c, "", nil, err)
|
return resp.R500(c, "", nil, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter by name if provided
|
||||||
|
var filtered []corev1.ConfigMap
|
||||||
|
if name != "" {
|
||||||
|
for _, configmap := range configmaps.Items {
|
||||||
|
if strings.Contains(configmap.Name, name) {
|
||||||
|
filtered = append(filtered, configmap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filtered = configmaps.Items
|
||||||
|
}
|
||||||
|
|
||||||
return resp.R200(c, map[string]any{
|
return resp.R200(c, map[string]any{
|
||||||
"items": configmaps.Items,
|
"items": filtered,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,6 +194,7 @@ func K8sConfigMapList(ctx context.Context, db *gorm.DB, store store.Store) fiber
|
|||||||
func K8sPodList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler {
|
func K8sPodList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler {
|
||||||
return func(c fiber.Ctx) error {
|
return func(c fiber.Ctx) error {
|
||||||
namespace := c.Query("namespace", "")
|
namespace := c.Query("namespace", "")
|
||||||
|
name := c.Query("name", "")
|
||||||
|
|
||||||
clientset, err := getK8sClient(db)
|
clientset, err := getK8sClient(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -135,8 +206,20 @@ func K8sPodList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handl
|
|||||||
return resp.R500(c, "", nil, err)
|
return resp.R500(c, "", nil, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter by name if provided
|
||||||
|
var filtered []corev1.Pod
|
||||||
|
if name != "" {
|
||||||
|
for _, pod := range pods.Items {
|
||||||
|
if strings.Contains(pod.Name, name) {
|
||||||
|
filtered = append(filtered, pod)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filtered = pods.Items
|
||||||
|
}
|
||||||
|
|
||||||
return resp.R200(c, map[string]any{
|
return resp.R200(c, map[string]any{
|
||||||
"items": pods.Items,
|
"items": filtered,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,6 +245,7 @@ func K8sPVList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handle
|
|||||||
func K8sPVCList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler {
|
func K8sPVCList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler {
|
||||||
return func(c fiber.Ctx) error {
|
return func(c fiber.Ctx) error {
|
||||||
namespace := c.Query("namespace", "")
|
namespace := c.Query("namespace", "")
|
||||||
|
name := c.Query("name", "")
|
||||||
|
|
||||||
clientset, err := getK8sClient(db)
|
clientset, err := getK8sClient(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -173,8 +257,20 @@ func K8sPVCList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handl
|
|||||||
return resp.R500(c, "", nil, err)
|
return resp.R500(c, "", nil, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter by name if provided
|
||||||
|
var filtered []corev1.PersistentVolumeClaim
|
||||||
|
if name != "" {
|
||||||
|
for _, pvc := range pvcs.Items {
|
||||||
|
if strings.Contains(pvc.Name, name) {
|
||||||
|
filtered = append(filtered, pvc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filtered = pvcs.Items
|
||||||
|
}
|
||||||
|
|
||||||
return resp.R200(c, map[string]any{
|
return resp.R200(c, map[string]any{
|
||||||
"items": pvcs.Items,
|
"items": filtered,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,6 +278,7 @@ func K8sPVCList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handl
|
|||||||
func K8sServiceList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler {
|
func K8sServiceList(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler {
|
||||||
return func(c fiber.Ctx) error {
|
return func(c fiber.Ctx) error {
|
||||||
namespace := c.Query("namespace", "")
|
namespace := c.Query("namespace", "")
|
||||||
|
name := c.Query("name", "")
|
||||||
|
|
||||||
clientset, err := getK8sClient(db)
|
clientset, err := getK8sClient(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -193,8 +290,20 @@ func K8sServiceList(ctx context.Context, db *gorm.DB, store store.Store) fiber.H
|
|||||||
return resp.R500(c, "", nil, err)
|
return resp.R500(c, "", nil, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter by name if provided
|
||||||
|
var filtered []corev1.Service
|
||||||
|
if name != "" {
|
||||||
|
for _, service := range services.Items {
|
||||||
|
if strings.Contains(service.Name, name) {
|
||||||
|
filtered = append(filtered, service)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filtered = services.Items
|
||||||
|
}
|
||||||
|
|
||||||
return resp.R200(c, map[string]any{
|
return resp.R200(c, map[string]any{
|
||||||
"items": services.Items,
|
"items": filtered,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -299,3 +408,105 @@ func getResourceName(kind string) string {
|
|||||||
|
|
||||||
return kind + "s"
|
return kind + "s"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func K8sPodLogs(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler {
|
||||||
|
return func(c fiber.Ctx) error {
|
||||||
|
podName := c.Query("name", "")
|
||||||
|
namespace := c.Query("namespace", "")
|
||||||
|
tailLines := int64(1000)
|
||||||
|
follow := c.Query("follow", "") == "true"
|
||||||
|
|
||||||
|
if podName == "" || namespace == "" {
|
||||||
|
return resp.R400(c, "", nil, fmt.Errorf("name and namespace are required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
restConfig, err := getK8sConfig(db)
|
||||||
|
if err != nil {
|
||||||
|
return resp.R500(c, "", nil, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
clientset, err := kubernetes.NewForConfig(restConfig)
|
||||||
|
if err != nil {
|
||||||
|
return resp.R500(c, "", nil, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
podLogOpts := &corev1.PodLogOptions{
|
||||||
|
TailLines: &tailLines,
|
||||||
|
Follow: follow,
|
||||||
|
}
|
||||||
|
|
||||||
|
req := clientset.CoreV1().Pods(namespace).GetLogs(podName, podLogOpts)
|
||||||
|
|
||||||
|
logCtx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
stream, err := req.Stream(logCtx)
|
||||||
|
if err != nil {
|
||||||
|
return resp.R500(c, "", nil, fmt.Errorf("failed to get pod logs: %w", err))
|
||||||
|
}
|
||||||
|
defer stream.Close()
|
||||||
|
|
||||||
|
// Use the existing SSE manager from resp package
|
||||||
|
manager := resp.SSE(c, "pod-logs")
|
||||||
|
|
||||||
|
// Start streaming logs in a goroutine
|
||||||
|
go func() {
|
||||||
|
defer manager.Close()
|
||||||
|
|
||||||
|
reader := bufio.NewReader(stream)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-logCtx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
line, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
manager.Send("[EOF]")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
manager.Send(fmt.Sprintf("error: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
manager.Send(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Return nil since we're handling the response directly
|
||||||
|
c.Context().SetBodyStreamWriter(manager.Writer())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func K8sPodDelete(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler {
|
||||||
|
return func(c fiber.Ctx) error {
|
||||||
|
var req struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(c.Body(), &req); err != nil {
|
||||||
|
return resp.R400(c, "", nil, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Name == "" || req.Namespace == "" {
|
||||||
|
return resp.R400(c, "", nil, fmt.Errorf("name and namespace are required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
clientset, err := getK8sClient(db)
|
||||||
|
if err != nil {
|
||||||
|
return resp.R500(c, "", nil, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = clientset.CoreV1().Pods(req.Namespace).Delete(c.Context(), req.Name, metav1.DeleteOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return resp.R500(c, "", nil, fmt.Errorf("failed to delete pod: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.R200(c, map[string]any{
|
||||||
|
"name": req.Name,
|
||||||
|
"namespace": req.Namespace,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user