summary history files

backend/services/secrets_service.go
package services

import (
	"context"
	"encoding/json"
	"fmt"
	"kd/backend/config"
	"kd/backend/logwrap"
	"kd/backend/types"
	"strings"
	"sync"

	corev1 "k8s.io/api/core/v1"
	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/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 secretsService struct {
	ctx    context.Context
	conf   config.Config
	logger *logwrap.LogWrap
}

var secrets *secretsService
var onceSecrets sync.Once

func Secrets() *secretsService {
	if secrets == nil {
		onceSecrets.Do(func() {
			secrets = &secretsService{}
		})
	}
	return secrets
}

func (s *secretsService) Start(ctx context.Context, conf config.Config, logger *logwrap.LogWrap) {
	s.ctx = ctx
	s.conf = conf
	s.logger = logger
}

func (s *secretsService) List(context, namespace string) types.SecretsResponse {
	r := types.NewSecretsResponse()

	cs, err := s.conf.GetClientSet(context)
	if err != nil {
		r.Msg = "Failed to find context name"
		return r
	}

	secretsList, err := cs.CoreV1().Secrets(namespace).List(s.ctx, metav1.ListOptions{})
	if err != nil {
		r.Msg = "Failed to list Secrets"
		return r
	}

	r.Data = secretsList.Items
	r.Success = true
	return r
}

func (s *secretsService) Describe(context, namespace, name string) types.DescribeResponse {
	r := types.NewDescribeResponse()

	rc, err := s.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: "Secret"}
	d, ok := describecmd.DescriberFor(gk, rc)
	if !ok {
		r.Msg = fmt.Sprintf("Failed to find describer: %s", err.Error())
		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 (s *secretsService) getSecretFromCustomSecret(secretMap map[string]any) (*corev1.Secret, error) {
	secret := &corev1.Secret{
		TypeMeta: metav1.TypeMeta{
			APIVersion: "v1",
			Kind:       "Secret",
		},
		ObjectMeta: metav1.ObjectMeta{},
		Type:       corev1.SecretTypeOpaque, // Default type
		Data:       map[string][]byte{},
		StringData: map[string]string{},
	}

	// Extract metadata
	if metadata, ok := secretMap["metadata"].(map[string]interface{}); ok {
		// Set name
		if name, ok := metadata["name"].(string); ok {
			secret.ObjectMeta.Name = name
		}

		// Set namespace
		if namespace, ok := metadata["namespace"].(string); ok {
			secret.ObjectMeta.Namespace = namespace
		}

		// Set labels
		if labels, ok := metadata["labels"].(map[string]interface{}); ok {
			secret.ObjectMeta.Labels = make(map[string]string)
			for key, value := range labels {
				if strValue, ok := value.(string); ok {
					secret.ObjectMeta.Labels[key] = strValue
				}
			}
		}

		// Set annotations
		if annotations, ok := metadata["annotations"].(map[string]interface{}); ok {
			secret.ObjectMeta.Annotations = make(map[string]string)
			for key, value := range annotations {
				if strValue, ok := value.(string); ok {
					secret.ObjectMeta.Annotations[key] = strValue
				}
			}
		}
	}

	// Extract type
	if secretType, ok := secretMap["type"].(string); ok {
		secret.Type = corev1.SecretType(secretType)
	}

	// Extract data (base64 encoded)
	if data, ok := secretMap["data"].(map[string]interface{}); ok {
		for key, value := range data {
			if strValue, ok := value.(string); ok {
				// Data in k8s secrets is base64 encoded
				secret.Data[key] = []byte(strValue)
			}
		}
	}

	// Extract stringData (not base64 encoded)
	if stringData, ok := secretMap["stringData"].(map[string]interface{}); ok {
		for key, value := range stringData {
			if strValue, ok := value.(string); ok {
				secret.StringData[key] = strValue
			}
		}
	}

	return secret, nil
}

func (s *secretsService) Delete(context, namespace, name string) types.Response {
	r := types.NewResponse()
	cs, err := s.conf.GetClientSet(context)
	if err != nil {
		r.Msg = "Failed to find context name"
		return r
	}

	client := cs.CoreV1().Secrets(namespace)
	if err := client.Delete(s.ctx, name, kubernetesDeleteOptions); err != nil {
		r.Msg = fmt.Sprintf("Failed to delete Secret: %s", name)
		return r
	}
	r.Msg = fmt.Sprintf("Secret deleted: %s", name)
	r.Success = true
	return r
}

func (s *secretsService) Get(context, namespace, name string) types.SecretResponse {
	r := types.NewSecretResponse()
	cs, err := s.conf.GetClientSet(context)
	if err != nil {
		r.Msg = "Failed to find context name"
		return r
	}
	secret, err := cs.CoreV1().Secrets(namespace).Get(s.ctx, name, metav1.GetOptions{})
	if err != nil {
		r.Msg = fmt.Sprintf("Failed to get Secret: %s", err.Error())
		return r
	}
	r.Data = *secret
	r.Success = true
	return r
}

func (s *secretsService) Update(context, input, originalNamespace, originalName string) types.SecretResponse {
	r := types.NewSecretResponse()

	cs, err := s.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(input), nil, nil)
	if err != nil {
		r.Msg = fmt.Sprintf("Failed to parse YAML: %v", err)
		return r
	}

	if gvk.Kind != "Secret" {
		r.Msg = "YAML does not define a Secret resource"
		return r
	}

	secret, ok := obj.(*corev1.Secret)
	if !ok {
		r.Msg = "Failed to convert YAML to Secret"
		return r
	}

	if strings.ToLower(secret.GetName()) == strings.ToLower(originalName) && strings.ToLower(secret.GetNamespace()) == strings.ToLower(originalNamespace) {
		b, err := json.Marshal(obj)
		if err != nil {
			r.Msg = fmt.Sprintf("Failed to convert object to JSON: %v", err)
			return r
		}

		if err := s.patch(cs, originalNamespace, originalName, b); err != nil {
			r.Msg = fmt.Sprintf("Failed to patch Secret: %v", err)
			return r
		}
	} else {
		originalClient := cs.CoreV1().Secrets(originalNamespace)
		client := cs.CoreV1().Secrets(secret.GetNamespace())
		secret, err = client.Create(s.ctx, secret, metav1.CreateOptions{})
		if err != nil {
			r.Msg = fmt.Sprintf("Failed to create Secret: %v", err)
			return r
		}
		if err = originalClient.Delete(s.ctx, originalName, kubernetesDeleteOptions); err != nil {
			r.Msg = fmt.Sprintf("Failed to delete original Secret: %v", err)
			return r
		}
		r.Data = *secret
	}

	r.Msg = fmt.Sprintf("Updated Secret: %s", originalName)
	r.Success = true
	return r
}

func (s *secretsService) patch(clientset *kubernetes.Clientset, namespace, name string, patchData []byte) error {
	return retry.RetryOnConflict(retry.DefaultRetry, func() error {
		_, err := clientset.CoreV1().Secrets(namespace).Patch(
			context.TODO(),
			name,
			k8types.StrategicMergePatchType,
			patchData,
			metav1.PatchOptions{},
		)
		return err
	})
}

func (s *secretsService) Create(context string, input any) types.Response {
	var secret *corev1.Secret
	var err error

	r := types.NewResponse()

	switch t := input.(type) {
	case map[string]any:
		secret, err = s.getSecretFromCustomSecret(t)
		if err != nil {
			r.Msg = fmt.Sprintf("Failed to get resource from definition: %s", err.Error())
			return r
		}
	case string:
		secret, err = s.getSecretFromYamlWithCodec(t)
		if err != nil {
			r.Msg = fmt.Sprintf("Failed to create Secret from YAML: %s", err.Error())
			return r
		}
	default:
		r.Msg = "Input must be either a Secret definition object or YAML string"
		return r
	}

	cs, err := s.conf.GetClientSet(context)
	if err != nil {
		r.Msg = fmt.Sprintf("Failed to find context name: %s", err.Error())
		return r
	}

	namespace := secret.ObjectMeta.GetNamespace()
	if _, err := cs.CoreV1().Secrets(namespace).Create(s.ctx, secret, metav1.CreateOptions{}); err != nil {
		r.Msg = fmt.Sprintf("Failed to create resource: %s", err.Error())
		return r
	}

	r.Msg = fmt.Sprintf("Secret created: %s", secret.GetName())
	r.Success = true
	return r
}

func (s *secretsService) getSecretFromYamlWithCodec(yamlContent string) (*corev1.Secret, 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 != "Secret" {
		return nil, fmt.Errorf("YAML does not define a Secret resource, got: %s", gvk.Kind)
	}

	secret, ok := obj.(*corev1.Secret)
	if !ok {
		return nil, fmt.Errorf("failed to convert object to Secret")
	}

	return secret, nil
}