From 507f6ad65862b13beddfd244db961a58bd133135 Mon Sep 17 00:00:00 2001 From: Vadim Rutkovsky Date: Thu, 24 Jul 2025 08:54:54 +0200 Subject: [PATCH] cert inspection: add an option to make refresh period annotation human-readable This option converts `time.ParseDuration`-compatible string of duration (i.e. `77h30m00s`) into human-redable string (`3d5h30m`) so that TLS registry markdowns have improved readability --- .../certgraphanalysis/metadata_options.go | 86 +++++++++++++++++++ .../certgraphanalysis/metadata_test.go | 79 +++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 pkg/certs/cert-inspection/certgraphanalysis/metadata_test.go diff --git a/pkg/certs/cert-inspection/certgraphanalysis/metadata_options.go b/pkg/certs/cert-inspection/certgraphanalysis/metadata_options.go index 4e3e5d6d13..f4e42606c0 100644 --- a/pkg/certs/cert-inspection/certgraphanalysis/metadata_options.go +++ b/pkg/certs/cert-inspection/certgraphanalysis/metadata_options.go @@ -4,11 +4,14 @@ import ( "fmt" "os" "regexp" + "strconv" "strings" + "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/openshift/library-go/pkg/certs/cert-inspection/certgraphapi" + "github.com/openshift/library-go/pkg/operator/certrotation" corev1 "k8s.io/api/core/v1" ) @@ -158,8 +161,32 @@ var ( secret.Name = strings.ReplaceAll(secret.Name, hash, "") }, } + RewriteRefreshPeriod = &metadataOptions{ + rewriteSecretFn: func(secret *corev1.Secret) { + humanizeRefreshPeriodFromMetadata(secret.Annotations) + }, + rewriteConfigMapFn: func(configMap *corev1.ConfigMap) { + humanizeRefreshPeriodFromMetadata(configMap.Annotations) + }, + } ) +func humanizeRefreshPeriodFromMetadata(annotations map[string]string) { + period, ok := annotations[certrotation.CertificateRefreshPeriodAnnotation] + if !ok { + return + } + d, err := time.ParseDuration(period) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to parse certificate refresh period %q: %v\n", period, err) + return + } + humanReadableDate := durationToHumanReadableString(d) + annotations[certrotation.CertificateRefreshPeriodAnnotation] = humanReadableDate + annotations[rewritePrefix+"RewriteRefreshPeriod"] = period + return +} + // skipRevisionedInOnDiskLocation returns true if location is for revisioned certificate and needs to be skipped func skipRevisionedInOnDiskLocation(location certgraphapi.OnDiskLocation) bool { if len(location.Path) == 0 { @@ -235,3 +262,62 @@ func StripRootFSMountPoint(rootfsMount string) *metadataOptions { }, } } + +// durationToHumanReadableString formats a duration into a human-readable string. +// Unlike Go's built-in `time.Duration.String()`, which returns a string like "72h0m0s", this function returns a more concise format like "3d" or "5d4h25m". +// Implementation is based on https://github.com/gomodules/sprig/blob/master/date.go#L97-L139, +// but it doesn't round the duration to the nearest largest value but converts it precisely +// This function rounds duration to the nearest second and handles negative durations by taking the absolute value. +func durationToHumanReadableString(d time.Duration) string { + if d == 0 { + return "0s" + } + // Handle negative durations by taking the absolute value + // This also rounds the duration to the nearest second + u := uint64(d.Abs().Seconds()) + + var b strings.Builder + + writeUnit := func(value uint64, suffix string) { + if value > 0 { + b.WriteString(strconv.FormatUint(value, 10)) + b.WriteString(suffix) + } + } + + const ( + // Unit values in seconds + year = 60 * 60 * 24 * 365 + month = 60 * 60 * 24 * 30 + day = 60 * 60 * 24 + hour = 60 * 60 + minute = 60 + second = 1 + ) + + years := u / year + u %= year + writeUnit(years, "y") + + months := u / month + u %= month + writeUnit(months, "mo") + + days := u / day + u %= day + writeUnit(days, "d") + + hours := u / hour + u %= hour + writeUnit(hours, "h") + + minutes := u / minute + u %= minute + writeUnit(minutes, "m") + + seconds := u / second + u %= second + writeUnit(seconds, "s") + + return b.String() +} diff --git a/pkg/certs/cert-inspection/certgraphanalysis/metadata_test.go b/pkg/certs/cert-inspection/certgraphanalysis/metadata_test.go new file mode 100644 index 0000000000..402ca6e6cb --- /dev/null +++ b/pkg/certs/cert-inspection/certgraphanalysis/metadata_test.go @@ -0,0 +1,79 @@ +package certgraphanalysis + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" +) + +func TestDurationToHumanReadableString(t *testing.T) { + tests := []struct { + duration time.Duration + expected string + }{ + {0, "0s"}, + {time.Second, "1s"}, + {2 * time.Second, "2s"}, + {time.Minute, "1m"}, + {time.Minute + 30*time.Second, "1m30s"}, + {time.Hour, "1h"}, + {25 * time.Hour, "1d1h"}, + {30 * 24 * time.Hour, "1mo"}, + {365 * 24 * time.Hour, "1y"}, + {400 * 24 * time.Hour, "1y1mo5d"}, + {-time.Minute, "1m"}, // negative duration + {-400 * 24 * time.Hour, "1y1mo5d"}, // negative composite + {3*time.Minute + 4*time.Second, "3m4s"}, + } + for _, test := range tests { + t.Run(test.expected, func(t *testing.T) { + result := durationToHumanReadableString(test.duration) + if result != test.expected { + t.Errorf("expected %s, got %s", test.expected, result) + } + }) + } +} + +func TestHumanizeRefreshPeriodFromMetadata(t *testing.T) { + tests := []struct { + metadata string + expected string + }{ + { + metadata: "72h00m00s", + expected: "3d", + }, + { + metadata: "124h25m00s", + expected: "5d4h25m", + }, + { + metadata: "82080h00m00s", + expected: "9y4mo15d", + }, + } + for _, test := range tests { + t.Run(test.metadata, func(t *testing.T) { + result := map[string]string{ + "certificates.openshift.io/refresh-period": test.metadata, + } + humanizeRefreshPeriodFromMetadata(result) + + expected := map[string]string{ + "certificates.openshift.io/refresh-period": test.expected, + "rewritten.cert-info.openshift.io/RewriteRefreshPeriod": test.metadata, + } + diff := cmp.Diff(expected, result) + if diff != "" { + t.Errorf("expected %v, got %v, diff: %s", test.expected, result, diff) + } + }) + } +} + +func TestHumanizeRefreshPeriodFromMetadataNils(t *testing.T) { + humanizeRefreshPeriodFromMetadata(nil) + humanizeRefreshPeriodFromMetadata(map[string]string{}) +}