mirror of
https://github.com/rancher/steve.git
synced 2025-07-16 16:01:37 +00:00
* 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:
parent
2e8a0f2851
commit
b3539616e0
35
pkg/resources/common/duration.go
Normal file
35
pkg/resources/common/duration.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseHumanReadableDuration(s string) (time.Duration, error) {
|
||||||
|
var total time.Duration
|
||||||
|
var val int
|
||||||
|
var unit byte
|
||||||
|
|
||||||
|
r := strings.NewReader(s)
|
||||||
|
for r.Len() > 0 {
|
||||||
|
if _, err := fmt.Fscanf(r, "%d%c", &val, &unit); err != nil {
|
||||||
|
return 0, fmt.Errorf("invalid duration in %s: %w", s, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch unit {
|
||||||
|
case 'd':
|
||||||
|
total += time.Duration(val) * 24 * time.Hour
|
||||||
|
case 'h':
|
||||||
|
total += time.Duration(val) * time.Hour
|
||||||
|
case 'm':
|
||||||
|
total += time.Duration(val) * time.Minute
|
||||||
|
case 's':
|
||||||
|
total += time.Duration(val) * time.Second
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("invalid duration unit %s in %s", string(unit), s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return total, nil
|
||||||
|
}
|
74
pkg/resources/common/duration_test.go
Normal file
74
pkg/resources/common/duration_test.go
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseHumanDuration(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected time.Duration
|
||||||
|
expectedErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "days + hours + mins + secs",
|
||||||
|
input: "1d23h45m56s",
|
||||||
|
expected: 24*time.Hour + 23*time.Hour + 45*time.Minute + 56*time.Second,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hours + mins + secs",
|
||||||
|
input: "12h34m56s",
|
||||||
|
expected: 12*time.Hour + 34*time.Minute + 56*time.Second,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "days + hours",
|
||||||
|
input: "1d2h",
|
||||||
|
expected: 24*time.Hour + 2*time.Hour,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hours + secs",
|
||||||
|
input: "1d2s",
|
||||||
|
expected: 24*time.Hour + 2*time.Second,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mins + secs",
|
||||||
|
input: "1d2m",
|
||||||
|
expected: 24*time.Hour + 2*time.Minute,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hours",
|
||||||
|
input: "1h",
|
||||||
|
expected: 1 * time.Hour,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mins",
|
||||||
|
input: "1m",
|
||||||
|
expected: 1 * time.Minute,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "secs",
|
||||||
|
input: "0s",
|
||||||
|
expected: 0 * time.Second,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid input",
|
||||||
|
input: "<invalid>",
|
||||||
|
expectedErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
output, err := ParseHumanReadableDuration(tc.input)
|
||||||
|
if tc.expectedErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, tc.expected, output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -2,12 +2,17 @@ package common
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/rancher/apiserver/pkg/types"
|
"github.com/rancher/apiserver/pkg/types"
|
||||||
"github.com/rancher/steve/pkg/accesscontrol"
|
"github.com/rancher/steve/pkg/accesscontrol"
|
||||||
"github.com/rancher/steve/pkg/attributes"
|
"github.com/rancher/steve/pkg/attributes"
|
||||||
"github.com/rancher/steve/pkg/resources/virtual/common"
|
"github.com/rancher/steve/pkg/resources/virtual/common"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/rancher/steve/pkg/schema"
|
"github.com/rancher/steve/pkg/schema"
|
||||||
metricsStore "github.com/rancher/steve/pkg/stores/metrics"
|
metricsStore "github.com/rancher/steve/pkg/stores/metrics"
|
||||||
"github.com/rancher/steve/pkg/stores/proxy"
|
"github.com/rancher/steve/pkg/stores/proxy"
|
||||||
@ -19,25 +24,32 @@ import (
|
|||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
schema2 "k8s.io/apimachinery/pkg/runtime/schema"
|
schema2 "k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/util/duration"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type TemplateOptions struct {
|
||||||
|
InSQLMode bool
|
||||||
|
}
|
||||||
|
|
||||||
func DefaultTemplate(clientGetter proxy.ClientGetter,
|
func DefaultTemplate(clientGetter proxy.ClientGetter,
|
||||||
summaryCache *summarycache.SummaryCache,
|
summaryCache *summarycache.SummaryCache,
|
||||||
asl accesscontrol.AccessSetLookup,
|
asl accesscontrol.AccessSetLookup,
|
||||||
namespaceCache corecontrollers.NamespaceCache) schema.Template {
|
namespaceCache corecontrollers.NamespaceCache,
|
||||||
|
options TemplateOptions) schema.Template {
|
||||||
return schema.Template{
|
return schema.Template{
|
||||||
Store: metricsStore.NewMetricsStore(proxy.NewProxyStore(clientGetter, summaryCache, asl, namespaceCache)),
|
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.
|
// 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,
|
func DefaultTemplateForStore(store types.Store,
|
||||||
summaryCache *summarycache.SummaryCache,
|
summaryCache *summarycache.SummaryCache,
|
||||||
asl accesscontrol.AccessSetLookup) schema.Template {
|
asl accesscontrol.AccessSetLookup,
|
||||||
|
options TemplateOptions) schema.Template {
|
||||||
return schema.Template{
|
return schema.Template{
|
||||||
Store: store,
|
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()
|
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) {
|
return func(request *types.APIRequest, resource *types.RawResource) {
|
||||||
if resource.Schema == nil {
|
if resource.Schema == nil {
|
||||||
return
|
return
|
||||||
@ -140,6 +152,7 @@ func formatter(summarycache common.SummaryCache, asl accesscontrol.AccessSetLook
|
|||||||
delete(resource.Links, "patch")
|
delete(resource.Links, "patch")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gvk := attributes.GVK(resource.Schema)
|
||||||
if unstr, ok := resource.APIObject.Object.(*unstructured.Unstructured); ok {
|
if unstr, ok := resource.APIObject.Object.(*unstructured.Unstructured); ok {
|
||||||
// with the sql cache, these were already added by the indexer. However, the sql cache
|
// 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
|
// 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)
|
includeFields(request, unstr)
|
||||||
excludeFields(request, unstr)
|
excludeFields(request, unstr)
|
||||||
excludeValues(request, unstr)
|
excludeValues(request, unstr)
|
||||||
|
|
||||||
|
if options.InSQLMode {
|
||||||
|
convertMetadataTimestampFields(request, gvk, unstr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if permsQuery := request.Query.Get("checkPermissions"); permsQuery != "" {
|
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) {
|
func excludeValues(request *types.APIRequest, unstr *unstructured.Unstructured) {
|
||||||
if values, ok := request.Query["excludeValues"]; ok {
|
if values, ok := request.Query["excludeValues"]; ok {
|
||||||
for _, f := range values {
|
for _, f := range values {
|
||||||
|
@ -1068,7 +1068,7 @@ func Test_formatterLinks(t *testing.T) {
|
|||||||
APIObject: test.apiObject,
|
APIObject: test.apiObject,
|
||||||
Links: test.currentLinks,
|
Links: test.currentLinks,
|
||||||
}
|
}
|
||||||
fmtter := formatter(nil, asl)
|
fmtter := formatter(nil, asl, TemplateOptions{InSQLMode: false})
|
||||||
fmtter(request, resource)
|
fmtter(request, resource)
|
||||||
require.Equal(t, test.wantLinks, resource.Links)
|
require.Equal(t, test.wantLinks, resource.Links)
|
||||||
|
|
||||||
@ -1269,7 +1269,7 @@ func TestFormatterAddsResourcePermissions(t *testing.T) {
|
|||||||
|
|
||||||
asl.EXPECT().AccessFor(&defaultUserInfo).Return(&accessSet).AnyTimes()
|
asl.EXPECT().AccessFor(&defaultUserInfo).Return(&accessSet).AnyTimes()
|
||||||
|
|
||||||
formatter := formatter(fakeCache, asl)
|
formatter := formatter(fakeCache, asl, TemplateOptions{InSQLMode: false})
|
||||||
formatter(req, resource)
|
formatter(req, resource)
|
||||||
|
|
||||||
// Extract the resultant resourcePermissions
|
// Extract the resultant resourcePermissions
|
||||||
|
78
pkg/resources/common/gvkdatefields.go
Normal file
78
pkg/resources/common/gvkdatefields.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
var DateFieldsByGVKBuiltins = map[schema.GroupVersionKind][]string{
|
||||||
|
{Group: "", Version: "v1", Kind: "ConfigMap"}: {"Age"},
|
||||||
|
{Group: "", Version: "v1", Kind: "Endpoints"}: {"Age"},
|
||||||
|
{Group: "", Version: "v1", Kind: "Event"}: {"Last Seen", "First Seen"},
|
||||||
|
{Group: "", Version: "v1", Kind: "Namespace"}: {"Age"},
|
||||||
|
{Group: "", Version: "v1", Kind: "Node"}: {"Age"},
|
||||||
|
{Group: "", Version: "v1", Kind: "PersistentVolume"}: {"Age"},
|
||||||
|
{Group: "", Version: "v1", Kind: "PersistentVolumeClaim"}: {"Age"},
|
||||||
|
{Group: "", Version: "v1", Kind: "Pod"}: {"Age"},
|
||||||
|
{Group: "", Version: "v1", Kind: "ReplicationController"}: {"Age"},
|
||||||
|
{Group: "", Version: "v1", Kind: "ResourceQuota"}: {"Age"},
|
||||||
|
{Group: "", Version: "v1", Kind: "Secret"}: {"Age"},
|
||||||
|
{Group: "", Version: "v1", Kind: "Service"}: {"Age"},
|
||||||
|
{Group: "", Version: "v1", Kind: "ServiceAccount"}: {"Age"},
|
||||||
|
|
||||||
|
{Group: "admissionregistration.k8s.io", Version: "v1alpha2", Kind: "MutatingAdmissionPolicy"}: {"Age"},
|
||||||
|
{Group: "admissionregistration.k8s.io", Version: "v1alpha2", Kind: "MutatingAdmissionPolicyBinding"}: {"Age"},
|
||||||
|
{Group: "admissionregistration.k8s.io", Version: "v1alpha2", Kind: "ValidatingAdmissionPolicy"}: {"Age"},
|
||||||
|
{Group: "admissionregistration.k8s.io", Version: "v1alpha2", Kind: "ValidatingAdmissionPolicyBinding"}: {"Age"},
|
||||||
|
{Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "MutatingWebhookConfiguration"}: {"Age"},
|
||||||
|
{Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "ValidatingWebhookConfiguration"}: {"Age"},
|
||||||
|
|
||||||
|
{Group: "apiserverinternal.k8s.io", Version: "v1alpha1", Kind: "StorageVersion"}: {"Age"},
|
||||||
|
|
||||||
|
{Group: "apps", Version: "v1", Kind: "DaemonSet"}: {"Age"},
|
||||||
|
{Group: "apps", Version: "v1", Kind: "Deployment"}: {"Age"},
|
||||||
|
{Group: "apps", Version: "v1", Kind: "ReplicaSet"}: {"Age"},
|
||||||
|
{Group: "apps", Version: "v1", Kind: "StatefulSet"}: {"Age"},
|
||||||
|
{Group: "apps", Version: "v1beta1", Kind: "ControllerRevision"}: {"Age"},
|
||||||
|
|
||||||
|
{Group: "autoscaling", Version: "v1", Kind: "Scale"}: {"Age"},
|
||||||
|
{Group: "autoscaling", Version: "v2beta1", Kind: "HorizontalPodAutoscaler"}: {"Age"},
|
||||||
|
|
||||||
|
{Group: "batch", Version: "v1", Kind: "Job"}: {"Duration", "Age"},
|
||||||
|
{Group: "batch", Version: "v1beta1", Kind: "CronJob"}: {"Last Schedule", "Age"},
|
||||||
|
|
||||||
|
{Group: "certificates.k8s.io", Version: "v1beta1", Kind: "CertificateSigningRequest"}: {"Age"},
|
||||||
|
|
||||||
|
{Group: "coordination.k8s.io", Version: "v1", Kind: "Lease"}: {"Age"},
|
||||||
|
{Group: "coordination.k8s.io", Version: "v1alpha2", Kind: "LeaseCandidate"}: {"Age"},
|
||||||
|
|
||||||
|
{Group: "discovery.k8s.io", Version: "v1beta1", Kind: "EndpointSlice"}: {"Age"},
|
||||||
|
|
||||||
|
{Group: "flowcontrol.k8s.io", Version: "v1", Kind: "FlowSchema"}: {"Age"},
|
||||||
|
{Group: "flowcontrol.k8s.io", Version: "v1", Kind: "PriorityLevelConfiguration"}: {"Age"},
|
||||||
|
|
||||||
|
{Group: "networking.k8s.io", Version: "v1beta1", Kind: "Ingress"}: {"Age"},
|
||||||
|
{Group: "networking.k8s.io", Version: "v1beta1", Kind: "IngressClass"}: {"Age"},
|
||||||
|
{Group: "networking.k8s.io", Version: "v1beta1", Kind: "IPAddress"}: {"Age"},
|
||||||
|
{Group: "networking.k8s.io", Version: "v1beta1", Kind: "NetworkPolicy"}: {"Age"},
|
||||||
|
{Group: "networking.k8s.io", Version: "v1beta1", Kind: "ServiceCIDR"}: {"Age"},
|
||||||
|
|
||||||
|
{Group: "node.k8s.io", Version: "v1", Kind: "RuntimeClass"}: {"Age"},
|
||||||
|
|
||||||
|
{Group: "policy", Version: "v1", Kind: "PodDisruptionBudget"}: {"Age"},
|
||||||
|
|
||||||
|
{Group: "rbac.authorization.k8s.io", Version: "v1beta1", Kind: "ClusterRoleBinding"}: {"Age"},
|
||||||
|
{Group: "rbac.authorization.k8s.io", Version: "v1beta1", Kind: "RoleBinding"}: {"Age"},
|
||||||
|
|
||||||
|
{Group: "resource.k8s.io", Version: "v1beta1", Kind: "DeviceClass"}: {"Age"},
|
||||||
|
{Group: "resource.k8s.io", Version: "v1beta1", Kind: "ResourceClaim"}: {"Age"},
|
||||||
|
{Group: "resource.k8s.io", Version: "v1beta1", Kind: "ResourceClaimTemplate"}: {"Age"},
|
||||||
|
{Group: "resource.k8s.io", Version: "v1beta1", Kind: "ResourceSlice"}: {"Age"},
|
||||||
|
|
||||||
|
{Group: "scheduling.k8s.io", Version: "v1", Kind: "PriorityClass"}: {"Age"},
|
||||||
|
|
||||||
|
{Group: "storage.k8s.io", Version: "v1", Kind: "CSIDriver"}: {"Age"},
|
||||||
|
{Group: "storage.k8s.io", Version: "v1", Kind: "CSINode"}: {"Age"},
|
||||||
|
{Group: "storage.k8s.io", Version: "v1", Kind: "StorageClass"}: {"Age"},
|
||||||
|
{Group: "storage.k8s.io", Version: "v1", Kind: "VolumeAttachment"}: {"Age"},
|
||||||
|
{Group: "storage.k8s.io", Version: "v1beta1", Kind: "VolumeAttributesClass"}: {"Age"},
|
||||||
|
}
|
41
pkg/resources/common/util.go
Normal file
41
pkg/resources/common/util.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rancher/apiserver/pkg/types"
|
||||||
|
"github.com/rancher/steve/pkg/attributes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetIndexValueFromString looks for values between [ ].
|
||||||
|
// e.g: $.metadata.fields[2], in this case it would return 2
|
||||||
|
// In case it doesn't find any value between brackets it returns -1
|
||||||
|
func GetIndexValueFromString(pathString string) int {
|
||||||
|
idxStart := strings.Index(pathString, "[")
|
||||||
|
if idxStart == -1 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
idxEnd := strings.Index(pathString[idxStart+1:], "]")
|
||||||
|
if idxEnd == -1 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
idx, err := strconv.Atoi(pathString[idxStart+1 : idxStart+1+idxEnd])
|
||||||
|
if err != nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return idx
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetColumnDefinitions returns ColumnDefinitions from an APISchema
|
||||||
|
func GetColumnDefinitions(schema *types.APISchema) []ColumnDefinition {
|
||||||
|
columns := attributes.Columns(schema)
|
||||||
|
if columns == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
colDefs, ok := columns.([]ColumnDefinition)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return colDefs
|
||||||
|
}
|
@ -47,9 +47,10 @@ func DefaultSchemaTemplates(cf *client.Factory,
|
|||||||
summaryCache *summarycache.SummaryCache,
|
summaryCache *summarycache.SummaryCache,
|
||||||
lookup accesscontrol.AccessSetLookup,
|
lookup accesscontrol.AccessSetLookup,
|
||||||
discovery discovery.DiscoveryInterface,
|
discovery discovery.DiscoveryInterface,
|
||||||
namespaceCache corecontrollers.NamespaceCache) []schema.Template {
|
namespaceCache corecontrollers.NamespaceCache,
|
||||||
|
options common.TemplateOptions) []schema.Template {
|
||||||
return []schema.Template{
|
return []schema.Template{
|
||||||
common.DefaultTemplate(cf, summaryCache, lookup, namespaceCache),
|
common.DefaultTemplate(cf, summaryCache, lookup, namespaceCache, options),
|
||||||
apigroups.Template(discovery),
|
apigroups.Template(discovery),
|
||||||
{
|
{
|
||||||
ID: "configmap",
|
ID: "configmap",
|
||||||
@ -77,10 +78,11 @@ func DefaultSchemaTemplatesForStore(store types.Store,
|
|||||||
baseSchemas *types.APISchemas,
|
baseSchemas *types.APISchemas,
|
||||||
summaryCache *summarycache.SummaryCache,
|
summaryCache *summarycache.SummaryCache,
|
||||||
lookup accesscontrol.AccessSetLookup,
|
lookup accesscontrol.AccessSetLookup,
|
||||||
discovery discovery.DiscoveryInterface) []schema.Template {
|
discovery discovery.DiscoveryInterface,
|
||||||
|
options common.TemplateOptions) []schema.Template {
|
||||||
|
|
||||||
return []schema.Template{
|
return []schema.Template{
|
||||||
common.DefaultTemplateForStore(store, summaryCache, lookup),
|
common.DefaultTemplateForStore(store, summaryCache, lookup, options),
|
||||||
apigroups.Template(discovery),
|
apigroups.Template(discovery),
|
||||||
{
|
{
|
||||||
ID: "configmap",
|
ID: "configmap",
|
||||||
|
@ -4,7 +4,10 @@ package virtual
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
rescommon "github.com/rancher/steve/pkg/resources/common"
|
||||||
"github.com/rancher/steve/pkg/resources/virtual/clusters"
|
"github.com/rancher/steve/pkg/resources/virtual/clusters"
|
||||||
"github.com/rancher/steve/pkg/resources/virtual/common"
|
"github.com/rancher/steve/pkg/resources/virtual/common"
|
||||||
"github.com/rancher/steve/pkg/resources/virtual/events"
|
"github.com/rancher/steve/pkg/resources/virtual/events"
|
||||||
@ -13,6 +16,8 @@ import (
|
|||||||
"k8s.io/client-go/tools/cache"
|
"k8s.io/client-go/tools/cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var now = time.Now()
|
||||||
|
|
||||||
// TransformBuilder builds transform functions for specified GVKs through GetTransformFunc
|
// TransformBuilder builds transform functions for specified GVKs through GetTransformFunc
|
||||||
type TransformBuilder struct {
|
type TransformBuilder struct {
|
||||||
defaultFields *common.DefaultFields
|
defaultFields *common.DefaultFields
|
||||||
@ -28,13 +33,52 @@ func NewTransformBuilder(cache common.SummaryCache) *TransformBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetTransformFunc returns the func to transform a raw object into a fixed object, if needed
|
// GetTransformFunc returns the func to transform a raw object into a fixed object, if needed
|
||||||
func (t *TransformBuilder) GetTransformFunc(gvk schema.GroupVersionKind) cache.TransformFunc {
|
func (t *TransformBuilder) GetTransformFunc(gvk schema.GroupVersionKind, columns []rescommon.ColumnDefinition) cache.TransformFunc {
|
||||||
converters := make([]func(*unstructured.Unstructured) (*unstructured.Unstructured, error), 0)
|
converters := make([]func(*unstructured.Unstructured) (*unstructured.Unstructured, error), 0)
|
||||||
if gvk.Kind == "Event" && gvk.Group == "" && gvk.Version == "v1" {
|
if gvk.Kind == "Event" && gvk.Group == "" && gvk.Version == "v1" {
|
||||||
converters = append(converters, events.TransformEventObject)
|
converters = append(converters, events.TransformEventObject)
|
||||||
} else if gvk.Kind == "Cluster" && gvk.Group == "management.cattle.io" && gvk.Version == "v3" {
|
} else if gvk.Kind == "Cluster" && gvk.Group == "management.cattle.io" && gvk.Version == "v3" {
|
||||||
converters = append(converters, clusters.TransformManagedCluster)
|
converters = append(converters, clusters.TransformManagedCluster)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detecting if we need to convert date fields
|
||||||
|
for _, col := range columns {
|
||||||
|
gvkDateFields, gvkFound := rescommon.DateFieldsByGVKBuiltins[gvk]
|
||||||
|
if col.Type == "date" || (gvkFound && slices.Contains(gvkDateFields, col.Name)) {
|
||||||
|
converters = append(converters, func(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
|
||||||
|
index := rescommon.GetIndexValueFromString(col.Field)
|
||||||
|
if index == -1 {
|
||||||
|
return nil, fmt.Errorf("field index not found at column.Field struct variable: %s", col.Field)
|
||||||
|
}
|
||||||
|
|
||||||
|
curValue, got, err := unstructured.NestedSlice(obj.Object, "metadata", "fields")
|
||||||
|
if !got {
|
||||||
|
return obj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
value, cast := curValue[index].(string)
|
||||||
|
if !cast {
|
||||||
|
return nil, fmt.Errorf("could not cast metadata.fields %d to string", index)
|
||||||
|
}
|
||||||
|
duration, err := rescommon.ParseHumanReadableDuration(value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
curValue[index] = fmt.Sprintf("%d", now.Add(-duration).UnixMilli())
|
||||||
|
if err := unstructured.SetNestedSlice(obj.Object, curValue, "metadata", "fields"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
converters = append(converters, t.defaultFields.TransformCommon)
|
converters = append(converters, t.defaultFields.TransformCommon)
|
||||||
|
|
||||||
return func(raw interface{}) (interface{}, error) {
|
return func(raw interface{}) (interface{}, error) {
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
package virtual_test
|
package virtual
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/rancher/steve/pkg/resources/virtual"
|
rescommon "github.com/rancher/steve/pkg/resources/common"
|
||||||
"github.com/rancher/steve/pkg/resources/virtual/common"
|
"github.com/rancher/steve/pkg/resources/virtual/common"
|
||||||
"github.com/rancher/steve/pkg/summarycache"
|
"github.com/rancher/steve/pkg/summarycache"
|
||||||
"github.com/rancher/wrangler/v3/pkg/summary"
|
"github.com/rancher/wrangler/v3/pkg/summary"
|
||||||
@ -16,11 +17,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestTransformChain(t *testing.T) {
|
func TestTransformChain(t *testing.T) {
|
||||||
|
now = func() time.Time { return time.Date(1992, 9, 2, 0, 0, 0, 0, time.UTC) }()
|
||||||
|
noColumns := []rescommon.ColumnDefinition{}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
input any
|
input any
|
||||||
hasSummary *summary.SummarizedObject
|
hasSummary *summary.SummarizedObject
|
||||||
hasRelationships []summarycache.Relationship
|
hasRelationships []summarycache.Relationship
|
||||||
|
columns []rescommon.ColumnDefinition
|
||||||
wantOutput any
|
wantOutput any
|
||||||
wantError bool
|
wantError bool
|
||||||
}{
|
}{
|
||||||
@ -65,6 +69,7 @@ func TestTransformChain(t *testing.T) {
|
|||||||
"id": "old-id",
|
"id": "old-id",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
columns: noColumns,
|
||||||
wantOutput: &unstructured.Unstructured{
|
wantOutput: &unstructured.Unstructured{
|
||||||
Object: map[string]interface{}{
|
Object: map[string]interface{}{
|
||||||
"apiVersion": "test.cattle.io/v1",
|
"apiVersion": "test.cattle.io/v1",
|
||||||
@ -94,6 +99,131 @@ func TestTransformChain(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "CRD metadata.fields has a date field - should convert to timestamp",
|
||||||
|
hasSummary: &summary.SummarizedObject{
|
||||||
|
PartialObjectMetadata: v1.PartialObjectMetadata{
|
||||||
|
ObjectMeta: v1.ObjectMeta{
|
||||||
|
Name: "testobj",
|
||||||
|
Namespace: "test-ns",
|
||||||
|
},
|
||||||
|
TypeMeta: v1.TypeMeta{
|
||||||
|
APIVersion: "test.cattle.io/v1",
|
||||||
|
Kind: "TestResource",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Summary: summary.Summary{
|
||||||
|
State: "success",
|
||||||
|
Transitioning: false,
|
||||||
|
Error: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
input: &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "test.cattle.io/v1",
|
||||||
|
"kind": "TestResource",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "testobj",
|
||||||
|
"namespace": "test-ns",
|
||||||
|
"fields": []interface{}{"1d"},
|
||||||
|
},
|
||||||
|
"id": "old-id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
columns: []rescommon.ColumnDefinition{
|
||||||
|
{
|
||||||
|
Field: "metadata.fields[0]",
|
||||||
|
TableColumnDefinition: v1.TableColumnDefinition{
|
||||||
|
Name: "Age",
|
||||||
|
Type: "date",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantOutput: &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "test.cattle.io/v1",
|
||||||
|
"kind": "TestResource",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "testobj",
|
||||||
|
"namespace": "test-ns",
|
||||||
|
"relationships": []any(nil),
|
||||||
|
"state": map[string]interface{}{
|
||||||
|
"name": "success",
|
||||||
|
"error": false,
|
||||||
|
"transitioning": false,
|
||||||
|
"message": "",
|
||||||
|
},
|
||||||
|
"fields": []interface{}{
|
||||||
|
fmt.Sprintf("%d", now.Add(-24*time.Hour).UnixMilli()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"id": "test-ns/testobj",
|
||||||
|
"_id": "old-id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "built-in type metadata.fields has a date field - should convert to timestamp",
|
||||||
|
hasSummary: &summary.SummarizedObject{
|
||||||
|
PartialObjectMetadata: v1.PartialObjectMetadata{
|
||||||
|
ObjectMeta: v1.ObjectMeta{
|
||||||
|
Name: "testobj",
|
||||||
|
Namespace: "test-ns",
|
||||||
|
},
|
||||||
|
TypeMeta: v1.TypeMeta{
|
||||||
|
APIVersion: "apps/v1",
|
||||||
|
Kind: "Deployment",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Summary: summary.Summary{
|
||||||
|
State: "success",
|
||||||
|
Transitioning: false,
|
||||||
|
Error: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
input: &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "apps/v1",
|
||||||
|
"kind": "Deployment",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "testobj",
|
||||||
|
"namespace": "test-ns",
|
||||||
|
"fields": []interface{}{"1d"},
|
||||||
|
},
|
||||||
|
"id": "old-id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
columns: []rescommon.ColumnDefinition{
|
||||||
|
{
|
||||||
|
TableColumnDefinition: v1.TableColumnDefinition{
|
||||||
|
Name: "Age",
|
||||||
|
},
|
||||||
|
Field: "metadata.fields[0]",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantOutput: &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "apps/v1",
|
||||||
|
"kind": "Deployment",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "testobj",
|
||||||
|
"namespace": "test-ns",
|
||||||
|
"relationships": []any(nil),
|
||||||
|
"state": map[string]interface{}{
|
||||||
|
"name": "success",
|
||||||
|
"error": false,
|
||||||
|
"transitioning": false,
|
||||||
|
"message": "",
|
||||||
|
},
|
||||||
|
"fields": []interface{}{
|
||||||
|
fmt.Sprintf("%d", now.Add(-24*time.Hour).UnixMilli()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"id": "test-ns/testobj",
|
||||||
|
"_id": "old-id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "processable event",
|
name: "processable event",
|
||||||
input: &unstructured.Unstructured{
|
input: &unstructured.Unstructured{
|
||||||
@ -118,6 +248,7 @@ func TestTransformChain(t *testing.T) {
|
|||||||
"type": "Gorniplatz",
|
"type": "Gorniplatz",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
columns: noColumns,
|
||||||
wantOutput: &unstructured.Unstructured{
|
wantOutput: &unstructured.Unstructured{
|
||||||
Object: map[string]interface{}{
|
Object: map[string]interface{}{
|
||||||
"apiVersion": "/v1",
|
"apiVersion": "/v1",
|
||||||
@ -162,6 +293,7 @@ func TestTransformChain(t *testing.T) {
|
|||||||
"type": "Gorniplatz",
|
"type": "Gorniplatz",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
columns: noColumns,
|
||||||
wantOutput: &unstructured.Unstructured{
|
wantOutput: &unstructured.Unstructured{
|
||||||
Object: map[string]interface{}{
|
Object: map[string]interface{}{
|
||||||
"apiVersion": "palau.io/v1",
|
"apiVersion": "palau.io/v1",
|
||||||
@ -218,6 +350,7 @@ func TestTransformChain(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
columns: noColumns,
|
||||||
wantOutput: &unstructured.Unstructured{
|
wantOutput: &unstructured.Unstructured{
|
||||||
Object: map[string]interface{}{
|
Object: map[string]interface{}{
|
||||||
"apiVersion": "management.cattle.io/v3",
|
"apiVersion": "management.cattle.io/v3",
|
||||||
@ -302,6 +435,7 @@ func TestTransformChain(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
columns: noColumns,
|
||||||
wantOutput: &unstructured.Unstructured{
|
wantOutput: &unstructured.Unstructured{
|
||||||
Object: map[string]interface{}{
|
Object: map[string]interface{}{
|
||||||
"apiVersion": "management.cattle.io/v3",
|
"apiVersion": "management.cattle.io/v3",
|
||||||
@ -346,13 +480,14 @@ func TestTransformChain(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
fakeCache := common.FakeSummaryCache{
|
fakeCache := common.FakeSummaryCache{
|
||||||
SummarizedObject: test.hasSummary,
|
SummarizedObject: test.hasSummary,
|
||||||
Relationships: test.hasRelationships,
|
Relationships: test.hasRelationships,
|
||||||
}
|
}
|
||||||
tb := virtual.NewTransformBuilder(&fakeCache)
|
tb := NewTransformBuilder(&fakeCache)
|
||||||
raw, isSignal, err := common.GetUnstructured(test.input)
|
raw, isSignal, err := common.GetUnstructured(test.input)
|
||||||
require.False(t, isSignal)
|
require.False(t, isSignal)
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
@ -362,7 +497,7 @@ func TestTransformChain(t *testing.T) {
|
|||||||
if test.name == "a non-ready cluster" {
|
if test.name == "a non-ready cluster" {
|
||||||
fmt.Printf("Stop here")
|
fmt.Printf("Stop here")
|
||||||
}
|
}
|
||||||
output, err := tb.GetTransformFunc(gvk)(test.input)
|
output, err := tb.GetTransformFunc(gvk, test.columns)(test.input)
|
||||||
require.Equal(t, test.wantOutput, output)
|
require.Equal(t, test.wantOutput, output)
|
||||||
if test.wantError {
|
if test.wantError {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
@ -224,7 +224,7 @@ func setup(ctx context.Context, server *Server) error {
|
|||||||
store := metricsStore.NewMetricsStore(errStore)
|
store := metricsStore.NewMetricsStore(errStore)
|
||||||
// end store setup code
|
// end store setup code
|
||||||
|
|
||||||
for _, template := range resources.DefaultSchemaTemplatesForStore(store, server.BaseSchemas, summaryCache, asl, server.controllers.K8s.Discovery()) {
|
for _, template := range resources.DefaultSchemaTemplatesForStore(store, server.BaseSchemas, summaryCache, asl, server.controllers.K8s.Discovery(), common.TemplateOptions{InSQLMode: true}) {
|
||||||
sf.AddTemplate(template)
|
sf.AddTemplate(template)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,7 +238,7 @@ func setup(ctx context.Context, server *Server) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for _, template := range resources.DefaultSchemaTemplates(cf, server.BaseSchemas, summaryCache, asl, server.controllers.K8s.Discovery(), server.controllers.Core.Namespace().Cache()) {
|
for _, template := range resources.DefaultSchemaTemplates(cf, server.BaseSchemas, summaryCache, asl, server.controllers.K8s.Discovery(), server.controllers.Core.Namespace().Cache(), common.TemplateOptions{InSQLMode: false}) {
|
||||||
sf.AddTemplate(template)
|
sf.AddTemplate(template)
|
||||||
}
|
}
|
||||||
onSchemasHandler = ccache.OnSchemas
|
onSchemasHandler = ccache.OnSchemas
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
reflect "reflect"
|
reflect "reflect"
|
||||||
|
|
||||||
types "github.com/rancher/apiserver/pkg/types"
|
types "github.com/rancher/apiserver/pkg/types"
|
||||||
|
common "github.com/rancher/steve/pkg/resources/common"
|
||||||
factory "github.com/rancher/steve/pkg/sqlcache/informer/factory"
|
factory "github.com/rancher/steve/pkg/sqlcache/informer/factory"
|
||||||
partition "github.com/rancher/steve/pkg/sqlcache/partition"
|
partition "github.com/rancher/steve/pkg/sqlcache/partition"
|
||||||
sqltypes "github.com/rancher/steve/pkg/sqlcache/sqltypes"
|
sqltypes "github.com/rancher/steve/pkg/sqlcache/sqltypes"
|
||||||
@ -389,15 +390,15 @@ func (m *MockTransformBuilder) EXPECT() *MockTransformBuilderMockRecorder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetTransformFunc mocks base method.
|
// GetTransformFunc mocks base method.
|
||||||
func (m *MockTransformBuilder) GetTransformFunc(arg0 schema.GroupVersionKind) cache.TransformFunc {
|
func (m *MockTransformBuilder) GetTransformFunc(arg0 schema.GroupVersionKind, arg1 []common.ColumnDefinition) cache.TransformFunc {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "GetTransformFunc", arg0)
|
ret := m.ctrl.Call(m, "GetTransformFunc", arg0, arg1)
|
||||||
ret0, _ := ret[0].(cache.TransformFunc)
|
ret0, _ := ret[0].(cache.TransformFunc)
|
||||||
return ret0
|
return ret0
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTransformFunc indicates an expected call of GetTransformFunc.
|
// GetTransformFunc indicates an expected call of GetTransformFunc.
|
||||||
func (mr *MockTransformBuilderMockRecorder) GetTransformFunc(arg0 any) *gomock.Call {
|
func (mr *MockTransformBuilderMockRecorder) GetTransformFunc(arg0, arg1 any) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTransformFunc", reflect.TypeOf((*MockTransformBuilder)(nil).GetTransformFunc), arg0)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTransformFunc", reflect.TypeOf((*MockTransformBuilder)(nil).GetTransformFunc), arg0, arg1)
|
||||||
}
|
}
|
||||||
|
@ -240,7 +240,7 @@ type RelationshipNotifier interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TransformBuilder interface {
|
type TransformBuilder interface {
|
||||||
GetTransformFunc(gvk schema.GroupVersionKind) cache.TransformFunc
|
GetTransformFunc(gvk schema.GroupVersionKind, colDefs []common.ColumnDefinition) cache.TransformFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
type Store struct {
|
type Store struct {
|
||||||
@ -335,9 +335,10 @@ func (s *Store) initializeNamespaceCache() error {
|
|||||||
|
|
||||||
// get any type-specific fields that steve is interested in
|
// get any type-specific fields that steve is interested in
|
||||||
fields = append(fields, getFieldForGVK(gvk)...)
|
fields = append(fields, getFieldForGVK(gvk)...)
|
||||||
|
cols := common.GetColumnDefinitions(&nsSchema)
|
||||||
|
|
||||||
// get the type-specific transform func
|
// get the type-specific transform func
|
||||||
transformFunc := s.transformBuilder.GetTransformFunc(gvk)
|
transformFunc := s.transformBuilder.GetTransformFunc(gvk, cols)
|
||||||
|
|
||||||
// get the ns informer
|
// get the ns informer
|
||||||
tableClient := &tablelistconvert.Client{ResourceInterface: client}
|
tableClient := &tablelistconvert.Client{ResourceInterface: client}
|
||||||
@ -545,7 +546,7 @@ func (s *Store) watch(apiOp *types.APIRequest, schema *types.APISchema, w types.
|
|||||||
gvk := attributes.GVK(schema)
|
gvk := attributes.GVK(schema)
|
||||||
fields := getFieldsFromSchema(schema)
|
fields := getFieldsFromSchema(schema)
|
||||||
fields = append(fields, getFieldForGVK(gvk)...)
|
fields = append(fields, getFieldForGVK(gvk)...)
|
||||||
transformFunc := s.transformBuilder.GetTransformFunc(gvk)
|
transformFunc := s.transformBuilder.GetTransformFunc(gvk, nil)
|
||||||
tableClient := &tablelistconvert.Client{ResourceInterface: client}
|
tableClient := &tablelistconvert.Client{ResourceInterface: client}
|
||||||
attrs := attributes.GVK(schema)
|
attrs := attributes.GVK(schema)
|
||||||
ns := attributes.Namespaced(schema)
|
ns := attributes.Namespaced(schema)
|
||||||
@ -756,7 +757,9 @@ func (s *Store) ListByPartitions(apiOp *types.APIRequest, apiSchema *types.APISc
|
|||||||
gvk := attributes.GVK(apiSchema)
|
gvk := attributes.GVK(apiSchema)
|
||||||
fields := getFieldsFromSchema(apiSchema)
|
fields := getFieldsFromSchema(apiSchema)
|
||||||
fields = append(fields, getFieldForGVK(gvk)...)
|
fields = append(fields, getFieldForGVK(gvk)...)
|
||||||
transformFunc := s.transformBuilder.GetTransformFunc(gvk)
|
cols := common.GetColumnDefinitions(apiSchema)
|
||||||
|
|
||||||
|
transformFunc := s.transformBuilder.GetTransformFunc(gvk, cols)
|
||||||
tableClient := &tablelistconvert.Client{ResourceInterface: client}
|
tableClient := &tablelistconvert.Client{ResourceInterface: client}
|
||||||
attrs := attributes.GVK(apiSchema)
|
attrs := attributes.GVK(apiSchema)
|
||||||
ns := attributes.Namespaced(apiSchema)
|
ns := attributes.Namespaced(apiSchema)
|
||||||
|
@ -245,7 +245,7 @@ func TestListByPartitions(t *testing.T) {
|
|||||||
cg.EXPECT().TableAdminClient(req, schema, "", &WarningBuffer{}).Return(ri, nil)
|
cg.EXPECT().TableAdminClient(req, schema, "", &WarningBuffer{}).Return(ri, nil)
|
||||||
// This tests that fields are being extracted from schema columns and the type specific fields map
|
// This tests that fields are being extracted from schema columns and the type specific fields map
|
||||||
cf.EXPECT().CacheFor(context.Background(), [][]string{{"some", "field"}, {`id`}, {`metadata`, `state`, `name`}, {"gvk", "specific", "fields"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(schema), attributes.Namespaced(schema), true).Return(c, nil)
|
cf.EXPECT().CacheFor(context.Background(), [][]string{{"some", "field"}, {`id`}, {`metadata`, `state`, `name`}, {"gvk", "specific", "fields"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(schema), attributes.Namespaced(schema), true).Return(c, nil)
|
||||||
tb.EXPECT().GetTransformFunc(attributes.GVK(schema)).Return(func(obj interface{}) (interface{}, error) { return obj, nil })
|
tb.EXPECT().GetTransformFunc(attributes.GVK(schema), []common.ColumnDefinition{{Field: "some.field"}}).Return(func(obj interface{}) (interface{}, error) { return obj, nil })
|
||||||
bloi.EXPECT().ListByOptions(req.Context(), &opts, partitions, req.Namespace).Return(listToReturn, len(listToReturn.Items), "", nil)
|
bloi.EXPECT().ListByOptions(req.Context(), &opts, partitions, req.Namespace).Return(listToReturn, len(listToReturn.Items), "", nil)
|
||||||
list, total, contToken, err := s.ListByPartitions(req, schema, partitions)
|
list, total, contToken, err := s.ListByPartitions(req, schema, partitions)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
@ -462,7 +462,7 @@ func TestListByPartitions(t *testing.T) {
|
|||||||
// note also the watchable bool is expected to be false
|
// note also the watchable bool is expected to be false
|
||||||
cf.EXPECT().CacheFor(context.Background(), [][]string{{"some", "field"}, {`id`}, {`metadata`, `state`, `name`}, {"gvk", "specific", "fields"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(schema), attributes.Namespaced(schema), false).Return(c, nil)
|
cf.EXPECT().CacheFor(context.Background(), [][]string{{"some", "field"}, {`id`}, {`metadata`, `state`, `name`}, {"gvk", "specific", "fields"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(schema), attributes.Namespaced(schema), false).Return(c, nil)
|
||||||
|
|
||||||
tb.EXPECT().GetTransformFunc(attributes.GVK(schema)).Return(func(obj interface{}) (interface{}, error) { return obj, nil })
|
tb.EXPECT().GetTransformFunc(attributes.GVK(schema), []common.ColumnDefinition{{Field: "some.field"}}).Return(func(obj interface{}) (interface{}, error) { return obj, nil })
|
||||||
bloi.EXPECT().ListByOptions(req.Context(), &opts, partitions, req.Namespace).Return(listToReturn, len(listToReturn.Items), "", nil)
|
bloi.EXPECT().ListByOptions(req.Context(), &opts, partitions, req.Namespace).Return(listToReturn, len(listToReturn.Items), "", nil)
|
||||||
list, total, contToken, err := s.ListByPartitions(req, schema, partitions)
|
list, total, contToken, err := s.ListByPartitions(req, schema, partitions)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
@ -534,7 +534,7 @@ func TestListByPartitions(t *testing.T) {
|
|||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
cg.EXPECT().TableAdminClient(req, schema, "", &WarningBuffer{}).Return(ri, nil)
|
cg.EXPECT().TableAdminClient(req, schema, "", &WarningBuffer{}).Return(ri, nil)
|
||||||
// This tests that fields are being extracted from schema columns and the type specific fields map
|
// This tests that fields are being extracted from schema columns and the type specific fields map
|
||||||
tb.EXPECT().GetTransformFunc(attributes.GVK(schema)).Return(func(obj interface{}) (interface{}, error) { return obj, nil })
|
tb.EXPECT().GetTransformFunc(attributes.GVK(schema), gomock.Any()).Return(func(obj interface{}) (interface{}, error) { return obj, nil })
|
||||||
cf.EXPECT().CacheFor(context.Background(), [][]string{{"some", "field"}, {`id`}, {`metadata`, `state`, `name`}, {"gvk", "specific", "fields"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(schema), attributes.Namespaced(schema), true).Return(factory.Cache{}, fmt.Errorf("error"))
|
cf.EXPECT().CacheFor(context.Background(), [][]string{{"some", "field"}, {`id`}, {`metadata`, `state`, `name`}, {"gvk", "specific", "fields"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(schema), attributes.Namespaced(schema), true).Return(factory.Cache{}, fmt.Errorf("error"))
|
||||||
|
|
||||||
_, _, _, err = s.ListByPartitions(req, schema, partitions)
|
_, _, _, err = s.ListByPartitions(req, schema, partitions)
|
||||||
@ -613,7 +613,7 @@ func TestListByPartitions(t *testing.T) {
|
|||||||
// This tests that fields are being extracted from schema columns and the type specific fields map
|
// This tests that fields are being extracted from schema columns and the type specific fields map
|
||||||
cf.EXPECT().CacheFor(context.Background(), [][]string{{"some", "field"}, {`id`}, {`metadata`, `state`, `name`}, {"gvk", "specific", "fields"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(schema), attributes.Namespaced(schema), true).Return(c, nil)
|
cf.EXPECT().CacheFor(context.Background(), [][]string{{"some", "field"}, {`id`}, {`metadata`, `state`, `name`}, {"gvk", "specific", "fields"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(schema), attributes.Namespaced(schema), true).Return(c, nil)
|
||||||
bloi.EXPECT().ListByOptions(req.Context(), &opts, partitions, req.Namespace).Return(nil, 0, "", fmt.Errorf("error"))
|
bloi.EXPECT().ListByOptions(req.Context(), &opts, partitions, req.Namespace).Return(nil, 0, "", fmt.Errorf("error"))
|
||||||
tb.EXPECT().GetTransformFunc(attributes.GVK(schema)).Return(func(obj interface{}) (interface{}, error) { return obj, nil })
|
tb.EXPECT().GetTransformFunc(attributes.GVK(schema), gomock.Any()).Return(func(obj interface{}) (interface{}, error) { return obj, nil })
|
||||||
|
|
||||||
_, _, _, err = s.ListByPartitions(req, schema, partitions)
|
_, _, _, err = s.ListByPartitions(req, schema, partitions)
|
||||||
assert.NotNil(t, err)
|
assert.NotNil(t, err)
|
||||||
@ -725,7 +725,7 @@ func TestListByPartitionWithUserAccess(t *testing.T) {
|
|||||||
attributes.SetGVK(theSchema, gvk)
|
attributes.SetGVK(theSchema, gvk)
|
||||||
cg.EXPECT().TableAdminClient(apiOp, theSchema, "", &WarningBuffer{}).Return(ri, nil)
|
cg.EXPECT().TableAdminClient(apiOp, theSchema, "", &WarningBuffer{}).Return(ri, nil)
|
||||||
cf.EXPECT().CacheFor(context.Background(), [][]string{{"some", "field"}, {"id"}, {"metadata", "state", "name"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(theSchema), attributes.Namespaced(theSchema), true).Return(c, nil)
|
cf.EXPECT().CacheFor(context.Background(), [][]string{{"some", "field"}, {"id"}, {"metadata", "state", "name"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(theSchema), attributes.Namespaced(theSchema), true).Return(c, nil)
|
||||||
tb.EXPECT().GetTransformFunc(attributes.GVK(theSchema)).Return(func(obj interface{}) (interface{}, error) { return obj, nil })
|
tb.EXPECT().GetTransformFunc(attributes.GVK(theSchema), gomock.Any()).Return(func(obj interface{}) (interface{}, error) { return obj, nil })
|
||||||
|
|
||||||
listToReturn := &unstructured.UnstructuredList{
|
listToReturn := &unstructured.UnstructuredList{
|
||||||
Items: make([]unstructured.Unstructured, 0, 0),
|
Items: make([]unstructured.Unstructured, 0, 0),
|
||||||
@ -767,7 +767,7 @@ func TestReset(t *testing.T) {
|
|||||||
cs.EXPECT().SetColumns(gomock.Any(), gomock.Any()).Return(nil)
|
cs.EXPECT().SetColumns(gomock.Any(), gomock.Any()).Return(nil)
|
||||||
cg.EXPECT().TableAdminClient(nil, &nsSchema, "", &WarningBuffer{}).Return(ri, nil)
|
cg.EXPECT().TableAdminClient(nil, &nsSchema, "", &WarningBuffer{}).Return(ri, nil)
|
||||||
cf.EXPECT().CacheFor(context.Background(), [][]string{{`id`}, {`metadata`, `state`, `name`}, {"metadata", "labels", "field.cattle.io/projectId"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(&nsSchema), false, true).Return(nsc2, nil)
|
cf.EXPECT().CacheFor(context.Background(), [][]string{{`id`}, {`metadata`, `state`, `name`}, {"metadata", "labels", "field.cattle.io/projectId"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(&nsSchema), false, true).Return(nsc2, nil)
|
||||||
tb.EXPECT().GetTransformFunc(attributes.GVK(&nsSchema)).Return(func(obj interface{}) (interface{}, error) { return obj, nil })
|
tb.EXPECT().GetTransformFunc(attributes.GVK(&nsSchema), gomock.Any()).Return(func(obj interface{}) (interface{}, error) { return obj, nil })
|
||||||
err := s.Reset()
|
err := s.Reset()
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, nsc2, s.namespaceCache)
|
assert.Equal(t, nsc2, s.namespaceCache)
|
||||||
@ -874,7 +874,7 @@ func TestReset(t *testing.T) {
|
|||||||
cs.EXPECT().SetColumns(gomock.Any(), gomock.Any()).Return(nil)
|
cs.EXPECT().SetColumns(gomock.Any(), gomock.Any()).Return(nil)
|
||||||
cg.EXPECT().TableAdminClient(nil, &nsSchema, "", &WarningBuffer{}).Return(ri, nil)
|
cg.EXPECT().TableAdminClient(nil, &nsSchema, "", &WarningBuffer{}).Return(ri, nil)
|
||||||
cf.EXPECT().CacheFor(context.Background(), [][]string{{`id`}, {`metadata`, `state`, `name`}, {"metadata", "labels", "field.cattle.io/projectId"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(&nsSchema), false, true).Return(factory.Cache{}, fmt.Errorf("error"))
|
cf.EXPECT().CacheFor(context.Background(), [][]string{{`id`}, {`metadata`, `state`, `name`}, {"metadata", "labels", "field.cattle.io/projectId"}}, gomock.Any(), &tablelistconvert.Client{ResourceInterface: ri}, attributes.GVK(&nsSchema), false, true).Return(factory.Cache{}, fmt.Errorf("error"))
|
||||||
tb.EXPECT().GetTransformFunc(attributes.GVK(&nsSchema)).Return(func(obj interface{}) (interface{}, error) { return obj, nil })
|
tb.EXPECT().GetTransformFunc(attributes.GVK(&nsSchema), gomock.Any()).Return(func(obj interface{}) (interface{}, error) { return obj, nil })
|
||||||
err := s.Reset()
|
err := s.Reset()
|
||||||
assert.NotNil(t, err)
|
assert.NotNil(t, err)
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user