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
}