1
0
mirror of https://github.com/rancher/steve.git synced 2025-09-12 13:31:57 +00:00

#48673 - Added Timestamp Cache Handling to metadata.fields (#648)

* added timestamp convertion to metadata.fields

* fixed duration parsing

* fixed tests

* removed tags file

* added comments

* added better error handling

* changed ParseHumanDuration to use Fscanf

* added builtins handling

* adding mock updates

* fixing tests

* another try

* added timestamp convertion to metadata.fields

* addressing comments from @ericpromislow

* converting error to warning

* added template options
This commit is contained in:
Felipe Gehrke
2025-06-16 19:33:28 -03:00
committed by GitHub
parent 2e8a0f2851
commit b3539616e0
13 changed files with 513 additions and 33 deletions

View File

@@ -2,12 +2,17 @@ package common
import (
"net/http"
"slices"
"strconv"
"strings"
"time"
"github.com/rancher/apiserver/pkg/types"
"github.com/rancher/steve/pkg/accesscontrol"
"github.com/rancher/steve/pkg/attributes"
"github.com/rancher/steve/pkg/resources/virtual/common"
"github.com/sirupsen/logrus"
"github.com/rancher/steve/pkg/schema"
metricsStore "github.com/rancher/steve/pkg/stores/metrics"
"github.com/rancher/steve/pkg/stores/proxy"
@@ -19,25 +24,32 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
schema2 "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/duration"
)
type TemplateOptions struct {
InSQLMode bool
}
func DefaultTemplate(clientGetter proxy.ClientGetter,
summaryCache *summarycache.SummaryCache,
asl accesscontrol.AccessSetLookup,
namespaceCache corecontrollers.NamespaceCache) schema.Template {
namespaceCache corecontrollers.NamespaceCache,
options TemplateOptions) schema.Template {
return schema.Template{
Store: metricsStore.NewMetricsStore(proxy.NewProxyStore(clientGetter, summaryCache, asl, namespaceCache)),
Formatter: formatter(summaryCache, asl),
Formatter: formatter(summaryCache, asl, options),
}
}
// DefaultTemplateForStore provides a default schema template which uses a provided, pre-initialized store. Primarily used when creating a Template that uses a Lasso SQL store internally.
func DefaultTemplateForStore(store types.Store,
summaryCache *summarycache.SummaryCache,
asl accesscontrol.AccessSetLookup) schema.Template {
asl accesscontrol.AccessSetLookup,
options TemplateOptions) schema.Template {
return schema.Template{
Store: store,
Formatter: formatter(summaryCache, asl),
Formatter: formatter(summaryCache, asl, options),
}
}
@@ -83,7 +95,7 @@ func buildBasePath(gvr schema2.GroupVersionResource, namespace string, includeNa
return buf.String()
}
func formatter(summarycache common.SummaryCache, asl accesscontrol.AccessSetLookup) types.Formatter {
func formatter(summarycache common.SummaryCache, asl accesscontrol.AccessSetLookup, options TemplateOptions) types.Formatter {
return func(request *types.APIRequest, resource *types.RawResource) {
if resource.Schema == nil {
return
@@ -140,6 +152,7 @@ func formatter(summarycache common.SummaryCache, asl accesscontrol.AccessSetLook
delete(resource.Links, "patch")
}
gvk := attributes.GVK(resource.Schema)
if unstr, ok := resource.APIObject.Object.(*unstructured.Unstructured); ok {
// with the sql cache, these were already added by the indexer. However, the sql cache
// is only used for lists, so we need to re-add here for get/watch
@@ -157,6 +170,10 @@ func formatter(summarycache common.SummaryCache, asl accesscontrol.AccessSetLook
includeFields(request, unstr)
excludeFields(request, unstr)
excludeValues(request, unstr)
if options.InSQLMode {
convertMetadataTimestampFields(request, gvk, unstr)
}
}
if permsQuery := request.Query.Get("checkPermissions"); permsQuery != "" {
@@ -212,6 +229,56 @@ func excludeFields(request *types.APIRequest, unstr *unstructured.Unstructured)
}
}
// convertMetadataTimestampFields updates metadata timestamp fields to ensure they remain fresh and human-readable when sent back
// to the client. Internally, fields are stored as Unix timestamps; on each request, we calculate the elapsed time since
// those timestamps by subtracting them from time.Now(), then format the resulting duration into a human-friendly string.
// This prevents cached durations (e.g. “2d” - 2 days) from becoming stale over time.
func convertMetadataTimestampFields(request *types.APIRequest, gvk schema2.GroupVersionKind, unstr *unstructured.Unstructured) {
if request.Schema != nil {
cols := GetColumnDefinitions(request.Schema)
for _, col := range cols {
gvkDateFields, gvkFound := DateFieldsByGVKBuiltins[gvk]
if col.Type == "date" || (gvkFound && slices.Contains(gvkDateFields, col.Name)) {
index := GetIndexValueFromString(col.Field)
if index == -1 {
logrus.Errorf("field index not found at column.Field struct variable: %s", col.Field)
return
}
curValue, got, err := unstructured.NestedSlice(unstr.Object, "metadata", "fields")
if err != nil {
logrus.Errorf("failed to get metadata.fields slice from unstr.Object: %s", err.Error())
}
if !got {
logrus.Debugf("couldn't find metadata.fields at unstr.Object")
return
}
timeValue, ok := curValue[index].(string)
if !ok {
logrus.Debugf("time field isn't a string")
return
}
millis, err := strconv.ParseInt(timeValue, 10, 64)
if err != nil {
logrus.Warnf("failed to convert timestamp value: %s", err.Error())
return
}
timestamp := time.Unix(0, millis*int64(time.Millisecond))
dur := time.Since(timestamp)
curValue[index] = duration.HumanDuration(dur)
if err := unstructured.SetNestedSlice(unstr.Object, curValue, "metadata", "fields"); err != nil {
logrus.Errorf("failed to set value back to metadata.fields slice: %s", err.Error())
return
}
}
}
}
}
func excludeValues(request *types.APIRequest, unstr *unstructured.Unstructured) {
if values, ok := request.Query["excludeValues"]; ok {
for _, f := range values {