From ee6ac72dc7fac8337f6507e61fa96584b10421c7 Mon Sep 17 00:00:00 2001 From: karim-w Date: Thu, 15 Aug 2024 03:44:58 +0400 Subject: [PATCH] feat: added support for an interactive editor to edit kube secrets --- README.md | 40 +++++-- cmd/root.go | 33 +++--- models/secrets.go | 31 +++--- service/kubesecretes.go | 230 ++++++++++++++++++++++++++++++++++++---- 4 files changed, 278 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 105d38e..4ee51c6 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,58 @@ # ksec KSec is a command line tool to manage secrets in Kubernetes with the following functionallities: + - [x] Create a secret from an env file - [x] Append a secret to an existing secret -- [x] Get a secret from Kubernetes secrets +- [x] Get a secret from Kubernetes secrets - [x] Delete a secret from Kubernetes secrets - [x] List all secrets in a namespace - [x] Fill a file with a secret from Kubernetes secrets +- [x] Modify a secret in Kubernetes secrets using an editor ## Installation + to install Ksec use the following command: -` go install github.com/karim-w/ksec ` +`go install github.com/karim-w/ksec` ## Usage + ### Create a secret from an env file -` ksec -e <.env file path> -n -s ` + +`ksec -e <.env file path> -n -s ` this command will : + - create a secret from the env file and will add it to the kubernetes secrets -- create a yaml file with the env config map +- create a yaml file with the env config map + ### Append a secret to an existing secret -` ksec -w <.env file path> -n -s -a ` + +`ksec -w <.env file path> -n -s -a` this commmand will add a secret to a existing secret in kubernetes secrets + ### List all secrets in a Kubernetes secret -` ksec -l -n -s ` + +`ksec -l -n -s ` this command will retrieve the secrets embedde in a kubernetes secrets + ### Get a secret from Kubernetes secrets -` ksec -g -n -s -k ` + +`ksec -g -n -s -k ` this command will retrieve a the value of secret within an existing kubernetes secret + ### Delete a secret from Kubernetes secrets -` ksec -d -n -s -k ` + +`ksec -d -n -s -k ` this command will delete a secret from an existing kubernetes secret ### Fill a file with secrets from Kubernetes secrets -` ksec -f -n -s ` +`ksec -f -n -s ` + +### Modify a secret in Kubernetes secrets using an editor + +`ksec -m -n -s ` + +### Specify the file format + +`ksec -m -n -s -F ` diff --git a/cmd/root.go b/cmd/root.go index ed98657..6e88d35 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -18,23 +18,28 @@ var RootCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { // Do Stuff Here c := &models.Secrets{ - Namespace: cmd.Flag("namespace").Value.String(), - Secret: cmd.Flag("secret").Value.String(), - Set: cmd.Flag("set").Value.String() == "true", - Key: cmd.Flag("key").Value.String(), - Value: cmd.Flag("value").Value.String(), - Get: cmd.Flag("get").Value.String() == "true", - Delete: cmd.Flag("delete").Value.String() == "true", - List: cmd.Flag("list").Value.String() == "true", - All: cmd.Flag("all").Value.String() == "true", - EnvPath: cmd.Flag("env").Value.String(), - FillPath: cmd.Flag("fill").Value.String(), + Namespace: cmd.Flag("namespace").Value.String(), + Secret: cmd.Flag("secret").Value.String(), + Set: cmd.Flag("set").Value.String() == "true", + Key: cmd.Flag("key").Value.String(), + Value: cmd.Flag("value").Value.String(), + Get: cmd.Flag("get").Value.String() == "true", + Delete: cmd.Flag("delete").Value.String() == "true", + List: cmd.Flag("list").Value.String() == "true", + All: cmd.Flag("all").Value.String() == "true", + EnvPath: cmd.Flag("env").Value.String(), + FillPath: cmd.Flag("fill").Value.String(), + Modify: cmd.Flag("modify").Value.String() == "true", + FileFormat: cmd.Flag("file-format").Value.String(), } service.KubectlSecretsSvc(c) }, } -var Verbose bool -var Source string + +var ( + Verbose bool + Source string +) func Execute() { err := RootCmd.Execute() @@ -57,4 +62,6 @@ func init() { RootCmd.PersistentFlags().BoolP("all", "a", false, "list all secrets") RootCmd.PersistentFlags().StringP("env", "e", "", "Create from a .env file") RootCmd.PersistentFlags().StringP("fill", "f", "", "Fill a file with secrets") + RootCmd.PersistentFlags().BoolP("modify", "m", false, "Modify a secret in an interactive mode") + RootCmd.PersistentFlags().StringP("file-format", "F", "yaml", "File format") } diff --git a/models/secrets.go b/models/secrets.go index faf6f35..d842286 100644 --- a/models/secrets.go +++ b/models/secrets.go @@ -1,16 +1,23 @@ package models type Secrets struct { - Namespace string - Secret string - Set bool - Key string - Value string - Get bool - Delete bool - List bool - All bool - Operation string - EnvPath string - FillPath string + Namespace string + Secret string + Set bool + Key string + Value string + Get bool + Delete bool + List bool + All bool + Operation string + EnvPath string + FillPath string + Modify bool + FileFormat string +} + +var SupportedFormats = map[string]struct{}{ + "yaml": {}, + "json": {}, } diff --git a/service/kubesecretes.go b/service/kubesecretes.go index 43623e0..1dd643e 100644 --- a/service/kubesecretes.go +++ b/service/kubesecretes.go @@ -3,13 +3,18 @@ package service import ( "bufio" "context" + "crypto/md5" + "encoding/hex" + "encoding/json" "fmt" "log" "os" + "os/exec" "strings" "sync" "github.com/karim-w/ksec/models" + "gopkg.in/yaml.v3" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -17,15 +22,20 @@ import ( "k8s.io/client-go/tools/clientcmd" ) -var instance *kubernetes.Clientset -var mtx sync.Mutex +var ( + instance *kubernetes.Clientset + mtx sync.Mutex +) func boot() *kubernetes.Clientset { mtx.Lock() defer mtx.Unlock() if instance == nil { rules := clientcmd.NewDefaultClientConfigLoadingRules() - kubeconfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, &clientcmd.ConfigOverrides{}) + kubeconfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + rules, + &clientcmd.ConfigOverrides{}, + ) config, err := kubeconfig.ClientConfig() if err != nil { panic(err) @@ -48,6 +58,10 @@ func KubectlSecretsSvc(conf *models.Secrets) { handleFillPath(conf.FillPath, conf.Namespace, conf.Secret) return } + if conf.Modify { + modifyKubeSecret(conf.Namespace, conf.Secret, conf.FileFormat) + return + } if conf.Set { addKubeSecret(conf.Namespace, conf.Secret, conf.Key, conf.Value) return @@ -67,7 +81,9 @@ func KubectlSecretsSvc(conf *models.Secrets) { } func getkubeSecretValue(namespace string, secret string, key string) { - s, err := GetKubeClient().CoreV1().Secrets(namespace).Get(context.TODO(), secret, metav1.GetOptions{}) + s, err := GetKubeClient().CoreV1(). + Secrets(namespace). + Get(context.TODO(), secret, metav1.GetOptions{}) if err != nil { println(err.Error()) return @@ -76,7 +92,9 @@ func getkubeSecretValue(namespace string, secret string, key string) { } func listKubeSecrets(namespace string, secret string) { - s, err := GetKubeClient().CoreV1().Secrets(namespace).Get(context.TODO(), secret, metav1.GetOptions{}) + s, err := GetKubeClient().CoreV1(). + Secrets(namespace). + Get(context.TODO(), secret, metav1.GetOptions{}) if err != nil { println(err.Error()) return @@ -87,15 +105,19 @@ func listKubeSecrets(namespace string, secret string) { } func addKubeSecret(namespace string, secret string, key string, value string) { - //get secrets - s, err := GetKubeClient().CoreV1().Secrets(namespace).Get(context.TODO(), secret, metav1.GetOptions{}) + // get secrets + s, err := GetKubeClient().CoreV1(). + Secrets(namespace). + Get(context.TODO(), secret, metav1.GetOptions{}) if err != nil { println(err.Error()) return } s.Data[key] = []byte(value) - //update secret - s, err = GetKubeClient().CoreV1().Secrets(namespace).Update(context.TODO(), s, metav1.UpdateOptions{}) + // update secret + s, err = GetKubeClient().CoreV1(). + Secrets(namespace). + Update(context.TODO(), s, metav1.UpdateOptions{}) if err != nil { println(err.Error()) return @@ -107,14 +129,18 @@ func addKubeSecret(namespace string, secret string, key string, value string) { func deleteKubeSecret(namespace string, secret string, key string) { // get secret - s, err := GetKubeClient().CoreV1().Secrets(namespace).Get(context.TODO(), secret, metav1.GetOptions{}) + s, err := GetKubeClient().CoreV1(). + Secrets(namespace). + Get(context.TODO(), secret, metav1.GetOptions{}) if err != nil { println(err.Error()) return } delete(s.Data, key) // delete Key through update - s, err = GetKubeClient().CoreV1().Secrets(namespace).Update(context.TODO(), s, metav1.UpdateOptions{}) + s, err = GetKubeClient().CoreV1(). + Secrets(namespace). + Update(context.TODO(), s, metav1.UpdateOptions{}) if err != nil { println(err.Error()) return @@ -137,9 +163,9 @@ func extractValuesFromEnv(path string, namespace string, secret string) *map[str for scanner.Scan() { line := scanner.Text() if line != "" { - //get location of first "=" + // get location of first "=" index := strings.Index(line, "=") - //get key and value + // get key and value key := line[:index] value := line[index+1:] sList[key] = []byte(value) @@ -153,7 +179,7 @@ func extractValuesFromEnv(path string, namespace string, secret string) *map[str func handleEnvPath(path string, namespace string, secret string) { vals := extractValuesFromEnv(path, namespace, secret) - //Create secret + // Create secret s, err := GetKubeClient().CoreV1().Secrets(namespace).Create( context.TODO(), &v1.Secret{ @@ -174,16 +200,21 @@ func handleEnvPath(path string, namespace string, secret string) { } func generateDecalrationFile(secret string, vals *map[string][]byte) { - //create file + // create file file, err := os.Create(secret + ".yaml") if err != nil { log.Fatal(err) } defer file.Close() overall := "env:" - //write to file - for k, _ := range *vals { - line := fmt.Sprintf("\n- name: %s\n\tvalueFrom:\n\t\tsecretKeyRef:\n\t\t\tname: %s\n\t\t\tkey: %s", k, secret, k) + // write to file + for k := range *vals { + line := fmt.Sprintf( + "\n- name: %s\n\tvalueFrom:\n\t\tsecretKeyRef:\n\t\t\tname: %s\n\t\t\tkey: %s", + k, + secret, + k, + ) overall += line } _, err = file.WriteString(overall) @@ -194,7 +225,9 @@ func generateDecalrationFile(secret string, vals *map[string][]byte) { func handleFillPath(path string, namespace string, secret string) { // fetch secrets - s, err := GetKubeClient().CoreV1().Secrets(namespace).Get(context.TODO(), secret, metav1.GetOptions{}) + s, err := GetKubeClient().CoreV1(). + Secrets(namespace). + Get(context.TODO(), secret, metav1.GetOptions{}) if err != nil { println(err.Error()) return @@ -203,19 +236,172 @@ func handleFillPath(path string, namespace string, secret string) { for k, v := range s.Data { secretsMap[k] = v } - //create the file + // create the file handleGenerateFileWithSecrets(path, &secretsMap) } func handleGenerateFileWithSecrets(path string, secrets *map[string][]byte) { - //create file + // create file file, err := os.Create(path) if err != nil { log.Fatal(err) } defer file.Close() - //write to file + // write to file for k, v := range *secrets { file.WriteString(k + "=" + string(v) + "\n") } } + +func modifyKubeSecret(namespace, secret, format string) { + // check if file type is supported + _, ok := models.SupportedFormats[format] + if !ok { + fmt.Println(format + " format is not supported") + return + } + // check if secret exists, if not create it + s, err := GetKubeClient().CoreV1(). + Secrets(namespace). + Get(context.TODO(), secret, metav1.GetOptions{}) + if err != nil { + if err.Error() != `secrets "`+secret+`" not found` { + println(err.Error()) + return + } + + // Create generic secret + s, err = GetKubeClient().CoreV1().Secrets(namespace).Create( + context.TODO(), + &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secret, + Namespace: namespace, + }, + Data: map[string][]byte{ + "key": []byte("value"), + }, + }, + metav1.CreateOptions{}, + ) + } + + m := map[string]interface{}{} + + for k, v := range s.Data { + m[k] = string(v) + } + + var out []byte + + switch format { + case "yaml": + out, err = yaml.Marshal(m) + case "json": + out, err = json.MarshalIndent(m, "", " ") + } + + if err != nil { + println(err.Error()) + return + } + + file, err := os.Create("." + secret + "." + format) + if err != nil { + println(err.Error()) + return + } + + defer func() { + defer file.Close() + // delete file + err := os.Remove("." + secret + "." + format) + if err != nil { + println(err.Error()) + } + }() + + before_hash := md5.Sum(out) + before_check_sum := hex.EncodeToString(before_hash[:]) + + _, err = file.Write(out) + if err != nil { + println(err.Error()) + return + } + + // open file in editor + err = openFileInEditor("." + secret + "." + format) + if err != nil { + println(err.Error()) + return + } + + // read file + file, err = os.Open("." + secret + "." + format) + if err != nil { + println(err.Error()) + return + } + + defer file.Close() + + // read file whole + byts := make([]byte, 1024) + n, err := file.Read(byts) + if err != nil { + println(err.Error()) + return + } + + after_hash := md5.Sum(byts[:n]) + after_check_sum := hex.EncodeToString(after_hash[:]) + + if before_check_sum == after_check_sum { + println("No changes detected") + return + } + + dat := make(map[string]string) + + // unmarshal + switch format { + case "yaml": + err = yaml.Unmarshal(byts[:n], &dat) + case "json": + err = json.Unmarshal(byts[:n], &dat) + } + + if err != nil { + println(err.Error()) + return + } + + // check if there is a change + + // update secret + s.Data = make(map[string][]byte) + for k, v := range dat { + s.Data[k] = []byte(v) + } + + _, err = GetKubeClient().CoreV1(). + Secrets(namespace). + Update(context.TODO(), s, metav1.UpdateOptions{}) + if err != nil { + println(err.Error()) + return + } +} + +func openFileInEditor(path string) error { + editor := os.Getenv("EDITOR") + if editor == "" { + editor = "vim" + } + cmd := exec.Command(editor, path) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +}