Skip to content

Commit 40c0d2d

Browse files
authored
Merge pull request #3 from travelping/cennso-3136-time-till-certificate-expires
[CENNSO-3136] Time till certificate expires
2 parents e234e18 + fd7f5e7 commit 40c0d2d

File tree

2 files changed

+170
-47
lines changed

2 files changed

+170
-47
lines changed

strongswan/collector.go

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,9 @@ type Collector struct {
5858
saRekeySecs *prometheus.Desc
5959
saLifetimeSecs *prometheus.Desc
6060

61-
crtCnt *prometheus.Desc
62-
crtValid *prometheus.Desc
61+
crtCnt *prometheus.Desc
62+
crtValid *prometheus.Desc
63+
crtExpireSecs *prometheus.Desc
6364
}
6465

6566
func NewCollector(viciClientFn viciClientFn) *Collector {
@@ -214,6 +215,11 @@ func NewCollector(viciClientFn viciClientFn) *Collector {
214215
"X509 certificate validity",
215216
[]string{"serial_number", "subject", "alternate_names", "not_before", "not_after"}, nil,
216217
),
218+
crtExpireSecs: prometheus.NewDesc(
219+
prefix+"crt_expire_secs",
220+
"Seconds until the X509 certificate expires",
221+
[]string{"serial_number", "subject", "alternate_names", "not_before", "not_after"}, nil,
222+
),
217223
}
218224
}
219225

@@ -249,6 +255,7 @@ func (c *Collector) Describe(ch chan<- *prometheus.Desc) {
249255

250256
ch <- c.crtCnt
251257
ch <- c.crtValid
258+
ch <- c.crtExpireSecs
252259
}
253260

