Compare commits
13 Commits
7d2e2ab842
...
7a666303be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a666303be | ||
|
|
0536ce9755 | ||
|
|
8ffb0eec09 | ||
|
|
e82dfec1ba | ||
|
|
b7a5f85da2 | ||
|
|
488c7b90bf | ||
|
|
a632f68c29 | ||
|
|
c2bde4a0ff | ||
|
|
23add6447d | ||
|
|
fa0298b4d8 | ||
|
|
40c10235f6 | ||
|
|
08be388322 | ||
|
|
529a90b80d |
@@ -28,6 +28,9 @@ import {
|
|||||||
Snackbar,
|
Snackbar,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Select,
|
Select,
|
||||||
|
AppBar,
|
||||||
|
Toolbar,
|
||||||
|
Tooltip,
|
||||||
} 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'
|
||||||
@@ -35,6 +38,7 @@ 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 VisibilityIcon from '@mui/icons-material/Visibility'
|
||||||
import DeleteIcon from '@mui/icons-material/Delete'
|
import DeleteIcon from '@mui/icons-material/Delete'
|
||||||
|
import EditIcon from '@mui/icons-material/Edit'
|
||||||
|
|
||||||
const KINDS = [
|
const KINDS = [
|
||||||
{ key: 'namespace', label: 'Namespace', endpoint: '/api/v1/k8s/namespace/list' },
|
{ key: 'namespace', label: 'Namespace', endpoint: '/api/v1/k8s/namespace/list' },
|
||||||
@@ -75,6 +79,10 @@ export default function K8sResourceList() {
|
|||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
||||||
const [deleteTarget, setDeleteTarget] = useState<{ name: string; namespace: string } | null>(null)
|
const [deleteTarget, setDeleteTarget] = useState<{ name: string; namespace: string } | null>(null)
|
||||||
const [deleting, setDeleting] = useState(false)
|
const [deleting, setDeleting] = useState(false)
|
||||||
|
const [editDialogOpen, setEditDialogOpen] = useState(false)
|
||||||
|
const [editResource, setEditResource] = useState<{ name: string; namespace: string; kind: string; yaml: string } | null>(null)
|
||||||
|
const [editYaml, setEditYaml] = useState('')
|
||||||
|
const [editing, setEditing] = useState(false)
|
||||||
const logsEndRef = useRef<HTMLDivElement>(null)
|
const logsEndRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -183,6 +191,81 @@ export default function K8sResourceList() {
|
|||||||
return () => eventSource.close()
|
return () => eventSource.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleDeleteResource = async () => {
|
||||||
|
if (!deleteTarget) return
|
||||||
|
|
||||||
|
setDeleting(true)
|
||||||
|
try {
|
||||||
|
let endpoint = '';
|
||||||
|
let kind = '';
|
||||||
|
let requestBody = {
|
||||||
|
name: deleteTarget.name,
|
||||||
|
namespace: deleteTarget.namespace
|
||||||
|
};
|
||||||
|
|
||||||
|
// Determine the correct endpoint based on the selected resource kind
|
||||||
|
switch (selectedKind.key) {
|
||||||
|
case 'pod':
|
||||||
|
endpoint = '/api/v1/k8s/pod/delete'
|
||||||
|
kind = 'Pod'
|
||||||
|
break
|
||||||
|
case 'deployment':
|
||||||
|
endpoint = '/api/v1/k8s/deployment/delete'
|
||||||
|
kind = 'Deployment'
|
||||||
|
break
|
||||||
|
case 'statefulset':
|
||||||
|
endpoint = '/api/v1/k8s/statefulset/delete'
|
||||||
|
kind = 'StatefulSet'
|
||||||
|
break
|
||||||
|
case 'service':
|
||||||
|
endpoint = '/api/v1/k8s/service/delete'
|
||||||
|
kind = 'Service'
|
||||||
|
break
|
||||||
|
case 'configmap':
|
||||||
|
endpoint = '/api/v1/k8s/configmap/delete'
|
||||||
|
kind = 'ConfigMap'
|
||||||
|
break
|
||||||
|
case 'namespace':
|
||||||
|
endpoint = '/api/v1/k8s/namespace/delete'
|
||||||
|
kind = 'Namespace'
|
||||||
|
// Namespace doesn't need namespace field
|
||||||
|
requestBody = { name: deleteTarget.name }
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported resource kind: ${selectedKind.key}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(endpoint, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(requestBody),
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await res.json()
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(result.err || `Failed to delete ${kind}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
setSnackbar({
|
||||||
|
open: true,
|
||||||
|
message: `${kind} 删除成功`,
|
||||||
|
severity: 'success'
|
||||||
|
})
|
||||||
|
setDeleteDialogOpen(false)
|
||||||
|
setDeleteTarget(null)
|
||||||
|
fetchResources()
|
||||||
|
} catch (e: any) {
|
||||||
|
setSnackbar({
|
||||||
|
open: true,
|
||||||
|
message: `删除失败: ${e.message}`,
|
||||||
|
severity: 'error'
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setDeleting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleDeletePod = async () => {
|
const handleDeletePod = async () => {
|
||||||
if (!deleteTarget) return
|
if (!deleteTarget) return
|
||||||
|
|
||||||
@@ -211,6 +294,52 @@ export default function K8sResourceList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleEditResource = async (name: string, namespace: string, kind: string) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/v1/k8s/resource/get?name=${encodeURIComponent(name)}&namespace=${encodeURIComponent(namespace)}&kind=${encodeURIComponent(kind)}`)
|
||||||
|
const result = await res.json()
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(result.err || 'Failed to get resource')
|
||||||
|
}
|
||||||
|
|
||||||
|
setEditResource({ name, namespace, kind, yaml: result.data.yaml })
|
||||||
|
setEditYaml(result.data.yaml)
|
||||||
|
setEditDialogOpen(true)
|
||||||
|
} catch (e: any) {
|
||||||
|
setSnackbar({ open: true, message: `获取资源失败: ${e.message}`, severity: 'error' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleApplyEdit = async () => {
|
||||||
|
if (!editResource) return
|
||||||
|
|
||||||
|
setEditing(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/v1/k8s/resource/update', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ yaml: editYaml }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await res.json()
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(result.err || 'Failed to update resource')
|
||||||
|
}
|
||||||
|
|
||||||
|
setSnackbar({ open: true, message: '资源更新成功', severity: 'success' })
|
||||||
|
setEditDialogOpen(false)
|
||||||
|
setEditResource(null)
|
||||||
|
setEditYaml('')
|
||||||
|
fetchResources()
|
||||||
|
} catch (e: any) {
|
||||||
|
setSnackbar({ open: true, message: `更新失败: ${e.message}`, severity: 'error' })
|
||||||
|
} finally {
|
||||||
|
setEditing(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const openDeleteDialog = (podName: string, podNamespace: string) => {
|
const openDeleteDialog = (podName: string, podNamespace: string) => {
|
||||||
setDeleteTarget({ name: podName, namespace: podNamespace })
|
setDeleteTarget({ name: podName, namespace: podNamespace })
|
||||||
setDeleteDialogOpen(true)
|
setDeleteDialogOpen(true)
|
||||||
@@ -259,22 +388,22 @@ export default function K8sResourceList() {
|
|||||||
const getResourceColumns = () => {
|
const getResourceColumns = () => {
|
||||||
switch (selectedKind.key) {
|
switch (selectedKind.key) {
|
||||||
case 'namespace':
|
case 'namespace':
|
||||||
return ['Name', 'Status', 'Age']
|
return ['Name', 'Status', 'Age', 'Actions']
|
||||||
case 'deployment':
|
case 'deployment':
|
||||||
case 'statefulset':
|
case 'statefulset':
|
||||||
return ['Name', 'Namespace', 'Replicas', 'Age']
|
return ['Name', 'Namespace', 'Replicas', 'Age', 'Actions']
|
||||||
case 'service':
|
case 'service':
|
||||||
return ['Name', 'Namespace', 'Type', 'Cluster IP', 'External IP', 'Ports', 'Age']
|
return ['Name', 'Namespace', 'Type', 'Cluster IP', 'Ports', 'NodePort', 'Age', 'Actions']
|
||||||
case 'configmap':
|
case 'configmap':
|
||||||
return ['Name', 'Namespace', 'Data Keys', 'Age']
|
return ['Name', 'Namespace', 'Data Keys', 'Age', 'Actions']
|
||||||
case 'pod':
|
case 'pod':
|
||||||
return ['Name', 'Namespace', 'Status', 'IP', 'Node', 'Age', 'Actions']
|
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', 'Actions']
|
||||||
case 'pvc':
|
case 'pvc':
|
||||||
return ['Name', 'Namespace', 'Status', 'Volume', 'Capacity', 'Access Modes', 'Age']
|
return ['Name', 'Namespace', 'Status', 'Volume', 'Capacity', 'Access Modes', 'Age', 'Actions']
|
||||||
default:
|
default:
|
||||||
return ['Name']
|
return ['Name', 'Actions']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,6 +428,17 @@ export default function K8sResourceList() {
|
|||||||
<TableCell>{metadata.name || '-'}</TableCell>
|
<TableCell>{metadata.name || '-'}</TableCell>
|
||||||
<TableCell>{status.phase || '-'}</TableCell>
|
<TableCell>{status.phase || '-'}</TableCell>
|
||||||
<TableCell>{getAge(metadata.creationTimestamp)}</TableCell>
|
<TableCell>{getAge(metadata.creationTimestamp)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Tooltip title="删除">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
onClick={() => openDeleteDialog(metadata.name, metadata.namespace)}
|
||||||
|
>
|
||||||
|
<DeleteIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)
|
)
|
||||||
case 'deployment':
|
case 'deployment':
|
||||||
@@ -309,18 +449,67 @@ export default function K8sResourceList() {
|
|||||||
<TableCell>{metadata.namespace || '-'}</TableCell>
|
<TableCell>{metadata.namespace || '-'}</TableCell>
|
||||||
<TableCell>{`${status.readyReplicas || 0}/${spec.replicas || 0}`}</TableCell>
|
<TableCell>{`${status.readyReplicas || 0}/${spec.replicas || 0}`}</TableCell>
|
||||||
<TableCell>{getAge(metadata.creationTimestamp)}</TableCell>
|
<TableCell>{getAge(metadata.creationTimestamp)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Tooltip title="编辑">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => handleEditResource(metadata.name, metadata.namespace, selectedKind.label)}
|
||||||
|
>
|
||||||
|
<EditIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="删除">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
onClick={() => openDeleteDialog(metadata.name, metadata.namespace)}
|
||||||
|
>
|
||||||
|
<DeleteIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)
|
)
|
||||||
case 'service':
|
case 'service':
|
||||||
|
// Format ports display
|
||||||
|
let portsDisplay = '-';
|
||||||
|
let nodePortsDisplay = '-';
|
||||||
|
if (spec.ports && spec.ports.length > 0) {
|
||||||
|
portsDisplay = spec.ports.map((p: any) => `${p.port}/${p.protocol}`).join(', ');
|
||||||
|
const nodePorts = spec.ports.filter((p: any) => p.nodePort).map((p: any) => p.nodePort);
|
||||||
|
if (nodePorts.length > 0) {
|
||||||
|
nodePortsDisplay = nodePorts.join(', ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={metadata.uid}>
|
<TableRow key={metadata.uid}>
|
||||||
<TableCell>{metadata.name || '-'}</TableCell>
|
<TableCell>{metadata.name || '-'}</TableCell>
|
||||||
<TableCell>{metadata.namespace || '-'}</TableCell>
|
<TableCell>{metadata.namespace || '-'}</TableCell>
|
||||||
<TableCell>{spec.type || '-'}</TableCell>
|
<TableCell>{spec.type || '-'}</TableCell>
|
||||||
<TableCell>{spec.clusterIP || '-'}</TableCell>
|
<TableCell>{spec.clusterIP || '-'}</TableCell>
|
||||||
<TableCell>{spec.externalIPs?.join(', ') || status.loadBalancer?.ingress?.map((i: any) => i.ip || i.hostname).join(', ') || '-'}</TableCell>
|
<TableCell>{portsDisplay}</TableCell>
|
||||||
<TableCell>{spec.ports?.map((p: any) => `${p.port}/${p.protocol}`).join(', ') || '-'}</TableCell>
|
<TableCell>{nodePortsDisplay}</TableCell>
|
||||||
<TableCell>{getAge(metadata.creationTimestamp)}</TableCell>
|
<TableCell>{getAge(metadata.creationTimestamp)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Tooltip title="编辑">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => handleEditResource(metadata.name, metadata.namespace, selectedKind.label)}
|
||||||
|
>
|
||||||
|
<EditIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="删除">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
onClick={() => openDeleteDialog(metadata.name, metadata.namespace)}
|
||||||
|
>
|
||||||
|
<DeleteIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)
|
)
|
||||||
case 'configmap':
|
case 'configmap':
|
||||||
@@ -330,6 +519,25 @@ export default function K8sResourceList() {
|
|||||||
<TableCell>{metadata.namespace || '-'}</TableCell>
|
<TableCell>{metadata.namespace || '-'}</TableCell>
|
||||||
<TableCell>{Object.keys(resource.data || {}).length}</TableCell>
|
<TableCell>{Object.keys(resource.data || {}).length}</TableCell>
|
||||||
<TableCell>{getAge(metadata.creationTimestamp)}</TableCell>
|
<TableCell>{getAge(metadata.creationTimestamp)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Tooltip title="编辑">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => handleEditResource(metadata.name, metadata.namespace, selectedKind.label)}
|
||||||
|
>
|
||||||
|
<EditIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="删除">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
onClick={() => openDeleteDialog(metadata.name, metadata.namespace)}
|
||||||
|
>
|
||||||
|
<DeleteIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)
|
)
|
||||||
case 'pod':
|
case 'pod':
|
||||||
@@ -537,6 +745,46 @@ export default function K8sResourceList() {
|
|||||||
</Box>
|
</Box>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={editDialogOpen}
|
||||||
|
onClose={() => setEditDialogOpen(false)}
|
||||||
|
maxWidth="md"
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<DialogTitle>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Typography variant="h6">
|
||||||
|
编辑 {editResource?.kind}: {editResource?.name} ({editResource?.namespace})
|
||||||
|
</Typography>
|
||||||
|
<IconButton onClick={() => setEditDialogOpen(false)}>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<TextField
|
||||||
|
multiline
|
||||||
|
rows={20}
|
||||||
|
value={editYaml}
|
||||||
|
onChange={(e) => setEditYaml(e.target.value)}
|
||||||
|
placeholder="YAML 内容..."
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
sx={{ fontFamily: 'monospace', fontSize: '0.875rem' }}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setEditDialogOpen(false)}>取消</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleApplyEdit}
|
||||||
|
disabled={editing}
|
||||||
|
>
|
||||||
|
{editing ? <CircularProgress size={24} /> : '应用'}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
open={createDialogOpen}
|
open={createDialogOpen}
|
||||||
onClose={() => setCreateDialogOpen(false)}
|
onClose={() => setCreateDialogOpen(false)}
|
||||||
@@ -636,7 +884,8 @@ export default function K8sResourceList() {
|
|||||||
<DialogTitle>确认删除</DialogTitle>
|
<DialogTitle>确认删除</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Typography>
|
<Typography>
|
||||||
确定要删除 Pod <strong>{deleteTarget?.name}</strong> (namespace: {deleteTarget?.namespace}) 吗?
|
确定要删除 {selectedKind.label} <strong>{deleteTarget?.name}</strong>
|
||||||
|
{deleteTarget?.namespace && selectedKind.key !== 'namespace' ? ` (namespace: ${deleteTarget?.namespace})` : ''} 吗?
|
||||||
</Typography>
|
</Typography>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
@@ -644,7 +893,7 @@ export default function K8sResourceList() {
|
|||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="error"
|
color="error"
|
||||||
onClick={handleDeletePod}
|
onClick={handleDeleteResource}
|
||||||
disabled={deleting}
|
disabled={deleting}
|
||||||
>
|
>
|
||||||
{deleting ? <CircularProgress size={24} /> : '删除'}
|
{deleting ? <CircularProgress size={24} /> : '删除'}
|
||||||
|
|||||||
@@ -67,17 +67,24 @@ func Init(ctx context.Context, address string, db *gorm.DB, store store.Store) (
|
|||||||
k8sAPI.Post("/config", k8s.ClusterConfigSet(ctx, db, store))
|
k8sAPI.Post("/config", k8s.ClusterConfigSet(ctx, db, store))
|
||||||
// resource operations
|
// resource operations
|
||||||
k8sAPI.Post("/resource/apply", k8s.K8sResourceApply(ctx, db, store))
|
k8sAPI.Post("/resource/apply", k8s.K8sResourceApply(ctx, db, store))
|
||||||
|
k8sAPI.Get("/resource/get", k8s.K8sResourceFetch(ctx, db, store))
|
||||||
|
k8sAPI.Post("/resource/update", k8s.K8sResourceUpdate(ctx, db, store))
|
||||||
// resource list
|
// resource list
|
||||||
k8sAPI.Get("/namespace/list", k8s.K8sNamespaceList(ctx, db, store))
|
k8sAPI.Get("/namespace/list", k8s.K8sNamespaceList(ctx, db, store))
|
||||||
|
k8sAPI.Delete("/namespace/delete", k8s.K8sNamespaceDelete(ctx, db, store))
|
||||||
k8sAPI.Get("/deployment/list", k8s.K8sDeploymentList(ctx, db, store))
|
k8sAPI.Get("/deployment/list", k8s.K8sDeploymentList(ctx, db, store))
|
||||||
|
k8sAPI.Delete("/deployment/delete", k8s.K8sDeploymentDelete(ctx, db, store))
|
||||||
k8sAPI.Get("/statefulset/list", k8s.K8sStatefulSetList(ctx, db, store))
|
k8sAPI.Get("/statefulset/list", k8s.K8sStatefulSetList(ctx, db, store))
|
||||||
|
k8sAPI.Delete("/statefulset/delete", k8s.K8sStatefulSetDelete(ctx, db, store))
|
||||||
k8sAPI.Get("/configmap/list", k8s.K8sConfigMapList(ctx, db, store))
|
k8sAPI.Get("/configmap/list", k8s.K8sConfigMapList(ctx, db, store))
|
||||||
|
k8sAPI.Delete("/configmap/delete", k8s.K8sConfigMapDelete(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.Get("/pod/logs", k8s.K8sPodLogs(ctx, db, store))
|
||||||
k8sAPI.Delete("/pod/delete", k8s.K8sPodDelete(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))
|
||||||
|
k8sAPI.Delete("/service/delete", k8s.K8sServiceDelete(ctx, db, store))
|
||||||
}
|
}
|
||||||
|
|
||||||
ln, err = net.Listen("tcp", address)
|
ln, err = net.Listen("tcp", address)
|
||||||
|
|||||||
@@ -308,6 +308,112 @@ func K8sServiceList(ctx context.Context, db *gorm.DB, store store.Store) fiber.H
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func K8sResourceGet(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler {
|
||||||
|
return func(c fiber.Ctx) error {
|
||||||
|
name := c.Query("name", "")
|
||||||
|
namespace := c.Query("namespace", "")
|
||||||
|
kind := c.Query("kind", "")
|
||||||
|
|
||||||
|
if name == "" || kind == "" {
|
||||||
|
return resp.R400(c, "", nil, fmt.Errorf("name and kind are required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
clientset, err := getK8sClient(db)
|
||||||
|
if err != nil {
|
||||||
|
return resp.R500(c, "", nil, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var yamlData []byte
|
||||||
|
|
||||||
|
switch kind {
|
||||||
|
case "Deployment":
|
||||||
|
var deployment *appsv1.Deployment
|
||||||
|
if namespace != "" {
|
||||||
|
deployment, err = clientset.AppsV1().Deployments(namespace).Get(c.Context(), name, metav1.GetOptions{})
|
||||||
|
} else {
|
||||||
|
return resp.R400(c, "", nil, fmt.Errorf("namespace is required for Deployment"))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return resp.R500(c, "", nil, fmt.Errorf("failed to get deployment: %w", err))
|
||||||
|
}
|
||||||
|
yamlData, err = yaml.Marshal(deployment)
|
||||||
|
case "StatefulSet":
|
||||||
|
var statefulset *appsv1.StatefulSet
|
||||||
|
if namespace != "" {
|
||||||
|
statefulset, err = clientset.AppsV1().StatefulSets(namespace).Get(c.Context(), name, metav1.GetOptions{})
|
||||||
|
} else {
|
||||||
|
return resp.R400(c, "", nil, fmt.Errorf("namespace is required for StatefulSet"))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return resp.R500(c, "", nil, fmt.Errorf("failed to get statefulset: %w", err))
|
||||||
|
}
|
||||||
|
yamlData, err = yaml.Marshal(statefulset)
|
||||||
|
case "Service":
|
||||||
|
var service *corev1.Service
|
||||||
|
if namespace != "" {
|
||||||
|
service, err = clientset.CoreV1().Services(namespace).Get(c.Context(), name, metav1.GetOptions{})
|
||||||
|
} else {
|
||||||
|
return resp.R400(c, "", nil, fmt.Errorf("namespace is required for Service"))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return resp.R500(c, "", nil, fmt.Errorf("failed to get service: %w", err))
|
||||||
|
}
|
||||||
|
yamlData, err = yaml.Marshal(service)
|
||||||
|
case "ConfigMap":
|
||||||
|
var configmap *corev1.ConfigMap
|
||||||
|
if namespace != "" {
|
||||||
|
configmap, err = clientset.CoreV1().ConfigMaps(namespace).Get(c.Context(), name, metav1.GetOptions{})
|
||||||
|
} else {
|
||||||
|
return resp.R400(c, "", nil, fmt.Errorf("namespace is required for ConfigMap"))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return resp.R500(c, "", nil, fmt.Errorf("failed to get configmap: %w", err))
|
||||||
|
}
|
||||||
|
yamlData, err = yaml.Marshal(configmap)
|
||||||
|
default:
|
||||||
|
return resp.R400(c, "", nil, fmt.Errorf("unsupported resource kind: %s", kind))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return resp.R500(c, "", nil, fmt.Errorf("failed to marshal resource to yaml: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.R200(c, map[string]any{
|
||||||
|
"yaml": string(yamlData),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
func K8sResourceApply(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler {
|
func K8sResourceApply(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler {
|
||||||
return func(c fiber.Ctx) error {
|
return func(c fiber.Ctx) error {
|
||||||
var req struct {
|
var req struct {
|
||||||
@@ -336,6 +442,10 @@ func K8sResourceApply(ctx context.Context, db *gorm.DB, store store.Store) fiber
|
|||||||
return resp.R500(c, "", nil, fmt.Errorf("failed to parse kubeconfig: %w", err))
|
return resp.R500(c, "", nil, fmt.Errorf("failed to parse kubeconfig: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Force HTTP/1.1 to avoid stream closing issues
|
||||||
|
clientConfig.TLSClientConfig.NextProtos = []string{"http/1.1"}
|
||||||
|
clientConfig.Timeout = 0
|
||||||
|
|
||||||
dynamicClient, err := dynamic.NewForConfig(clientConfig)
|
dynamicClient, err := dynamic.NewForConfig(clientConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp.R500(c, "", nil, fmt.Errorf("failed to create dynamic client: %w", err))
|
return resp.R500(c, "", nil, fmt.Errorf("failed to create dynamic client: %w", err))
|
||||||
@@ -379,34 +489,213 @@ func K8sResourceApply(ctx context.Context, db *gorm.DB, store store.Store) fiber
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getResourceName(kind string) string {
|
func K8sResourceFetch(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler {
|
||||||
kindToResource := map[string]string{
|
return func(c fiber.Ctx) error {
|
||||||
"Namespace": "namespaces",
|
name := c.Query("name", "")
|
||||||
"Deployment": "deployments",
|
namespace := c.Query("namespace", "")
|
||||||
"StatefulSet": "statefulsets",
|
kind := c.Query("kind", "")
|
||||||
"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 {
|
if name == "" || kind == "" {
|
||||||
return resource
|
return resp.R400(c, "", nil, fmt.Errorf("name and kind are required"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return kind + "s"
|
clientset, err := getK8sClient(db)
|
||||||
|
if err != nil {
|
||||||
|
return resp.R500(c, "", nil, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var yamlData []byte
|
||||||
|
|
||||||
|
switch kind {
|
||||||
|
case "Deployment":
|
||||||
|
var deployment *appsv1.Deployment
|
||||||
|
if namespace != "" {
|
||||||
|
deployment, err = clientset.AppsV1().Deployments(namespace).Get(c.Context(), name, metav1.GetOptions{})
|
||||||
|
} else {
|
||||||
|
return resp.R400(c, "", nil, fmt.Errorf("namespace is required for Deployment"))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return resp.R500(c, "", nil, fmt.Errorf("failed to get deployment: %w", err))
|
||||||
|
}
|
||||||
|
// Ensure Kind and APIVersion are set
|
||||||
|
if deployment.Kind == "" {
|
||||||
|
deployment.Kind = "Deployment"
|
||||||
|
}
|
||||||
|
if deployment.APIVersion == "" {
|
||||||
|
deployment.APIVersion = "apps/v1"
|
||||||
|
}
|
||||||
|
// Clean up managed fields and other metadata that cause conflicts
|
||||||
|
deployment.ManagedFields = nil
|
||||||
|
deployment.ResourceVersion = ""
|
||||||
|
deployment.UID = ""
|
||||||
|
deployment.CreationTimestamp = metav1.Time{}
|
||||||
|
deployment.SelfLink = ""
|
||||||
|
deployment.Generation = 0
|
||||||
|
yamlData, err = yaml.Marshal(deployment)
|
||||||
|
case "StatefulSet":
|
||||||
|
var statefulset *appsv1.StatefulSet
|
||||||
|
if namespace != "" {
|
||||||
|
statefulset, err = clientset.AppsV1().StatefulSets(namespace).Get(c.Context(), name, metav1.GetOptions{})
|
||||||
|
} else {
|
||||||
|
return resp.R400(c, "", nil, fmt.Errorf("namespace is required for StatefulSet"))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return resp.R500(c, "", nil, fmt.Errorf("failed to get statefulset: %w", err))
|
||||||
|
}
|
||||||
|
// Ensure Kind and APIVersion are set
|
||||||
|
if statefulset.Kind == "" {
|
||||||
|
statefulset.Kind = "StatefulSet"
|
||||||
|
}
|
||||||
|
if statefulset.APIVersion == "" {
|
||||||
|
statefulset.APIVersion = "apps/v1"
|
||||||
|
}
|
||||||
|
// Clean up managed fields and other metadata that cause conflicts
|
||||||
|
statefulset.ManagedFields = nil
|
||||||
|
statefulset.ResourceVersion = ""
|
||||||
|
statefulset.UID = ""
|
||||||
|
statefulset.CreationTimestamp = metav1.Time{}
|
||||||
|
statefulset.SelfLink = ""
|
||||||
|
statefulset.Generation = 0
|
||||||
|
yamlData, err = yaml.Marshal(statefulset)
|
||||||
|
case "Service":
|
||||||
|
var service *corev1.Service
|
||||||
|
if namespace != "" {
|
||||||
|
service, err = clientset.CoreV1().Services(namespace).Get(c.Context(), name, metav1.GetOptions{})
|
||||||
|
} else {
|
||||||
|
return resp.R400(c, "", nil, fmt.Errorf("namespace is required for Service"))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return resp.R500(c, "", nil, fmt.Errorf("failed to get service: %w", err))
|
||||||
|
}
|
||||||
|
// Ensure Kind and APIVersion are set
|
||||||
|
if service.Kind == "" {
|
||||||
|
service.Kind = "Service"
|
||||||
|
}
|
||||||
|
if service.APIVersion == "" {
|
||||||
|
service.APIVersion = "v1"
|
||||||
|
}
|
||||||
|
// Clean up managed fields and other metadata that cause conflicts
|
||||||
|
service.ManagedFields = nil
|
||||||
|
service.ResourceVersion = ""
|
||||||
|
service.UID = ""
|
||||||
|
service.CreationTimestamp = metav1.Time{}
|
||||||
|
service.SelfLink = ""
|
||||||
|
service.Generation = 0
|
||||||
|
// Don't clean spec fields as they contain important information like NodePort
|
||||||
|
yamlData, err = yaml.Marshal(service)
|
||||||
|
case "ConfigMap":
|
||||||
|
var configmap *corev1.ConfigMap
|
||||||
|
if namespace != "" {
|
||||||
|
configmap, err = clientset.CoreV1().ConfigMaps(namespace).Get(c.Context(), name, metav1.GetOptions{})
|
||||||
|
} else {
|
||||||
|
return resp.R400(c, "", nil, fmt.Errorf("namespace is required for ConfigMap"))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return resp.R500(c, "", nil, fmt.Errorf("failed to get configmap: %w", err))
|
||||||
|
}
|
||||||
|
// Ensure Kind and APIVersion are set
|
||||||
|
if configmap.Kind == "" {
|
||||||
|
configmap.Kind = "ConfigMap"
|
||||||
|
}
|
||||||
|
if configmap.APIVersion == "" {
|
||||||
|
configmap.APIVersion = "v1"
|
||||||
|
}
|
||||||
|
// Clean up managed fields and other metadata that cause conflicts
|
||||||
|
configmap.ManagedFields = nil
|
||||||
|
configmap.ResourceVersion = ""
|
||||||
|
configmap.UID = ""
|
||||||
|
configmap.CreationTimestamp = metav1.Time{}
|
||||||
|
configmap.SelfLink = ""
|
||||||
|
configmap.Generation = 0
|
||||||
|
yamlData, err = yaml.Marshal(configmap)
|
||||||
|
default:
|
||||||
|
return resp.R400(c, "", nil, fmt.Errorf("unsupported resource kind: %s", kind))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return resp.R500(c, "", nil, fmt.Errorf("failed to marshal resource to yaml: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.R200(c, map[string]any{
|
||||||
|
"yaml": string(yamlData),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func K8sResourceUpdate(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))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force HTTP/1.1 to avoid stream closing issues
|
||||||
|
clientConfig.TLSClientConfig.NextProtos = []string{"http/1.1"}
|
||||||
|
clientConfig.Timeout = 0
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the resource
|
||||||
|
var result *unstructured.Unstructured
|
||||||
|
if namespace != "" {
|
||||||
|
result, err = dynamicClient.Resource(gvr).Namespace(namespace).Update(c.Context(), &obj, metav1.UpdateOptions{})
|
||||||
|
} else {
|
||||||
|
result, err = dynamicClient.Resource(gvr).Update(c.Context(), &obj, metav1.UpdateOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return resp.R500(c, "", nil, fmt.Errorf("failed to update resource: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.R200(c, map[string]any{
|
||||||
|
"name": result.GetName(),
|
||||||
|
"namespace": result.GetNamespace(),
|
||||||
|
"kind": result.GetKind(),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func K8sPodLogs(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler {
|
func K8sPodLogs(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler {
|
||||||
@@ -510,3 +799,161 @@ func K8sPodDelete(ctx context.Context, db *gorm.DB, store store.Store) fiber.Han
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func K8sDeploymentDelete(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.AppsV1().Deployments(req.Namespace).Delete(c.Context(), req.Name, metav1.DeleteOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return resp.R500(c, "", nil, fmt.Errorf("failed to delete deployment: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.R200(c, map[string]any{
|
||||||
|
"name": req.Name,
|
||||||
|
"namespace": req.Namespace,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func K8sStatefulSetDelete(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.AppsV1().StatefulSets(req.Namespace).Delete(c.Context(), req.Name, metav1.DeleteOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return resp.R500(c, "", nil, fmt.Errorf("failed to delete statefulset: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.R200(c, map[string]any{
|
||||||
|
"name": req.Name,
|
||||||
|
"namespace": req.Namespace,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func K8sServiceDelete(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().Services(req.Namespace).Delete(c.Context(), req.Name, metav1.DeleteOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return resp.R500(c, "", nil, fmt.Errorf("failed to delete service: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.R200(c, map[string]any{
|
||||||
|
"name": req.Name,
|
||||||
|
"namespace": req.Namespace,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func K8sConfigMapDelete(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().ConfigMaps(req.Namespace).Delete(c.Context(), req.Name, metav1.DeleteOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return resp.R500(c, "", nil, fmt.Errorf("failed to delete configmap: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.R200(c, map[string]any{
|
||||||
|
"name": req.Name,
|
||||||
|
"namespace": req.Namespace,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func K8sNamespaceDelete(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler {
|
||||||
|
return func(c fiber.Ctx) error {
|
||||||
|
var req struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(c.Body(), &req); err != nil {
|
||||||
|
return resp.R400(c, "", nil, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Name == "" {
|
||||||
|
return resp.R400(c, "", nil, fmt.Errorf("name is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
clientset, err := getK8sClient(db)
|
||||||
|
if err != nil {
|
||||||
|
return resp.R500(c, "", nil, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = clientset.CoreV1().Namespaces().Delete(c.Context(), req.Name, metav1.DeleteOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return resp.R500(c, "", nil, fmt.Errorf("failed to delete namespace: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.R200(c, map[string]any{
|
||||||
|
"name": req.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user