From 36701fb6ed7ab5d4f82d54ddd1e36a2cb3406a2c Mon Sep 17 00:00:00 2001 From: Marcos Lorenzo Date: Thu, 11 Apr 2024 13:51:54 +0200 Subject: [PATCH 1/4] Add support for `Opaque` secrets & avoid replicated ones --- cmd/kubectl-view-cert/model.go | 1 + cmd/kubectl-view-cert/root.go | 74 ++++++++++++++++++++++------------ internal/parse/parse.go | 21 ++++++---- 3 files changed, 62 insertions(+), 34 deletions(-) diff --git a/cmd/kubectl-view-cert/model.go b/cmd/kubectl-view-cert/model.go index d369048..cd7c901 100644 --- a/cmd/kubectl-view-cert/model.go +++ b/cmd/kubectl-view-cert/model.go @@ -8,6 +8,7 @@ import ( type Certificate struct { SecretName string Namespace string + Type string Version int SerialNumber string Issuer string diff --git a/cmd/kubectl-view-cert/root.go b/cmd/kubectl-view-cert/root.go index 1e9dc26..8da6260 100644 --- a/cmd/kubectl-view-cert/root.go +++ b/cmd/kubectl-view-cert/root.go @@ -182,7 +182,7 @@ func run(command *cobra.Command, args []string) error { } if parsedFlags.secretName != "" { - datas, secretKeys, err := getData(ctx, parsedFlags.secretName, ns, parsedFlags.secretKey, ri) + datas, secretKeys, err := getData(ctx, parsedFlags.secretName, ns, parsedFlags.secretKey, ri, parsedFlags.showCaCert) if err != nil { return err } @@ -200,7 +200,7 @@ func run(command *cobra.Command, args []string) error { } } } else { - datas, err := getDatas(ctx, ri) + datas, err := getDatas(ctx, ri, parsedFlags.showCaCert) if err != nil { return err } @@ -228,25 +228,40 @@ func run(command *cobra.Command, args []string) error { return nil } -func getDatas(ctx context.Context, ri dynamic.ResourceInterface) ([]*Certificate, error) { +func getDatas(ctx context.Context, ri dynamic.ResourceInterface, showCa bool) ([]*Certificate, error) { datas := make([]*Certificate, 0) tlsSecrets, err := ri.List(ctx, v1.ListOptions{FieldSelector: "type=kubernetes.io/tls"}) if err != nil { - return datas, fmt.Errorf("failed to get secrets: %w", err) - } - - for _, tlsSecret := range tlsSecrets.Items { - certData, caCertData, _, err := parseData(tlsSecret.GetNamespace(), tlsSecret.GetName(), tlsSecret.Object, "", false) - if err != nil { - return datas, err + return datas, fmt.Errorf("failed to get 'kubernetes.io/tls' secrets: %w", err) + } + OpaqueSecrets, errOpaque := ri.List(ctx, v1.ListOptions{FieldSelector: "type=Opaque"}) + if errOpaque != nil { + return datas, fmt.Errorf("failed to get 'Opaque' secrets: %w", err) + } + secrets := append(tlsSecrets.Items, OpaqueSecrets.Items...) + + var is_replicated bool + for _, secret := range secrets { + is_replicated = false + certData, caCertData, _ := parseData(secret.GetNamespace(), secret.GetName(), secret.Object, "", false, showCa) + for annotation_name := range secret.GetAnnotations() { + if annotation_name == "replicator.v1.mittwald.de/replicated-at" { + is_replicated = true + klog.V(2).Infoln("msg", "skipping secret replicated from another namespace '"+secret.GetNamespace()+"/"+secret.GetName()+"'") + } + continue + } + if is_replicated { + continue } - if certData != nil { + klog.V(1).Infoln("msg", "adding certificate '"+certData.Subject+"'", "secret", "'"+secret.GetNamespace()+"/"+secret.GetName()+"'") datas = append(datas, certData) } if caCertData != nil { + klog.V(1).Infoln("msg", "adding CA certificate '"+caCertData.Subject+"'", "secret", "'"+secret.GetNamespace()+"/"+secret.GetName()+"'") datas = append(datas, caCertData) } } @@ -254,27 +269,24 @@ func getDatas(ctx context.Context, ri dynamic.ResourceInterface) ([]*Certificate return datas, nil } -func getData(ctx context.Context, secretName, ns, secretKey string, ri dynamic.ResourceInterface) ([]*Certificate, *[]string, error) { +func getData(ctx context.Context, secretName, ns, secretKey string, ri dynamic.ResourceInterface, showCA bool) ([]*Certificate, *[]string, error) { datas := make([]*Certificate, 0) secret, err := ri.Get(ctx, secretName, v1.GetOptions{}) if err != nil { - return datas, nil, fmt.Errorf("failed to get secret with name %s: %w", secretName, err) + return datas, nil, fmt.Errorf("failed to get secret '%s/%s': %w", ns, secretName, err) } - certData, caCertData, secretKeys, err := parseData(ns, secretName, secret.Object, secretKey, true) - if err != nil { - return datas, nil, err + certData, caCertData, secretKeys := parseData(ns, secretName, secret.Object, secretKey, true, showCA) + if certData == nil { + return datas, nil, fmt.Errorf("failed to get secret '%s/%s'", ns, secretName) } if secretKeys != nil { return datas, secretKeys, nil } - if certData != nil { - datas = append(datas, certData) - } - + datas = append(datas, certData) if caCertData != nil { datas = append(datas, caCertData) } @@ -324,25 +336,34 @@ func getResourceInterface(allNs bool, secretName string) (string, dynamic.Resour return ns, ri, nil } -func parseData(ns, secretName string, data map[string]interface{}, secretKey string, listKeys bool) (certData, caCertData *Certificate, secretKeys *[]string, err error) { - secretCertData, err := parse.NewCertificateData(ns, secretName, data, secretKey, listKeys) +func parseData(ns, secretName string, data map[string]interface{}, secretKey string, listKeys bool, showCA bool) (certData, caCertData *Certificate, secretKeys *[]string) { + secretCertData, err := parse.NewCertificateData(ns, secretName, data, secretKey, listKeys, showCA) if err != nil { - return nil, nil, nil, fmt.Errorf("failed to parse secret with name %s in namespace %s %v", secretName, ns, err) + klog.V(1).Infoln("msg", "failed to parse secret '"+ns+"/"+secretName+"'", "err", err) + return nil, nil, nil + } + + if secretCertData == nil { + klog.V(1).Infoln("msg", "no 'data' key found in secret '"+ns+"/"+secretName+"'", "err", err) + return nil, nil, nil } if len(secretCertData.SecretKeys) > 0 { - return nil, nil, &secretCertData.SecretKeys, nil + klog.V(1).Infoln("msg", "return '"+ns+"/"+secretName+"'") + return nil, nil, &secretCertData.SecretKeys } parsedCerts, err := secretCertData.ParseCertificates() if err != nil { - return nil, nil, nil, fmt.Errorf("unable to parse certificates for secret %s in namespace %s %v", secretName, ns, err) + klog.V(1).Infoln("msg", "unable to parse certificates for secret '"+ns+"/"+secretName+"'", "err", err) + return nil, nil, nil } if parsedCerts.Certificate != nil { certData = &Certificate{ SecretName: parsedCerts.SecretName, Namespace: parsedCerts.Namespace, + Type: secretCertData.Type, IsCA: parsedCerts.Certificate.IsCA, Issuer: parsedCerts.Certificate.Issuer.String(), SerialNumber: fmt.Sprintf("%x", parsedCerts.Certificate.SerialNumber), @@ -359,6 +380,7 @@ func parseData(ns, secretName string, data map[string]interface{}, secretKey str caCertData = &Certificate{ SecretName: parsedCerts.SecretName, Namespace: parsedCerts.Namespace, + Type: secretCertData.Type, IsCA: parsedCerts.CaCertificate.IsCA, Issuer: parsedCerts.CaCertificate.Issuer.String(), SerialNumber: fmt.Sprintf("%x", parsedCerts.CaCertificate.SerialNumber), @@ -371,5 +393,5 @@ func parseData(ns, secretName string, data map[string]interface{}, secretKey str } } - return certData, caCertData, nil, err + return certData, caCertData, nil } diff --git a/internal/parse/parse.go b/internal/parse/parse.go index f997fbe..4a3dc8b 100644 --- a/internal/parse/parse.go +++ b/internal/parse/parse.go @@ -11,6 +11,7 @@ import ( type CertificateData struct { SecretName string Namespace string + Type string Certificate string CaCertificate string SecretKeys []string @@ -25,7 +26,11 @@ type ParsedCertificateData struct { } // NewCertificateData takes secret data and extracts base64 pem strings -func NewCertificateData(ns, secretName string, data map[string]interface{}, secretKey string, listKeys bool) (*CertificateData, error) { +func NewCertificateData(ns, secretName string, data map[string]interface{}, secretKey string, listKeys bool, showCa bool) (*CertificateData, error) { + _, ok := data["data"] + if !ok { + return nil, nil + } certsMap := data["data"].(map[string]interface{}) certData := CertificateData{ @@ -33,8 +38,6 @@ func NewCertificateData(ns, secretName string, data map[string]interface{}, secr Namespace: ns, } - // TODO: Check if certsMap is nil ? - if secretKey != "" { if val, ok := certsMap[secretKey]; ok { certData.Certificate = fmt.Sprintf("%v", val) @@ -45,15 +48,17 @@ func NewCertificateData(ns, secretName string, data map[string]interface{}, secr secretType := fmt.Sprintf("%v", data["type"]) - if secretType == "kubernetes.io/tls" { // nolint gosec + if secretType == "kubernetes.io/tls" || + secretType == "Opaque" { // nolint gosec if val, ok := certsMap["tls.crt"]; ok { certData.Certificate = fmt.Sprintf("%v", val) } - - if val, ok := certsMap["ca.crt"]; ok { - certData.CaCertificate = fmt.Sprintf("%v", val) + if showCa { + if val, ok := certsMap["ca.crt"]; ok { + certData.CaCertificate = fmt.Sprintf("%v", val) + } } - + certData.Type = secretType return &certData, nil } From 093c882ad4541d7257dc2dd7063086024a38b0eb Mon Sep 17 00:00:00 2001 From: Marcos Lorenzo Date: Thu, 11 Apr 2024 14:05:49 +0200 Subject: [PATCH 2/4] Add support for YAML output --- cmd/kubectl-view-cert/root.go | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/cmd/kubectl-view-cert/root.go b/cmd/kubectl-view-cert/root.go index 8da6260..8578445 100644 --- a/cmd/kubectl-view-cert/root.go +++ b/cmd/kubectl-view-cert/root.go @@ -11,6 +11,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" + "gopkg.in/yaml.v2" "github.com/lmolas/kubectl-view-cert/internal/parse" @@ -27,6 +28,7 @@ const ( expiredFlag = "expired" showCaCertFlag = "show-ca" expiredDaysFromNowFlag = "expired-days-from-now" + yamlOutput = "yaml" ) type parsedFlags struct { @@ -34,6 +36,7 @@ type parsedFlags struct { expired bool showCaCert bool expiredInDays int + yamlOutput bool secretName string secretKey string } @@ -99,6 +102,7 @@ func init() { rootCmd.Flags().BoolP(expiredFlag, "E", false, "Show only expired certificates") rootCmd.Flags().BoolP(showCaCertFlag, "S", false, "Show CA certificates") rootCmd.Flags().IntP(expiredDaysFromNowFlag, "D", 0, "Show expired certificates at date in future (now plus number of days)") + rootCmd.Flags().BoolP(yamlOutput, "Y", false, "YAML output") cf.AddFlags(rootCmd.Flags()) if err := flag.Set("logtostderr", "true"); err != nil { @@ -147,6 +151,11 @@ func parseFlagsAndArguments(command *cobra.Command, args []string) parsedFlags { expiredInDays = 0 } + yamlOutput, err := command.Flags().GetBool(yamlOutput) + if err != nil { + yamlOutput = false + } + var secretName, secretKey string if len(args) > 0 { secretName = args[0] @@ -156,7 +165,7 @@ func parseFlagsAndArguments(command *cobra.Command, args []string) parsedFlags { secretKey = args[1] } - return parsedFlags{allNs, expired, showCaCert, expiredInDays, secretName, secretKey} + return parsedFlags{allNs, expired, showCaCert, expiredInDays, yamlOutput, secretName, secretKey} } // nolint gocognit // Better readability in one block @@ -194,7 +203,7 @@ func run(command *cobra.Command, args []string) error { } } else { // Display - err = displayDatas(datas) + err = displayDatas(datas, parsedFlags.yamlOutput) if err != nil { return err } @@ -219,7 +228,7 @@ func run(command *cobra.Command, args []string) error { } // Display - err = displayDatas(filteredDatas) + err = displayDatas(filteredDatas, parsedFlags.yamlOutput) if err != nil { return err } @@ -294,10 +303,15 @@ func getData(ctx context.Context, secretName, ns, secretKey string, ri dynamic.R return datas, nil, nil } -func displayDatas(datas []*Certificate) error { - encoder := json.NewEncoder(os.Stdout) - encoder.SetIndent("", " ") - return encoder.Encode(&datas) +func displayDatas(datas []*Certificate, yamlOutput bool) error { + if yamlOutput { + encoder := yaml.NewEncoder(os.Stdout) + return encoder.Encode(&datas) + } else { + encoder := json.NewEncoder(os.Stdout) + encoder.SetIndent("", " ") + return encoder.Encode(&datas) + } } func getResourceInterface(allNs bool, secretName string) (string, dynamic.ResourceInterface, error) { From 1068732fb59ac23acef2ad772cf8c0fb0334116d Mon Sep 17 00:00:00 2001 From: Marcos Lorenzo <11718206+mlorenzo-stratio@users.noreply.github.com> Date: Fri, 26 Apr 2024 11:48:13 +0200 Subject: [PATCH 3/4] Feature: Use env vars to set list of `Opaque` secret keys Signed-off-by: Marcos Lorenzo <11718206+mlorenzo-stratio@users.noreply.github.com> --- .krew.yaml => .krew.yaml.not | 0 cmd/kubectl-view-cert/model.go | 1 + cmd/kubectl-view-cert/root.go | 30 ++++++++++++++----------- internal/parse/parse.go | 40 ++++++++++++++++++++++++++++------ 4 files changed, 51 insertions(+), 20 deletions(-) rename .krew.yaml => .krew.yaml.not (100%) diff --git a/.krew.yaml b/.krew.yaml.not similarity index 100% rename from .krew.yaml rename to .krew.yaml.not diff --git a/cmd/kubectl-view-cert/model.go b/cmd/kubectl-view-cert/model.go index cd7c901..95ac775 100644 --- a/cmd/kubectl-view-cert/model.go +++ b/cmd/kubectl-view-cert/model.go @@ -8,6 +8,7 @@ import ( type Certificate struct { SecretName string Namespace string + SecretKey string Type string Version int SerialNumber string diff --git a/cmd/kubectl-view-cert/root.go b/cmd/kubectl-view-cert/root.go index 8578445..8dc19e9 100644 --- a/cmd/kubectl-view-cert/root.go +++ b/cmd/kubectl-view-cert/root.go @@ -1,5 +1,6 @@ package main +// nolint depguard import ( "context" "encoding/json" @@ -248,20 +249,22 @@ func getDatas(ctx context.Context, ri dynamic.ResourceInterface, showCa bool) ([ if errOpaque != nil { return datas, fmt.Errorf("failed to get 'Opaque' secrets: %w", err) } - secrets := append(tlsSecrets.Items, OpaqueSecrets.Items...) + secrets := tlsSecrets.Items + secrets = append(secrets, OpaqueSecrets.Items...) - var is_replicated bool + var isReplicated bool for _, secret := range secrets { - is_replicated = false + isReplicated = false certData, caCertData, _ := parseData(secret.GetNamespace(), secret.GetName(), secret.Object, "", false, showCa) - for annotation_name := range secret.GetAnnotations() { - if annotation_name == "replicator.v1.mittwald.de/replicated-at" { - is_replicated = true + for annotationName := range secret.GetAnnotations() { + if annotationName == "replicator.v1.mittwald.de/replicated-at" { + isReplicated = true + // nolint gomnd klog.V(2).Infoln("msg", "skipping secret replicated from another namespace '"+secret.GetNamespace()+"/"+secret.GetName()+"'") } continue } - if is_replicated { + if isReplicated { continue } if certData != nil { @@ -307,11 +310,10 @@ func displayDatas(datas []*Certificate, yamlOutput bool) error { if yamlOutput { encoder := yaml.NewEncoder(os.Stdout) return encoder.Encode(&datas) - } else { - encoder := json.NewEncoder(os.Stdout) - encoder.SetIndent("", " ") - return encoder.Encode(&datas) } + encoder := json.NewEncoder(os.Stdout) + encoder.SetIndent("", " ") + return encoder.Encode(&datas) } func getResourceInterface(allNs bool, secretName string) (string, dynamic.ResourceInterface, error) { @@ -350,8 +352,8 @@ func getResourceInterface(allNs bool, secretName string) (string, dynamic.Resour return ns, ri, nil } -func parseData(ns, secretName string, data map[string]interface{}, secretKey string, listKeys bool, showCA bool) (certData, caCertData *Certificate, secretKeys *[]string) { - secretCertData, err := parse.NewCertificateData(ns, secretName, data, secretKey, listKeys, showCA) +func parseData(ns, secretName string, data map[string]interface{}, secretKey string, listKeys, showCA bool) (certData, caCertData *Certificate, secretKeys *[]string) { + secretCertData, secretKeysList, err := parse.NewCertificateData(ns, secretName, data, secretKey, listKeys, showCA) if err != nil { klog.V(1).Infoln("msg", "failed to parse secret '"+ns+"/"+secretName+"'", "err", err) return nil, nil, nil @@ -378,6 +380,7 @@ func parseData(ns, secretName string, data map[string]interface{}, secretKey str SecretName: parsedCerts.SecretName, Namespace: parsedCerts.Namespace, Type: secretCertData.Type, + SecretKey: secretKeysList[0], IsCA: parsedCerts.Certificate.IsCA, Issuer: parsedCerts.Certificate.Issuer.String(), SerialNumber: fmt.Sprintf("%x", parsedCerts.Certificate.SerialNumber), @@ -394,6 +397,7 @@ func parseData(ns, secretName string, data map[string]interface{}, secretKey str caCertData = &Certificate{ SecretName: parsedCerts.SecretName, Namespace: parsedCerts.Namespace, + SecretKey: secretKeysList[1], Type: secretCertData.Type, IsCA: parsedCerts.CaCertificate.IsCA, Issuer: parsedCerts.CaCertificate.Issuer.String(), diff --git a/internal/parse/parse.go b/internal/parse/parse.go index 4a3dc8b..6302ae8 100644 --- a/internal/parse/parse.go +++ b/internal/parse/parse.go @@ -5,6 +5,8 @@ import ( "encoding/base64" "encoding/pem" "fmt" + "os" + "strings" ) // CertificateData struct contains base64 pem data @@ -26,10 +28,14 @@ type ParsedCertificateData struct { } // NewCertificateData takes secret data and extracts base64 pem strings -func NewCertificateData(ns, secretName string, data map[string]interface{}, secretKey string, listKeys bool, showCa bool) (*CertificateData, error) { +// nolint gocognit +func NewCertificateData(ns, secretName string, data map[string]interface{}, secretKey string, listKeys, showCa bool) (*CertificateData, []string, error) { _, ok := data["data"] + var keysList []string + returnCertPemKey := "tls.crt" + returnCaPemKey := "ca.crt" if !ok { - return nil, nil + return nil, nil, nil } certsMap := data["data"].(map[string]interface{}) @@ -43,23 +49,43 @@ func NewCertificateData(ns, secretName string, data map[string]interface{}, secr certData.Certificate = fmt.Sprintf("%v", val) } - return &certData, nil + return &certData, nil, nil } secretType := fmt.Sprintf("%v", data["type"]) + secretCrtPemKeyList := strings.Split(os.Getenv("CRT_PEM_KEY_LIST"), ",") + secretCaPemKeyList := strings.Split(os.Getenv("CA_PEM_KEY_LIST"), ",") + // nolint gosec if secretType == "kubernetes.io/tls" || - secretType == "Opaque" { // nolint gosec + secretType == "Opaque" { if val, ok := certsMap["tls.crt"]; ok { certData.Certificate = fmt.Sprintf("%v", val) + } else { + for _, crtPemKey := range secretCrtPemKeyList { + if val, ok := certsMap[crtPemKey]; ok { + certData.Certificate = fmt.Sprintf("%v", val) + returnCertPemKey = crtPemKey + break + } + } } if showCa { if val, ok := certsMap["ca.crt"]; ok { certData.CaCertificate = fmt.Sprintf("%v", val) + } else { + for _, caPemKey := range secretCaPemKeyList { + if val, ok := certsMap[caPemKey]; ok { + certData.CaCertificate = fmt.Sprintf("%v", val) + returnCaPemKey = caPemKey + break + } + } } } + keysList = append(keysList, returnCertPemKey, returnCaPemKey) certData.Type = secretType - return &certData, nil + return &certData, keysList, nil } if listKeys && certsMap != nil && len(certsMap) > 0 { @@ -71,10 +97,10 @@ func NewCertificateData(ns, secretName string, data map[string]interface{}, secr i++ } - return &certData, nil + return &certData, nil, nil } - return nil, fmt.Errorf("unsupported secret type %s", secretType) + return nil, nil, fmt.Errorf("unsupported secret type %s", secretType) } // ParseCertificates method parses each base64 pem strings and creates x509 certificates From bb93ef9be673416ff706428b0e206c2f1b08bc63 Mon Sep 17 00:00:00 2001 From: Marcos Lorenzo <11718206+mlorenzo-stratio@users.noreply.github.com> Date: Fri, 26 Apr 2024 18:29:22 +0200 Subject: [PATCH 4/4] Undo unintentional rename Signed-off-by: Marcos Lorenzo <11718206+mlorenzo-stratio@users.noreply.github.com> --- .krew.yaml.not => .krew.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .krew.yaml.not => .krew.yaml (100%) diff --git a/.krew.yaml.not b/.krew.yaml similarity index 100% rename from .krew.yaml.not rename to .krew.yaml