backend/services/cluster_service.go
package services
import (
"context"
"encoding/json"
"fmt"
"kd/backend/config"
"kd/backend/logwrap"
"kd/backend/types"
"strings"
"sync"
"time"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
k8types "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/util/retry"
"k8s.io/kubectl/pkg/describe"
describecmd "k8s.io/kubectl/pkg/describe"
"k8s.io/kubectl/pkg/scheme"
)
type clusterService struct {
ctx context.Context
conf config.Config
logger *logwrap.LogWrap
}
var cluster *clusterService
var onceCluster sync.Once
func Cluster() *clusterService {
if cluster == nil {
onceCluster.Do(func() {
cluster = &clusterService{}
})
}
return cluster
}
func (c *clusterService) Start(ctx context.Context, conf config.Config, logger *logwrap.LogWrap) {
c.ctx = ctx
c.conf = conf
c.logger = logger
}
func (c *clusterService) GetClusters() types.ClustersResponse {
r := types.NewClustersResponse()
for k := range c.conf.KubeClusters {
cluster := types.Cluster{ContextName: k.String()}
r.Data = append(r.Data, cluster)
}
r.Success = true
return r
}
func (c *clusterService) GetNamespace(contextName, name string) types.NamespaceResponse {
r := types.NewNamespaceResponse()
cs, err := c.conf.GetClientSet(contextName)
if err != nil {
r.Msg = "Failed to find context name"
return r
}
namespace, err := cs.CoreV1().Namespaces().Get(c.ctx, name, metav1.GetOptions{})
if err != nil {
r.Msg = fmt.Sprintf("Failed to get Namespace: %s", err.Error())
return r
}
r.Data = *namespace
r.Success = true
return r
}
func (c *clusterService) GetNamespaces(context string) types.NamespacesResponse {
r := types.NewNamespacesResponse()
cs, err := c.conf.GetClientSet(context)
if err != nil {
r.Msg = "Failed to find context name"
return r
}
namespacesClient := cs.CoreV1().Namespaces()
namespacesList, err := namespacesClient.List(c.ctx, metav1.ListOptions{})
if err != nil {
r.Msg = "Failed to list Namespaces"
return r
}
for _, namespace := range namespacesList.Items {
r.Data = append(r.Data, namespace)
}
r.Success = true
return r
}
func (c *clusterService) RestartDeployment(contextName, namespaceName, name string) types.DeploymentResponse {
r := types.NewDeploymentResponse()
cs, err := c.conf.GetClientSet(contextName)
if err != nil {
r.Msg = "Failed to find context name"
return r
}
deploymentsClient := cs.AppsV1().Deployments(namespaceName)
deployment, err := deploymentsClient.Get(c.ctx, name, metav1.GetOptions{})
if err != nil {
r.Msg = "Failed to get Deployment"
return r
}
deployment.Spec.Template.Annotations = make(map[string]string)
deployment.Spec.Template.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339)
deployment, err = deploymentsClient.Update(c.ctx, deployment, metav1.UpdateOptions{})
if err != nil {
r.Msg = "Failed to update Deployment"
return r
}
r.Data = *deployment
r.Success = true
return r
}
func (c *clusterService) RestartStatefulSet(contextName, namespaceName, name string) types.StatefulSetResponse {
r := types.NewStatefulSetResponse()
cs, err := c.conf.GetClientSet(contextName)
if err != nil {
r.Msg = "Failed to find context name"
return r
}
statefulSetsClient := cs.AppsV1().StatefulSets(namespaceName)
statefulSet, err := statefulSetsClient.Get(c.ctx, name, metav1.GetOptions{})
if err != nil {
r.Msg = "Failed to get StatefulSet"
return r
}
statefulSet.Spec.Template.Annotations = make(map[string]string)
statefulSet.Spec.Template.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339)
statefulSet, err = statefulSetsClient.Update(c.ctx, statefulSet, metav1.UpdateOptions{})
if err != nil {
r.Msg = "Failed to update StatefulSet"
return r
}
r.Data = *statefulSet
r.Success = true
return r
}
func (c *clusterService) GetStatefulSets(contextName string, namespace string) types.StatefulSetsResponse {
r := types.NewStatefulSetsResponse()
cs, err := c.conf.GetClientSet(contextName)
if err != nil {
r.Msg = fmt.Sprintf("Failed to find context name %s", contextName)
return r
}
statefulSetsClient := cs.AppsV1().StatefulSets(namespace)
statefulSetsList, err := statefulSetsClient.List(c.ctx, metav1.ListOptions{})
if err != nil {
r.Msg = fmt.Sprintf("Failed to list StatefulSets for %s: %v", contextName, err)
return r
}
for _, statefulSet := range statefulSetsList.Items {
r.Data = append(r.Data, statefulSet)
}
r.Success = true
return r
}
func (c *clusterService) GetDeployments(context string, namespaceName string) types.DeploymentsResponse {
r := types.NewDeploymentsResponse()
cs, err := c.conf.GetClientSet(context)
if err != nil {
r.Msg = fmt.Sprintf("Failed to find context name %s", context)
return r
}
deploymentsClient := cs.AppsV1().Deployments(namespaceName)
deploymentsList, err := deploymentsClient.List(c.ctx, metav1.ListOptions{})
if err != nil {
r.Msg = fmt.Sprintf("Failed to list Deployments for %s: %v", context, err)
return r
}
for _, deployment := range deploymentsList.Items {
r.Data = append(r.Data, deployment)
}
r.Success = true
return r
}
func (c *clusterService) GetPod(contextName, namespaceName, name string) types.PodResponse {
r := types.NewPodResponse()
cs, err := c.conf.GetClientSet(contextName)
if err != nil {
r.Msg = "Failed to find context name"
return r
}
podsClient := cs.CoreV1().Pods(namespaceName)
pod, err := podsClient.Get(c.ctx, name, metav1.GetOptions{})
if err != nil {
r.Msg = "Failed to list Pods"
return r
}
r.Data = *pod
r.Success = true
return r
}
func (c *clusterService) GetPods(contextName, namespaceName string) types.PodsResponse {
r := types.NewPodsResponse()
cs, err := c.conf.GetClientSet(contextName)
if err != nil {
r.Msg = "Failed to find context name"
return r
}
podsClient := cs.CoreV1().Pods(namespaceName)
podsList, err := podsClient.List(c.ctx, metav1.ListOptions{})
if err != nil {
r.Msg = "Failed to list Pods"
return r
}
r.Data = podsList.Items
r.Success = true
return r
}
func (c *clusterService) GetConfigMaps(contextName, namespaceName string) types.ConfigMapsResponse {
r := types.NewConfigMapsResponse()
cs, err := c.conf.GetClientSet(contextName)
if err != nil {
r.Msg = "Failed to find context name"
return r
}
configMapsClient := cs.CoreV1().ConfigMaps(namespaceName)
configMapsList, err := configMapsClient.List(c.ctx, metav1.ListOptions{})
if err != nil {
r.Msg = "Failed to list ConfigMaps"
return r
}
r.Data = configMapsList.Items
r.Success = true
return r
}
func (c *clusterService) GetIngresses(context, namespace string) types.IngressesResponse {
r := types.NewIngressesResponse()
cs, err := c.conf.GetClientSet(context)
if err != nil {
r.Msg = "Failed to find context name"
return r
}
client := cs.NetworkingV1().Ingresses(namespace)
out, err := client.List(c.ctx, metav1.ListOptions{})
if err != nil {
r.Msg = "Failed to list ConfigMaps"
return r
}
r.Data = out.Items
r.Success = true
return r
}
func (c *clusterService) DescribeConfigMap(context, namespace, name string) types.DescribeResponse {
r := types.NewDescribeResponse()
rc, err := c.conf.GetRestConfig(context)
if err != nil {
r.Msg = fmt.Sprintf("Failed to find rest config name: %s", err.Error())
return r
}
gk := schema.GroupKind{Group: "", Kind: "ConfigMap"}
d, ok := describecmd.DescriberFor(gk, rc)
if !ok {
r.Msg = "Failed to find describer"
return r
}
out, err := d.Describe(namespace, name, describe.DescriberSettings{})
if err != nil {
r.Msg = fmt.Sprintf("Failed to describe resource: %s", err.Error())
return r
}
r.Data = out
r.Success = true
return r
}
func (c *clusterService) DescribePod(context, namespace, name string) types.DescribeResponse {
r := types.NewDescribeResponse()
rc, err := c.conf.GetRestConfig(context)
if err != nil {
r.Msg = "Failed to find rest config name"
return r
}
gk := schema.GroupKind{Group: "", Kind: "Pod"}
d, ok := describecmd.DescriberFor(gk, rc)
if !ok {
r.Msg = "Failed to find describer"
return r
}
out, err := d.Describe(namespace, name, describe.DescriberSettings{})
if err != nil {
r.Msg = "Failed to describe pod"
return r
}
r.Data = out
r.Success = true
return r
}
func (c *clusterService) DescribeDeployment(context, namespace, name string) types.DescribeResponse {
r := types.NewDescribeResponse()
rc, err := c.conf.GetRestConfig(context)
if err != nil {
r.Msg = "Failed to find rest config name"
return r
}
gk := schema.GroupKind{Group: "apps", Kind: "Deployment"}
d, ok := describecmd.DescriberFor(gk, rc)
if !ok {
r.Msg = "Failed to find describer"
return r
}
out, err := d.Describe(namespace, name, describe.DescriberSettings{})
if err != nil {
r.Msg = "Failed to describe deployment"
return r
}
r.Data = out
r.Success = true
return r
}
func (c *clusterService) DescribeStatefulSet(context, namespace, name string) types.DescribeResponse {
r := types.NewDescribeResponse()
rc, err := c.conf.GetRestConfig(context)
if err != nil {
r.Msg = "Failed to find rest config name"
return r
}
gk := schema.GroupKind{Group: "apps", Kind: "StatefulSet"}
d, ok := describecmd.DescriberFor(gk, rc)
if !ok {
r.Msg = "Failed to find describer"
return r
}
out, err := d.Describe(namespace, name, describe.DescriberSettings{})
if err != nil {
r.Msg = "Failed to describe statefulset"
return r
}
r.Data = out
r.Success = true
return r
}
func (c *clusterService) DescribeNamespace(context, namespace string) types.DescribeResponse {
r := types.NewDescribeResponse()
rc, err := c.conf.GetRestConfig(context)
if err != nil {
r.Msg = "Failed to find rest config name"
return r
}
gk := schema.GroupKind{Group: "", Kind: "Namespace"}
d, ok := describecmd.DescriberFor(gk, rc)
if !ok {
r.Msg = "Failed to find describer"
return r
}
out, err := d.Describe(namespace, namespace, describe.DescriberSettings{})
if err != nil {
r.Msg = "Failed to describe namespace"
return r
}
r.Data = out
r.Success = true
return r
}
func (c *clusterService) DescribeIngress(context, namespace, name string) types.DescribeResponse {
r := types.NewDescribeResponse()
rc, err := c.conf.GetRestConfig(context)
if err != nil {
r.Msg = "Failed to find rest config name"
return r
}
gk := schema.GroupKind{Group: "networking.k8s.io", Kind: "Ingress"}
d, ok := describecmd.DescriberFor(gk, rc)
if !ok {
r.Msg = "Failed to find describer"
return r
}
out, err := d.Describe(namespace, name, describe.DescriberSettings{})
if err != nil {
r.Msg = "Failed to describe ingress"
return r
}
r.Data = out
r.Success = true
return r
}
func (c *clusterService) DeletePod(contextName, namespaceName, podName string) types.Response {
r := types.NewResponse()
cs, err := c.conf.GetClientSet(contextName)
if err != nil {
r.Msg = "Failed to find context name"
return r
}
podsClient := cs.CoreV1().Pods(namespaceName)
deletePolicy := metav1.DeletePropagationForeground
if err := podsClient.Delete(c.ctx, podName, metav1.DeleteOptions{PropagationPolicy: &deletePolicy}); err != nil {
r.Msg = "Failed to delete Pod"
return r
}
r.Success = true
return r
}
func (c *clusterService) DeleteIngress(context, namespace, name string) types.Response {
r := types.NewResponse()
cs, err := c.conf.GetClientSet(context)
if err != nil {
r.Msg = "Failed to find context name"
return r
}
client := cs.NetworkingV1().Ingresses(namespace)
deletePolicy := metav1.DeletePropagationForeground
if err := client.Delete(c.ctx, name, metav1.DeleteOptions{PropagationPolicy: &deletePolicy}); err != nil {
r.Msg = "Failed to delete Ingress"
return r
}
r.Success = true
return r
}
func (c *clusterService) getNamespaceFromCustomNamespace(customNamespace any) (*corev1.Namespace, error) {
namespace := &corev1.Namespace{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Namespace",
},
ObjectMeta: metav1.ObjectMeta{},
Spec: corev1.NamespaceSpec{},
}
namespaceMap, ok := customNamespace.(map[string]interface{})
if !ok {
return namespace, fmt.Errorf("unable to convert custom namespace to runtime object")
}
if metadata, ok := namespaceMap["metadata"].(map[string]interface{}); ok {
name, ok := metadata["name"].(string)
if ok {
namespace.ObjectMeta.Name = name
}
}
return namespace, nil
}
// getPodFromCustomPod accepts any and returns a corev1.Pod. customPod is sent
// from frontend and as of 2025-04-28 is a map[string]interface. Normalising of
// the customPod needs to occur so that it can be coerced into a corev1.Pod,
// such as the pod.Spec.Container[0].Command.
//
// As of 2025-04-28 this only supports guided customPods.
func (c *clusterService) getPodFromCustomPod(customPod any) (*corev1.Pod, error) {
pod := &corev1.Pod{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Pod",
},
ObjectMeta: metav1.ObjectMeta{},
Spec: corev1.PodSpec{},
}
// Convert podDefinition to a runtime.Object
podMap, ok := customPod.(map[string]interface{})
if !ok {
return pod, fmt.Errorf("unable to convert custom pod to runtime object")
}
// Extract metadata
if metadataMap, ok := podMap["metadata"].(map[string]interface{}); ok {
if name, ok := metadataMap["name"].(string); ok {
pod.Name = name
}
if namespace, ok := metadataMap["namespace"].(string); ok {
pod.Namespace = namespace
}
}
// Extract containers
if specMap, ok := podMap["spec"].(map[string]interface{}); ok {
if containersSlice, ok := specMap["containers"].([]interface{}); ok {
for _, containerInterface := range containersSlice {
if containerMap, ok := containerInterface.(map[string]interface{}); ok {
container := corev1.Container{}
for key, value := range containerMap {
switch key {
case "name":
if name, ok := value.(string); ok {
container.Name = name
}
case "image":
if image, ok := value.(string); ok {
container.Image = image
}
case "command":
switch cmdValue := value.(type) {
case []interface{}:
command := make([]string, 0, len(cmdValue))
for _, cmdItem := range cmdValue {
if cmdStr, ok := cmdItem.(string); ok {
command = append(command, cmdStr)
}
}
// Special handling for "sh -c" commands
if len(command) >= 3 && command[0] == "sh" && command[1] == "-c" {
// Combine all arguments after "sh -c" into a single string
shellCommand := strings.Join(command[2:], " ")
command = []string{"sh", "-c", shellCommand}
}
container.Command = command
case string:
var commandSlice []string
// Try to unmarshal as JSON array
if err := json.Unmarshal([]byte(cmdValue), &commandSlice); err == nil {
// Special handling for "sh -c" commands
if len(commandSlice) >= 3 && commandSlice[0] == "sh" && commandSlice[1] == "-c" {
// Combine all arguments after "sh -c" into a single string
shellCommand := strings.Join(commandSlice[2:], " ")
commandSlice = []string{"sh", "-c", shellCommand}
}
container.Command = commandSlice
} else {
// If JSON parsing fails, manually parse the string
cmdStr := strings.TrimSpace(cmdValue)
// Check if it looks like a JSON array
if strings.HasPrefix(cmdStr, "[") && strings.HasSuffix(cmdStr, "]") {
// Remove the brackets
cmdStr = cmdStr[1 : len(cmdStr)-1]
// Split by commas and process each part
parts := strings.Split(cmdStr, ",")
command := make([]string, 0, len(parts))
for _, part := range parts {
// Trim spaces and quotes
part = strings.TrimSpace(part)
if strings.HasPrefix(part, "\"") && strings.HasSuffix(part, "\"") {
part = part[1 : len(part)-1]
}
if part != "" {
command = append(command, part)
}
}
// Special handling for "sh -c" commands
if len(command) >= 3 && command[0] == "sh" && command[1] == "-c" {
// Combine all arguments after "sh -c" into a single string
shellCommand := strings.Join(command[2:], " ")
command = []string{"sh", "-c", shellCommand}
}
container.Command = command
} else {
// If it doesn't look like a JSON array, split by spaces
parts := strings.Fields(cmdStr)
// Special handling for "sh -c" commands
if len(parts) >= 3 && parts[0] == "sh" && parts[1] == "-c" {
// Combine all arguments after "sh -c" into a single string
shellCommand := strings.Join(parts[2:], " ")
parts = []string{"sh", "-c", shellCommand}
}
container.Command = parts
}
}
}
}
}
pod.Spec.Containers = append(pod.Spec.Containers, container)
}
}
}
}
// Ensure namespace is set
if pod.Namespace == "" {
pod.Namespace = "default"
}
return pod, nil
}
func (c *clusterService) CreateNamespace(context string, definition any) types.Response {
r := types.NewResponse()
namespace, err := c.getNamespaceFromCustomNamespace(definition)
if err != nil {
r.Msg = "Failed to create namespace"
c.logger.Error(fmt.Sprintf("%s: %s", r.Msg, err.Error()))
return r
}
cs, err := c.conf.GetClientSet(context)
if err != nil {
r.Msg = fmt.Sprintf("Failed to find context name: %s", err.Error())
return r
}
client := cs.CoreV1().Namespaces()
if _, err := client.Create(c.ctx, namespace, metav1.CreateOptions{}); err != nil {
r.Msg = "Failed to create namespace"
c.logger.Error(fmt.Sprintf("%s: %s", r.Msg, err.Error()))
return r
}
r.Msg = fmt.Sprintf("Namespace created: %s", namespace.GetName())
r.Success = true
return r
}
func (c *clusterService) CreatePod(context string, podDefinition any) types.Response {
r := types.NewResponse()
pod, err := c.getPodFromCustomPod(podDefinition)
if err != nil {
r.Msg = fmt.Sprintf("Failed to create Pod: %s", err)
c.logger.Error(r.Msg)
return r
}
cs, err := c.conf.GetClientSet(context)
if err != nil {
r.Msg = "Failed to find context name"
c.logger.Error(r.Msg)
return r
}
client := cs.CoreV1().Pods(pod.GetNamespace())
if _, err := client.Create(c.ctx, pod, metav1.CreateOptions{}); err != nil {
r.Msg = fmt.Sprintf("Failed to create Pod: %s", err)
c.logger.Error(r.Msg)
return r
}
r.Msg = fmt.Sprintf("Pod created: %s", pod.GetName())
r.Success = true
return r
}
func (c *clusterService) CreateNamespaceYAML(context string, s string) types.Response {
r := types.NewResponse()
namespace, err := c.createNamespaceFromYamlWithCodec(s)
if err != nil {
r.Msg = "Failed to create Namespace from YAML"
c.logger.Error(fmt.Sprintf("%s: %s", r.Msg, err))
return r
}
cs, err := c.conf.GetClientSet(context)
if err != nil {
r.Msg = "Failed to find context name"
c.logger.Error(r.Msg)
return r
}
client := cs.CoreV1().Namespaces()
if _, err := client.Create(c.ctx, namespace, metav1.CreateOptions{}); err != nil {
r.Msg = fmt.Sprintf("Failed to create Namespace: %s", err)
c.logger.Error(r.Msg)
return r
}
r.Msg = fmt.Sprintf("Namespace created: %s", namespace.GetName())
r.Success = true
return r
}
func (c *clusterService) CreatePodYAML(context string, s string) types.Response {
r := types.NewResponse()
pod, err := c.createPodFromYamlWithCodec(s)
if err != nil {
r.Msg = "Failed to create Pod from YAML"
c.logger.Error(fmt.Sprintf("%s: %s", r.Msg, err))
return r
}
cs, err := c.conf.GetClientSet(context)
if err != nil {
r.Msg = "Failed to find context name"
c.logger.Error(r.Msg)
return r
}
client := cs.CoreV1().Pods(pod.GetNamespace())
if _, err := client.Create(c.ctx, pod, metav1.CreateOptions{}); err != nil {
r.Msg = fmt.Sprintf("Failed to create Pod: %s", err)
c.logger.Error(r.Msg)
return r
}
r.Msg = fmt.Sprintf("Pod created: %s", pod.GetName())
r.Success = true
return r
}
func (c *clusterService) DeleteNamespace(context, name string) types.Response {
r := types.NewResponse()
cs, err := c.conf.GetClientSet(context)
if err != nil {
r.Msg = "Failed to find context name"
return r
}
namespaceClient := cs.CoreV1().Namespaces()
deletePolicy := metav1.DeletePropagationForeground
if err := namespaceClient.Delete(c.ctx, name, metav1.DeleteOptions{PropagationPolicy: &deletePolicy}); err != nil {
r.Msg = "Failed to delete Namespace"
return r
}
r.Success = true
return r
}
func (c *clusterService) createNamespaceFromYamlWithCodec(yamlContent string) (*corev1.Namespace, error) {
decode := serializer.NewCodecFactory(scheme.Scheme).UniversalDeserializer().Decode
obj, gvk, err := decode([]byte(yamlContent), nil, nil)
if err != nil {
return nil, err
}
if gvk.Kind != "Namespace" {
return nil, fmt.Errorf("YAML does not define a Namespace resource, got: %s", gvk.Kind)
}
namespace, ok := obj.(*corev1.Namespace)
if !ok {
return nil, fmt.Errorf("failed to convert object to Namespace")
}
return namespace, nil
}
func (c *clusterService) createPodFromYamlWithCodec(yamlContent string) (*corev1.Pod, error) {
decode := serializer.NewCodecFactory(scheme.Scheme).UniversalDeserializer().Decode
obj, gvk, err := decode([]byte(yamlContent), nil, nil)
if err != nil {
return nil, err
}
// Check if the object is a Pod
if gvk.Kind != "Pod" {
return nil, fmt.Errorf("YAML does not define a Pod resource, got: %s", gvk.Kind)
}
pod, ok := obj.(*corev1.Pod)
if !ok {
return nil, fmt.Errorf("failed to convert object to Pod")
}
return pod, nil
}
// patchNamespace takes a namespace name and a slice of bytes which represents
// a patch to the namespace resource. It will attempt to patch the namespace.
func (c *clusterService) patchNamespace(clientset *kubernetes.Clientset, name string, patchData []byte) error {
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
_, err := clientset.CoreV1().Namespaces().Patch(
context.TODO(),
name,
k8types.StrategicMergePatchType, // You can also use types.MergePatchType or types.JSONPatchType
patchData,
metav1.PatchOptions{},
)
return err
})
}
func (c *clusterService) UpdateNamespaceFromYaml(context, yamlContent, originalName string) types.NamespaceResponse {
r := types.NewNamespaceResponse()
cs, err := c.conf.GetClientSet(context)
if err != nil {
r.Msg = "Failed to find context name"
return r
}
decode := serializer.NewCodecFactory(scheme.Scheme).UniversalDeserializer().Decode
obj, gvk, err := decode([]byte(yamlContent), nil, nil)
if err != nil {
r.Msg = fmt.Sprintf("Failed to parse YAML: %v", err)
return r
}
if gvk.Kind != "Namespace" {
r.Msg = "YAML does not define a Namespace resource"
return r
}
b, err := json.Marshal(obj)
if err != nil {
r.Msg = fmt.Sprintf("Failed to convert object to JSON: %v", err)
return r
}
if err := c.patchNamespace(cs, originalName, b); err != nil {
r.Msg = fmt.Sprintf("Failed to patch Namespace: %v", err)
return r
}
r.Msg = "Namespace updated"
r.Success = true
return r
}
// UpdatePodFromYaml updates a Pod from YAML definition, handling name and namespace changes
func (c *clusterService) UpdatePodFromYaml(contextName, yamlContent, originalName, originalNamespace string) types.PodResponse {
r := types.NewPodResponse()
// Get the client set for the context
cs, err := c.conf.GetClientSet(contextName)
if err != nil {
r.Msg = "Failed to find context name"
return r
}
// Decode YAML to Kubernetes object
decode := serializer.NewCodecFactory(scheme.Scheme).UniversalDeserializer().Decode
obj, gvk, err := decode([]byte(yamlContent), nil, nil)
if err != nil {
r.Msg = fmt.Sprintf("Failed to parse YAML: %v", err)
return r
}
// Check if the object is a Pod
if gvk.Kind != "Pod" {
r.Msg = "YAML does not define a Pod resource"
return r
}
pod, ok := obj.(*corev1.Pod)
if !ok {
r.Msg = "Failed to convert object to Pod"
return r
}
// Ensure namespace is set
newNamespace := pod.Namespace
if newNamespace == "" {
newNamespace = "default"
pod.Namespace = newNamespace
}
newName := pod.Name
// Check if name or namespace changed
nameChanged := newName != originalName
namespaceChanged := newNamespace != originalNamespace
// Get the original pod client
originalPodsClient := cs.CoreV1().Pods(originalNamespace)
// Get the current pod to check if it exists
_, err = originalPodsClient.Get(c.ctx, originalName, metav1.GetOptions{})
if err != nil {
if errors.IsNotFound(err) {
r.Msg = fmt.Sprintf("Original pod %s not found in namespace %s", originalName, originalNamespace)
} else {
r.Msg = fmt.Sprintf("Failed to get original pod: %v", err)
}
return r
}
// If name or namespace changed, check if the new pod already exists
if nameChanged || namespaceChanged {
newPodsClient := cs.CoreV1().Pods(newNamespace)
_, err = newPodsClient.Get(c.ctx, newName, metav1.GetOptions{})
if err == nil {
// Pod with new name/namespace already exists
r.Msg = fmt.Sprintf("Pod %s already exists in namespace %s", newName, newNamespace)
return r
} else if !errors.IsNotFound(err) {
// Some other error occurred
r.Msg = fmt.Sprintf("Error checking for existing pod: %v", err)
return r
}
// If we get here, the new pod doesn't exist, which is what we want
}
err = originalPodsClient.Delete(c.ctx, originalName, kubernetesDeleteOptions)
if err != nil {
r.Msg = fmt.Sprintf("Failed to delete original pod: %v", err)
return r
}
// Wait for the original pod to be deleted
// TODO(rene): move timeout duration to config option
err = wait.PollImmediate(time.Second, time.Second*120, func() (bool, error) {
_, err := originalPodsClient.Get(c.ctx, originalName, metav1.GetOptions{})
if errors.IsNotFound(err) {
return true, nil
}
if err != nil {
return false, err
}
return false, nil
})
if err != nil {
r.Msg = fmt.Sprintf("Timed out waiting for pod deletion: %v", err)
c.logger.Error(r.Msg)
return r
}
// Remove fields that shouldn't be set on creation
pod.ResourceVersion = ""
pod.UID = ""
pod.CreationTimestamp = metav1.Time{}
pod.Status = corev1.PodStatus{}
// Create the pod with the updated spec
newPodsClient := cs.CoreV1().Pods(newNamespace)
createdPod, err := newPodsClient.Create(c.ctx, pod, metav1.CreateOptions{})
if err != nil {
r.Msg = fmt.Sprintf("Failed to create updated pod: %v", err)
c.logger.Error(r.Msg)
return r
}
r.Data = *createdPod
r.Success = true
if nameChanged || namespaceChanged {
r.Msg = fmt.Sprintf("Pod renamed from %s/%s to %s/%s",
originalNamespace, originalName, newNamespace, newName)
} else {
r.Msg = fmt.Sprintf("Pod %s updated successfully in namespace %s", newName, newNamespace)
}
return r
}