254261
func (c *Collector) Collect(ch chan<- prometheus.Metric) {
@@ -558,6 +565,7 @@ func alternateNames(cert *x509.Certificate) string {
558565
func (c *Collector) collectCrtMetrics(crts []Crt, ch chan<- prometheus.Metric) {
559566
var x509Crts uint
560567

568+
now := time.Now()
561569
for _, crt := range crts {
562570
if crt.Type != "X509" {
563571
continue
@@ -569,21 +577,30 @@ func (c *Collector) collectCrtMetrics(crts []Crt, ch chan<- prometheus.Metric) {
569577
continue
570578
}
571579

572-
now := time.Now()
573580
valid := 0
574581
if now.After(cert.NotBefore) && now.Before(cert.NotAfter) {
575582
valid = 1
576583
}
584+
expire := cert.NotAfter.Sub(now).Seconds()
577585

578-
ch <- prometheus.MustNewConstMetric(
579-
c.crtValid,
580-
prometheus.GaugeValue,
581-
float64(valid),
586+
labels := []string{
582587
cert.SerialNumber.String(),
583588
cert.Subject.String(),
584589
alternateNames(cert),
585590
cert.NotBefore.Format(time.RFC3339),
586591
cert.NotAfter.Format(time.RFC3339),
592+
}
593+
ch <- prometheus.MustNewConstMetric(
594+
c.crtValid,
595+
prometheus.GaugeValue,
596+
float64(valid),
597+
labels...,
598+
)
599+
ch <- prometheus.MustNewConstMetric(
600+
c.crtExpireSecs,
601+
prometheus.GaugeValue,
602+
float64(expire),
603+
labels...,
587604
)
588605

589606
x509Crts++

strongswan/collector_test.go

Lines changed: 146 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@ import (
88
"crypto/x509/pkix"
99
"errors"
1010
"fmt"
11+
"math"
1112
"math/big"
1213
"net"
1314
"net/url"
15+
"strconv"
1416
"strings"
1517
"testing"
1618
"time"
1719

1820
"github.com/prometheus/client_golang/prometheus/testutil"
21+
"github.com/prometheus/common/expfmt"
1922
"github.com/stretchr/testify/require"
2023
"github.com/strongswan/govici/vici"
2124
)
@@ -28,6 +31,11 @@ type fakeViciClient struct {
2831
closeTriggered int
2932
}
3033

34+
const (
35+
crtExpireTimeMetricName = "strongswan_crt_expire_secs"
36+
crtExpireAllowedDiffFromExpected = 60.0
37+
)
38+
3139
func (fvc *fakeViciClient) StreamedCommandRequest(cmd string, event string, _ *vici.Message) ([]*vici.Message, error) {
3240
if cmd == "list-sas" && event == "list-sa" {
3341
return fvc.sasMsgs, fvc.sasErr
@@ -410,27 +418,16 @@ func TestCollector_Metrics(t *testing.T) {
410418
wantMetricsHelp: "Number of X509 certificates",
411419
wantMetricsType: "gauge",
412420
wantMetricsValue: 1,
413-
wantMetricsCount: 3,
421+
wantMetricsCount: 4,
414422
},
415423
{
416-
name: "one cert count",
417-
crtsMsgsModifierFn: func() []*vici.Message {
418-
crtMsg := vici.NewMessage()
419-
crtMsg.Set("type", "X509")
420-
421-
crt, err := createSingleX509Crt()
422-
if err != nil {
423-
return []*vici.Message{}
424-
}
425-
426-
crtMsg.Set("data", string(crt))
427-
return []*vici.Message{crtMsg}
428-
},
429-
metricName: "strongswan_crt_count",
430-
wantMetricsHelp: "Number of X509 certificates",
431-
wantMetricsType: "gauge",
432-
wantMetricsValue: 1,
433-
wantMetricsCount: 3,
424+
name: "one cert count",
425+
crtsMsgsModifierFn: singleCertViciMessages(createSingleX509Crt),
426+
metricName: "strongswan_crt_count",
427+
wantMetricsHelp: "Number of X509 certificates",
428+
wantMetricsType: "gauge",
429+
wantMetricsValue: 1,
430+
wantMetricsCount: 4,
434431
},
435432
{
436433
name: "two cert count",
@@ -454,28 +451,27 @@ func TestCollector_Metrics(t *testing.T) {
454451
wantMetricsHelp: "Number of X509 certificates",
455452
wantMetricsType: "gauge",
456453
wantMetricsValue: 2,
457-
wantMetricsCount: 4,
454+
wantMetricsCount: 6,
458455
},
459456
{
460-
name: "cert valid",
461-
crtsMsgsModifierFn: func() []*vici.Message {
462-
crtMsg := vici.NewMessage()
463-
crtMsg.Set("type", "X509")
464-
465-
crt, err := createSingleX509Crt()
466-
if err != nil {
467-
return []*vici.Message{}
468-
}
469-
470-
crtMsg.Set("data", string(crt))
471-
return []*vici.Message{crtMsg}
472-
},
473-
metricName: "strongswan_crt_valid",
474-
wantMetricsHelp: "X509 certificate validity",
475-
wantMetricsType: "gauge",
476-
wantMetricsLabels: `alternate_names="",not_after="2124-01-01T12:00:00Z",not_before="2024-01-01T12:00:00Z",serial_number="1",subject="CN=Test,O=Org1"`,
477-
wantMetricsValue: 1,
478-
wantMetricsCount: 3,
457+
name: "cert valid",
458+
crtsMsgsModifierFn: singleCertViciMessages(createSingleX509Crt),
459+
metricName: "strongswan_crt_valid",
460+
wantMetricsHelp: "X509 certificate validity",
461+
wantMetricsType: "gauge",
462+
wantMetricsLabels: `alternate_names="",not_after="2124-01-01T12:00:00Z",not_before="2024-01-01T12:00:00Z",serial_number="1",subject="CN=Test,O=Org1"`,
463+
wantMetricsValue: 1,
464+
wantMetricsCount: 4,
465+
},
466+
{
467+
name: "cert expired",
468+
crtsMsgsModifierFn: singleCertViciMessages(createExpiredX509Crt),
469+
metricName: "strongswan_crt_valid",
470+
wantMetricsHelp: "X509 certificate validity",
471+
wantMetricsType: "gauge",
472+
wantMetricsLabels: `alternate_names="",not_after="2024-12-01T12:00:00Z",not_before="2024-01-01T12:00:00Z",serial_number="1",subject="CN=Test,O=Org1"`,
473+
wantMetricsValue: 0,
474+
wantMetricsCount: 4,
479475
},
480476
{
481477
name: "cert alternate names",
@@ -510,7 +506,7 @@ func TestCollector_Metrics(t *testing.T) {
510506
wantMetricsType: "gauge",
511507
wantMetricsLabels: `alternate_names="DNS=test.org+DNS=test2.org,EM=Name1+EM=Name2,IP=192.168.0.1+IP=192.168.1.1,URI=https://test.org/foobar+URI=https://test2.org/foobar2",not_after="2124-01-01T12:00:00Z",not_before="2024-01-01T12:00:00Z",serial_number="1",subject="CN=Test,O=Org1"`,
512508
wantMetricsValue: 1,
513-
wantMetricsCount: 3,
509+
wantMetricsCount: 4,
514510
},
515511
}
516512
for _, tt := range tests {
@@ -750,6 +746,100 @@ func TestCollector_MetricsSaChild(t *testing.T) {
750746
}
751747
}
752748

749+
func TestCollector_MetricsCrtExpireTime(t *testing.T) {
750+
tz, err := time.LoadLocation("UTC")
751+
if err != nil {
752+
t.Errorf("failed to load timezone: %s", err)
753+
}
754+
755+
tests := []struct {
756+
name string
757+
crtsMsgsModifierFn func() []*vici.Message
758+
wantMetricsLabels string
759+
wantMetricsValue float64
760+
}{
761+
{
762+
name: "cert expire time for valid cert",
763+
crtsMsgsModifierFn: singleCertViciMessages(createSingleX509Crt),
764+
wantMetricsLabels: `alternate_names="",not_after="2124-01-01T12:00:00Z",not_before="2024-01-01T12:00:00Z",serial_number="1",subject="CN=Test,O=Org1"`,
765+
wantMetricsValue: time.Until(time.Date(2124, 1, 1, 12, 0, 0, 0, tz)).Seconds(),
766+
},
767+
{
768+
name: "cert expire time for expired cert",
769+
crtsMsgsModifierFn: singleCertViciMessages(createExpiredX509Crt),
770+
wantMetricsLabels: `alternate_names="",not_after="2024-12-01T12:00:00Z",not_before="2024-01-01T12:00:00Z",serial_number="1",subject="CN=Test,O=Org1"`,
771+
wantMetricsValue: time.Until(time.Date(2024, 12, 1, 12, 0, 0, 0, tz)).Seconds(),
772+
},
773+
}
774+
for _, tt := range tests {
775+
t.Run(tt.name, func(t *testing.T) {
776+
crtsMmsgs := []*vici.Message{}
777+
if tt.crtsMsgsModifierFn != nil {
778+
crtsMmsgs = tt.crtsMsgsModifierFn()
779+
}
780+
c := NewCollector(func() (ViciClient, error) {
781+
return &fakeViciClient{
782+
crtsMsgs: crtsMmsgs,
783+
},
784+
nil
785+
})
786+
787+
cnt := testutil.CollectAndCount(c)
788+
const wantMetricsCount = 4
789+
require.Equal(t, wantMetricsCount, cnt, "metrics count")
790+
791+
metricBytes, err := testutil.CollectAndFormat(c, expfmt.TypeTextPlain, crtExpireTimeMetricName)
792+
if err != nil {
793+
t.Fatalf("unexpected collecting result of '%s':\n%s", crtExpireTimeMetricName, err)
794+
}
795+
796+
metricStr := strings.TrimSpace(string(metricBytes))
797+
validateMetricsCrtExpireTimeValue(t, metricStr, tt.wantMetricsValue)
798+
validateMetricsCrtExpireTimeLabels(t, metricStr, tt.wantMetricsLabels)
799+
})
800+
}
801+
}
802+
803+
func validateMetricsCrtExpireTimeValue(t *testing.T, metricStr string, wantMetricsValue float64) {
804+
metricFields := strings.Split(metricStr, " ")
805+
if len(metricFields) == 0 {
806+
t.Fatalf("unexpected format of metric '%s': %s", crtExpireTimeMetricName, metricStr)
807+
}
808+
809+
metricValStr := metricFields[len(metricFields)-1]
810+
metricVal, err := strconv.ParseFloat(metricValStr, 64)
811+
if err != nil {
812+
t.Fatalf("failure in parsing of metric's value '%s': %s\n%s", crtExpireTimeMetricName, metricValStr, err)
813+
}
814+
815+
require.GreaterOrEqual(t, crtExpireAllowedDiffFromExpected, math.Abs(metricVal-wantMetricsValue), "seconds till cert expires value")
816+
}
817+
818+
func validateMetricsCrtExpireTimeLabels(t *testing.T, metricStr, wantMetricsLabels string) {
819+
labels := strings.Split(metricStr, "{")
820+
if len(labels) < 2 {
821+
t.Fatalf("unexpected format of metric '%s': %s", crtExpireTimeMetricName, metricStr)
822+
}
823+
824+
labels = strings.Split(labels[1], "}")
825+
require.Equal(t, labels[0], wantMetricsLabels, "seconds till cert expires labels")
826+
}
827+
828+
func singleCertViciMessages(createX509CrtFn func() ([]byte, error)) func() []*vici.Message {
829+
return func() []*vici.Message {
830+
crtMsg := vici.NewMessage()
831+
crtMsg.Set("type", "X509")
832+
833+
crt, err := createX509CrtFn()
834+
if err != nil {
835+
return []*vici.Message{}
836+
}
837+
838+
crtMsg.Set("data", string(crt))
839+
return []*vici.Message{crtMsg}
840+
}
841+
}
842+
753843
func createSingleX509Crt() ([]byte, error) {
754844
return createSingleX509CrtWithAlternateNames([]string{}, []string{}, []net.IP{}, []*url.URL{})
755845
}
@@ -808,6 +898,22 @@ func createDoubleX509Crt() ([]byte, []byte, error) {
808898
return crt1, crt2, nil
809899
}
810900

901+
func createExpiredX509Crt() ([]byte, error) {
902+
tz, err := time.LoadLocation("UTC")
903+
if err != nil {
904+
return nil, err
905+
}
906+
907+
return createX509Crt(
908+
pkix.Name{
909+
Organization: []string{"Org1"},
910+
CommonName: "Test",
911+
},
912+
time.Date(2024, 1, 1, 12, 0, 0, 0, tz),
913+
time.Date(2024, 12, 1, 12, 0, 0, 0, tz),
914+
)
915+
}
916+
811917
func createX509Crt(subject pkix.Name, notBefore, notAfter time.Time) ([]byte, error) {
812918
return createX509CrtWithAlternateNames(subject, notBefore, notAfter, []string{}, []string{}, []net.IP{}, []*url.URL{})
813919
}

0 commit comments

Comments
 (0)