feat: add resource edit functionality for k8s resources
- Add edit button for Deployment, StatefulSet, Service, and ConfigMap resources - Implement resource fetch API to get YAML representation - Implement resource update API to apply edited YAML - Add edit dialog with YAML editor and apply/cancel buttons - Add tooltip icons for better UX - Restore K8sResourceApply function with HTTP/1.1 enforcement - Support for fetching and updating the following resource kinds: - Deployment - StatefulSet - Service - ConfigMap 🤖 Generated with [Qoder][https://qoder.com]
This commit is contained in:
@@ -67,6 +67,8 @@ func Init(ctx context.Context, address string, db *gorm.DB, store store.Store) (
|
||||
k8sAPI.Post("/config", k8s.ClusterConfigSet(ctx, db, store))
|
||||
// resource operations
|
||||
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
|
||||
k8sAPI.Get("/namespace/list", k8s.K8sNamespaceList(ctx, db, store))
|
||||
k8sAPI.Get("/deployment/list", k8s.K8sDeploymentList(ctx, db, store))
|
||||
|
||||
@@ -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 {
|
||||
return func(c fiber.Ctx) error {
|
||||
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))
|
||||
}
|
||||
|
||||
// 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))
|
||||
@@ -379,34 +489,156 @@ func K8sResourceApply(ctx context.Context, db *gorm.DB, store store.Store) fiber
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
}
|
||||
func K8sResourceFetch(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 resource, ok := kindToResource[kind]; ok {
|
||||
return resource
|
||||
}
|
||||
if name == "" || kind == "" {
|
||||
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))
|
||||
}
|
||||
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 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 {
|
||||
|
||||
Reference in New Issue
Block a user