Implement regex support for secret-type flag

This commit is contained in:
Nuckal777
2024-04-26 17:20:21 +02:00
committed by Thibault VINCENT
parent ab35b9e0ca
commit 4cfad6fe50
5 changed files with 70 additions and 36 deletions

View File

@@ -57,8 +57,8 @@ func main() {
kubeConfig := getopt.StringLong("kubeconfig", 0, "", "Path to the kubeconfig file to use for requests. Takes precedence over the KUBECONFIG environment variable, and default path (~/.kube/config).", "path")
kubeSecretTypes := stringArrayFlag{}
getopt.FlagLong(&kubeSecretTypes, "secret-type", 's', "one or more kubernetes secret type & key to watch (e.g. \"kubernetes.io/tls:tls.crt\"")
kubeSecretTypePatterns := stringArrayFlag{}
getopt.FlagLong(&kubeSecretTypePatterns, "secret-type", 's', "one or more kubernetes secret type & key to watch (e.g. \"kubernetes.io/tls:tls.crt\"")
kubeConfigMapKeys := stringArrayFlag{}
getopt.FlagLong(&kubeConfigMapKeys, "configmap-keys", 'c', "keys in configmaps to watch")
@@ -118,6 +118,15 @@ func main() {
slog.Error("Cannot set GOMEMLIMIT with automemlimit", "reason", err.Error())
}
kubeSecretTypes := make([]internal.KubeSecretType, 0)
for _, pattern := range kubeSecretTypePatterns {
kst, err := internal.ParseSecretType(pattern)
if err != nil {
log.Fatal("failed to parse --secret-type argument: ", err)
}
kubeSecretTypes = append(kubeSecretTypes, kst)
}
exporter := internal.Exporter{
ListenAddress: *listenAddress,
SystemdSocket: *systemdSocket,

View File

@@ -12,6 +12,7 @@ import (
"os"
"path"
"path/filepath"
"regexp"
"strings"
"time"
@@ -38,8 +39,8 @@ type Exporter struct {
ExposeRelativeMetrics bool
ExposeErrorMetrics bool
ExposeLabels []string
KubeSecretTypes []string
ConfigMapKeys []string
KubeSecretTypes []KubeSecretType
KubeIncludeNamespaces []string
KubeExcludeNamespaces []string
KubeIncludeLabels []string
@@ -53,6 +54,33 @@ type Exporter struct {
configMapsCache *cache.Cache
}
type KubeSecretType struct {
Type string
Regexp *regexp.Regexp
}
func ParseSecretType(s string) (KubeSecretType, error) {
ty, pattern, found := strings.Cut(s, ":")
if !found {
return KubeSecretType{}, errors.New("secret type needs to contain at least a single colon")
}
compiled, err := regexp.Compile(pattern)
if err != nil {
return KubeSecretType{}, err
}
return KubeSecretType{
Type: ty,
Regexp: compiled,
}, nil
}
func (kst *KubeSecretType) Matches(secretType, key string) bool {
if kst.Type != secretType {
return false
}
return kst.Regexp.MatchString(key)
}
// ListenAndServe : Convenience function to start exporter
func (exporter *Exporter) ListenAndServe() error {
exporter.DiscoverCertificates()

View File

@@ -17,6 +17,7 @@ import (
"os"
"path"
"path/filepath"
"regexp"
"runtime"
"strings"
"testing"
@@ -991,7 +992,7 @@ func checkLabels(t *testing.T, labels []*model.LabelPair, path string, isKube bo
func testRequest(t *testing.T, exporter *Exporter, cb func(metrics []model.MetricFamily)) {
exporter.ListenAddress = listenAddress
if exporter.KubeSecretTypes == nil {
exporter.KubeSecretTypes = []string{"kubernetes.io/tls:tls.crt"}
exporter.KubeSecretTypes = []KubeSecretType{{Type: "kubernetes.io/tls", Regexp: regexp.MustCompile(`tls\.crt`)}}
}
exporter.DiscoverCertificates()

View File

@@ -32,15 +32,15 @@ func (exporter *Exporter) parseAllKubeObjects() ([]*certificateRef, []error) {
readCertificatesFromSecrets := func(secrets []v1.Secret) (outputs []*certificateRef) {
for _, secret := range secrets {
for _, secretType := range exporter.KubeSecretTypes {
typeAndKey := strings.Split(secretType, ":")
if secret.Type == v1.SecretType(typeAndKey[0]) && len(secret.Data[typeAndKey[1]]) > 0 {
outputs = append(outputs, &certificateRef{
path: fmt.Sprintf("k8s/%s/%s", secret.GetNamespace(), secret.GetName()),
format: certificateFormatKubeSecret,
kubeSecret: secret,
kubeSecretKey: typeAndKey[1],
})
for key := range secret.Data {
if secretType.Matches(string(secret.Type), key) {
output = append(output, &certificateRef{
path: fmt.Sprintf("k8s/%s/%s", secret.GetNamespace(), secret.GetName()),
format: certificateFormatKubeSecret,
kubeSecret: secret,
kubeSecretKey: key,
})
}
}
}
}
@@ -247,17 +247,12 @@ func (exporter *Exporter) filterSecrets(secrets []v1.Secret, includedLabels, exc
func (exporter *Exporter) checkHasIncludedType(secret *v1.Secret) (bool, error) {
for _, secretType := range exporter.KubeSecretTypes {
typeAndKey := strings.Split(secretType, ":")
if len(typeAndKey) != 2 {
return false, fmt.Errorf("malformed kube secret type: \"%s\"", secretType)
}
if secret.Type == v1.SecretType(typeAndKey[0]) && len(secret.Data[typeAndKey[1]]) > 0 {
return true, nil
for key := range secret.Data {
if secretType.Matches(string(secret.Type), key) {
return true, nil
}
}
}
return false, nil
}
@@ -272,9 +267,10 @@ func (exporter *Exporter) shrinkSecret(secret v1.Secret) v1.Secret {
}
for _, secretType := range exporter.KubeSecretTypes {
typeAndKey := strings.Split(secretType, ":")
if secret.Type == v1.SecretType(typeAndKey[0]) && len(secret.Data[typeAndKey[1]]) > 0 {
result.Data[typeAndKey[1]] = secret.Data[typeAndKey[1]]
for key := range secret.Data {
if secretType.Matches(string(secret.Type), key) {
result.Data[key] = secret.Data[key]
}
}
}

View File

@@ -7,6 +7,7 @@ import (
"os/exec"
"path"
"path/filepath"
"regexp"
"testing"
"time"
@@ -263,9 +264,9 @@ func TestKubeIncludeExcludeLabelMix4(t *testing.T) {
func TestKubeCustomSecret(t *testing.T) {
testRequestKube(t, &Exporter{
KubeSecretTypes: []string{
"istio.io/cert-and-key:cert-chain.pem",
"istio.io/cert-and-key:root-cert.pem",
KubeSecretTypes: []KubeSecretType{
{Type: "istio.io/cert-and-key", Regexp: regexp.MustCompile(`cert-chain\.pem`)},
{Type: "istio.io/cert-and-key", Regexp: regexp.MustCompile(`root-cert\.pem`)},
},
}, func(m []model.MetricFamily) {
metric := getMetricsForName(m, "x509_cert_expired")
@@ -349,19 +350,18 @@ func TestKubeInvalidConfig3(t *testing.T) {
}
func TestKubeInvalidSecretType(t *testing.T) {
testRequestKube(t, &Exporter{
KubeIncludeNamespaces: []string{"default"},
KubeSecretTypes: []string{"aze"},
}, func(m []model.MetricFamily) {
metrics := getMetricsForName(m, "x509_read_errors")
assert.Equal(t, 1., metrics[0].GetGauge().GetValue())
})
_, err := ParseSecretType("aze")
assert.Error(t, err)
}
func TestKubeEmptyStringKey(t *testing.T) {
testRequestKube(t, &Exporter{
KubeIncludeLabels: []string{"empty=true"},
KubeSecretTypes: []string{"kubernetes.io/tls:tls.crt", "kubernetes.io/tls:tls.key", "kubernetes.io/tls:nil.key"},
KubeSecretTypes: []KubeSecretType{
{Type: "kubernetes.io/tls", Regexp: regexp.MustCompile(`tls\.crt`)},
{Type: "kubernetes.io/tls", Regexp: regexp.MustCompile(`tls\.key`)},
{Type: "kubernetes.io/tls", Regexp: regexp.MustCompile(`nil\.key`)},
},
}, func(m []model.MetricFamily) {
metrics := getMetricsForName(m, "x509_read_errors")
assert.Equal(t, 0., metrics[0].GetGauge().GetValue())