mirror of
https://github.com/rancher/steve.git
synced 2025-09-17 15:58:41 +00:00
Add an attribute observedgeneration (#714)
* Add an attribute observedgeneration * add unit tests
This commit is contained in:
committed by
GitHub
parent
a020084518
commit
6946ffe8aa
2
go.mod
2
go.mod
@@ -26,7 +26,7 @@ require (
|
|||||||
github.com/rancher/lasso v0.2.3
|
github.com/rancher/lasso v0.2.3
|
||||||
github.com/rancher/norman v0.7.0
|
github.com/rancher/norman v0.7.0
|
||||||
github.com/rancher/remotedialer v0.4.5-rc.3
|
github.com/rancher/remotedialer v0.4.5-rc.3
|
||||||
github.com/rancher/wrangler/v3 v3.2.2
|
github.com/rancher/wrangler/v3 v3.2.3-rc.2
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/urfave/cli/v2 v2.27.7
|
github.com/urfave/cli/v2 v2.27.7
|
||||||
|
4
go.sum
4
go.sum
@@ -226,8 +226,8 @@ github.com/rancher/norman v0.7.0 h1:duBZxekBj13k/2RTyWKZgV/ntXkIXm0sRKqwFO8ui+I=
|
|||||||
github.com/rancher/norman v0.7.0/go.mod h1:IOQn3CNCms6UK72QHujesLKedqZh4+SP8/FDEFc+7Ns=
|
github.com/rancher/norman v0.7.0/go.mod h1:IOQn3CNCms6UK72QHujesLKedqZh4+SP8/FDEFc+7Ns=
|
||||||
github.com/rancher/remotedialer v0.4.5-rc.3 h1:Bfik0ZF89Bpm13ft1GPVg3/0xzCO+c0N0yD9jmlavXc=
|
github.com/rancher/remotedialer v0.4.5-rc.3 h1:Bfik0ZF89Bpm13ft1GPVg3/0xzCO+c0N0yD9jmlavXc=
|
||||||
github.com/rancher/remotedialer v0.4.5-rc.3/go.mod h1:N96a/IQXoP9JLc9cbeJEly3fLi8lnpXFeFOJofgJBbA=
|
github.com/rancher/remotedialer v0.4.5-rc.3/go.mod h1:N96a/IQXoP9JLc9cbeJEly3fLi8lnpXFeFOJofgJBbA=
|
||||||
github.com/rancher/wrangler/v3 v3.2.2 h1:IK1/v8n8gaZSB4izmJhGFXJt38Z8gkbwzl3Lo/e2jQc=
|
github.com/rancher/wrangler/v3 v3.2.3-rc.2 h1:CnbO8lT8ZwQF4PfrptfCwmYLfQBQR9BRxGvOtEiZZKU=
|
||||||
github.com/rancher/wrangler/v3 v3.2.2/go.mod h1:TA1QuuQxrtn/kmJbBLW/l24IcfHBmSXBa9an3IRlqQQ=
|
github.com/rancher/wrangler/v3 v3.2.3-rc.2/go.mod h1:TA1QuuQxrtn/kmJbBLW/l24IcfHBmSXBa9an3IRlqQQ=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
|
@@ -52,7 +52,7 @@ type clusterCache struct {
|
|||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
summaryClient client.Interface
|
summaryClient client.ExtendedInterface
|
||||||
watchers map[schema2.GroupVersionKind]*watcher
|
watchers map[schema2.GroupVersionKind]*watcher
|
||||||
workqueue workqueue.DelayingInterface
|
workqueue workqueue.DelayingInterface
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ type clusterCache struct {
|
|||||||
func NewClusterCache(ctx context.Context, dynamicClient dynamic.Interface) ClusterCache {
|
func NewClusterCache(ctx context.Context, dynamicClient dynamic.Interface) ClusterCache {
|
||||||
c := &clusterCache{
|
c := &clusterCache{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
summaryClient: client.NewForDynamicClient(dynamicClient),
|
summaryClient: client.NewForExtendedDynamicClient(dynamicClient),
|
||||||
watchers: map[schema2.GroupVersionKind]*watcher{},
|
watchers: map[schema2.GroupVersionKind]*watcher{},
|
||||||
workqueue: workqueue.NewNamedDelayingQueue("cluster-cache"),
|
workqueue: workqueue.NewNamedDelayingQueue("cluster-cache"),
|
||||||
}
|
}
|
||||||
@@ -147,7 +147,10 @@ func (h *clusterCache) OnSchemas(schemas *schema.Collection) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
summaryInformer := informer.NewFilteredSummaryInformer(h.summaryClient, gvr, metav1.NamespaceAll, 2*time.Hour,
|
opts := &client.Options{
|
||||||
|
Schema: schema.Schema,
|
||||||
|
}
|
||||||
|
summaryInformer := informer.NewFilteredSummaryInformerWithOptions(h.summaryClient, gvr, opts, metav1.NamespaceAll, 2*time.Hour,
|
||||||
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, nil)
|
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, nil)
|
||||||
ctx, cancel := context.WithCancel(h.ctx)
|
ctx, cancel := context.WithCancel(h.ctx)
|
||||||
w := &watcher{
|
w := &watcher{
|
||||||
|
@@ -29,7 +29,7 @@ func Register(ctx context.Context, apiSchemas *types.APISchemas, cg proxy.Client
|
|||||||
schema.ResourceMethods = []string{http.MethodGet}
|
schema.ResourceMethods = []string{http.MethodGet}
|
||||||
schema.Attributes["access"] = accesscontrol.AccessListByVerb{
|
schema.Attributes["access"] = accesscontrol.AccessListByVerb{
|
||||||
"watch": accesscontrol.AccessList{
|
"watch": accesscontrol.AccessList{
|
||||||
{
|
accesscontrol.Access{
|
||||||
Namespace: "*",
|
Namespace: "*",
|
||||||
ResourceName: "*",
|
ResourceName: "*",
|
||||||
},
|
},
|
||||||
|
@@ -10,6 +10,7 @@ import (
|
|||||||
"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/clustercache"
|
"github.com/rancher/steve/pkg/clustercache"
|
||||||
|
"github.com/rancher/wrangler/v3/pkg/schemas"
|
||||||
"github.com/rancher/wrangler/v3/pkg/summary"
|
"github.com/rancher/wrangler/v3/pkg/summary"
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
@@ -31,7 +32,7 @@ func Register(schemas *types.APISchemas, ccache clustercache.ClusterCache) {
|
|||||||
schema.ResourceMethods = []string{http.MethodGet}
|
schema.ResourceMethods = []string{http.MethodGet}
|
||||||
schema.Attributes["access"] = accesscontrol.AccessListByVerb{
|
schema.Attributes["access"] = accesscontrol.AccessListByVerb{
|
||||||
"watch": accesscontrol.AccessList{
|
"watch": accesscontrol.AccessList{
|
||||||
{
|
accesscontrol.Access{
|
||||||
Namespace: "*",
|
Namespace: "*",
|
||||||
ResourceName: "*",
|
ResourceName: "*",
|
||||||
},
|
},
|
||||||
@@ -151,7 +152,7 @@ func (s *Store) Watch(apiOp *types.APIRequest, schema *types.APISchema, w types.
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
_, namespace, revision, summary, ok := getInfo(obj)
|
_, namespace, revision, summary, ok := getInfo(obj, schema)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -162,7 +163,7 @@ func (s *Store) Watch(apiOp *types.APIRequest, schema *types.APISchema, w types.
|
|||||||
}
|
}
|
||||||
|
|
||||||
if oldObj != nil {
|
if oldObj != nil {
|
||||||
if _, _, _, oldSummary, ok := getInfo(oldObj); ok {
|
if _, _, _, oldSummary, ok := getInfo(oldObj, schema); ok {
|
||||||
if oldSummary.Transitioning == summary.Transitioning &&
|
if oldSummary.Transitioning == summary.Transitioning &&
|
||||||
oldSummary.Error == summary.Error &&
|
oldSummary.Error == summary.Error &&
|
||||||
simpleState(oldSummary) == simpleState(summary) {
|
simpleState(oldSummary) == simpleState(summary) {
|
||||||
@@ -230,7 +231,7 @@ func (s *Store) schemasToWatch(apiOp *types.APIRequest) (result []*types.APISche
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func getInfo(obj interface{}) (name string, namespace string, revision int, summaryResult summary.Summary, ok bool) {
|
func getInfo(obj interface{}, schema *types.APISchema) (name string, namespace string, revision int, summaryResult summary.Summary, ok bool) {
|
||||||
r, ok := obj.(runtime.Object)
|
r, ok := obj.(runtime.Object)
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", "", 0, summaryResult, false
|
return "", "", 0, summaryResult, false
|
||||||
@@ -246,7 +247,12 @@ func getInfo(obj interface{}) (name string, namespace string, revision int, summ
|
|||||||
return "", "", 0, summaryResult, false
|
return "", "", 0, summaryResult, false
|
||||||
}
|
}
|
||||||
|
|
||||||
summaryResult = summary.Summarize(r)
|
opts := &summary.SummarizeOptions{HasObservedGeneration: false}
|
||||||
|
if schema != nil && schema.Attributes != nil {
|
||||||
|
opts.HasObservedGeneration = schemas.HasObservedGeneration(schema.Schema)
|
||||||
|
}
|
||||||
|
|
||||||
|
summaryResult = summary.SummarizeWithOptions(r, opts)
|
||||||
return meta.GetName(), meta.GetNamespace(), revision, summaryResult, true
|
return meta.GetName(), meta.GetNamespace(), revision, summaryResult, true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,7 +330,7 @@ func (s *Store) getCount(apiOp *types.APIRequest) Count {
|
|||||||
all := access.Grants("list", "*", "*")
|
all := access.Grants("list", "*", "*")
|
||||||
|
|
||||||
for _, obj := range s.ccache.List(gvk) {
|
for _, obj := range s.ccache.List(gvk) {
|
||||||
name, ns, revision, summary, ok := getInfo(obj)
|
name, ns, revision, summary, ok := getInfo(obj, schema)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@@ -77,5 +77,27 @@ func forVersion(group, kind string, version v1.CustomResourceDefinitionVersion,
|
|||||||
}
|
}
|
||||||
if version.Schema != nil && version.Schema.OpenAPIV3Schema != nil {
|
if version.Schema != nil && version.Schema.OpenAPIV3Schema != nil {
|
||||||
schema.Description = version.Schema.OpenAPIV3Schema.Description
|
schema.Description = version.Schema.OpenAPIV3Schema.Description
|
||||||
|
|
||||||
|
if hasObservedGeneration(version.Schema.OpenAPIV3Schema) {
|
||||||
|
schemas.SetHasObservedGeneration(schema.Schema, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hasObservedGeneration(schema *v1.JSONSchemaProps) bool {
|
||||||
|
if schema == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if schema.Properties == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
status, ok := schema.Properties["status"]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if status.Properties == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, found := status.Properties["observedGeneration"]
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
@@ -306,3 +306,73 @@ func TestAddCustomResources(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
func TestHasObservedGeneration(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
schema *v1.JSONSchemaProps
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil schema",
|
||||||
|
schema: nil,
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil properties",
|
||||||
|
schema: &v1.JSONSchemaProps{},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no status property",
|
||||||
|
schema: &v1.JSONSchemaProps{
|
||||||
|
Properties: map[string]v1.JSONSchemaProps{
|
||||||
|
"foo": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "status property without properties",
|
||||||
|
schema: &v1.JSONSchemaProps{
|
||||||
|
Properties: map[string]v1.JSONSchemaProps{
|
||||||
|
"status": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "status property with properties but no observedGeneration",
|
||||||
|
schema: &v1.JSONSchemaProps{
|
||||||
|
Properties: map[string]v1.JSONSchemaProps{
|
||||||
|
"status": {
|
||||||
|
Properties: map[string]v1.JSONSchemaProps{
|
||||||
|
"foo": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "status property with observedGeneration",
|
||||||
|
schema: &v1.JSONSchemaProps{
|
||||||
|
Properties: map[string]v1.JSONSchemaProps{
|
||||||
|
"status": {
|
||||||
|
Properties: map[string]v1.JSONSchemaProps{
|
||||||
|
"observedGeneration": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := hasObservedGeneration(tt.schema)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -4,9 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rancher/wrangler/v3/pkg/generated/controllers/apiextensions.k8s.io"
|
apiextensions "github.com/rancher/wrangler/v3/pkg/generated/controllers/apiextensions.k8s.io"
|
||||||
apiextensionsv1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/apiextensions.k8s.io/v1"
|
apiextensionsv1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/apiextensions.k8s.io/v1"
|
||||||
"github.com/rancher/wrangler/v3/pkg/generated/controllers/apiregistration.k8s.io"
|
apiregistration "github.com/rancher/wrangler/v3/pkg/generated/controllers/apiregistration.k8s.io"
|
||||||
apiregistrationv1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/apiregistration.k8s.io/v1"
|
apiregistrationv1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/apiregistration.k8s.io/v1"
|
||||||
"github.com/rancher/wrangler/v3/pkg/generated/controllers/core"
|
"github.com/rancher/wrangler/v3/pkg/generated/controllers/core"
|
||||||
corev1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/core/v1"
|
corev1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/core/v1"
|
||||||
|
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/rancher/steve/pkg/clustercache"
|
"github.com/rancher/steve/pkg/clustercache"
|
||||||
"github.com/rancher/steve/pkg/schema"
|
"github.com/rancher/steve/pkg/schema"
|
||||||
"github.com/rancher/steve/pkg/schema/converter"
|
"github.com/rancher/steve/pkg/schema/converter"
|
||||||
|
wranglerSchemas "github.com/rancher/wrangler/v3/pkg/schemas"
|
||||||
"github.com/rancher/wrangler/v3/pkg/slice"
|
"github.com/rancher/wrangler/v3/pkg/slice"
|
||||||
"github.com/rancher/wrangler/v3/pkg/summary"
|
"github.com/rancher/wrangler/v3/pkg/summary"
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
@@ -106,7 +107,7 @@ func (s *SummaryCache) SummaryAndRelationship(obj runtime.Object) (*summary.Summ
|
|||||||
defer s.RUnlock()
|
defer s.RUnlock()
|
||||||
|
|
||||||
key := toKey(obj)
|
key := toKey(obj)
|
||||||
summarized := summary.Summarized(obj)
|
summarized := summary.SummarizedWithOptions(obj, getSummarizeOptions(obj, s.schemas))
|
||||||
|
|
||||||
relObjs, err := s.cache.ByIndex(relationshipIndex, key)
|
relObjs, err := s.cache.ByIndex(relationshipIndex, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -183,7 +184,7 @@ func (s *SummaryCache) toRel(ns string, rel *summary.Relationship) Relationship
|
|||||||
FromID: id,
|
FromID: id,
|
||||||
FromType: converter.GVKToSchemaID(runtimeschema.FromAPIVersionAndKind(rel.APIVersion, rel.Kind)),
|
FromType: converter.GVKToSchemaID(runtimeschema.FromAPIVersionAndKind(rel.APIVersion, rel.Kind)),
|
||||||
Rel: rel.Type,
|
Rel: rel.Type,
|
||||||
}, obj)
|
}, obj, s.schemas)
|
||||||
}
|
}
|
||||||
|
|
||||||
toNS := ""
|
toNS := ""
|
||||||
@@ -197,10 +198,10 @@ func (s *SummaryCache) toRel(ns string, rel *summary.Relationship) Relationship
|
|||||||
Rel: rel.Type,
|
Rel: rel.Type,
|
||||||
ToNamespace: toNS,
|
ToNamespace: toNS,
|
||||||
Selector: toSelector(rel.Selector),
|
Selector: toSelector(rel.Selector),
|
||||||
}, obj)
|
}, obj, s.schemas)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addObject(rel Relationship, obj interface{}) Relationship {
|
func addObject(rel Relationship, obj interface{}, schemas *schema.Collection) Relationship {
|
||||||
if obj == nil {
|
if obj == nil {
|
||||||
return rel
|
return rel
|
||||||
}
|
}
|
||||||
@@ -210,7 +211,8 @@ func addObject(rel Relationship, obj interface{}) Relationship {
|
|||||||
return rel
|
return rel
|
||||||
}
|
}
|
||||||
|
|
||||||
summarized := summary.Summarized(ro)
|
summarized := summary.SummarizedWithOptions(ro, getSummarizeOptions(ro, schemas))
|
||||||
|
|
||||||
rel.State = summarized.State
|
rel.State = summarized.State
|
||||||
rel.Error = summarized.Error
|
rel.Error = summarized.Error
|
||||||
rel.Message = strings.Join(summarized.Message, "; ")
|
rel.Message = strings.Join(summarized.Message, "; ")
|
||||||
@@ -267,7 +269,7 @@ func (s *SummaryCache) Change(newObj, oldObj runtime.Object) {
|
|||||||
func (s *SummaryCache) process(obj runtime.Object) (*summary.SummarizedObject, []*summary.Relationship) {
|
func (s *SummaryCache) process(obj runtime.Object) (*summary.SummarizedObject, []*summary.Relationship) {
|
||||||
var (
|
var (
|
||||||
rels []*summary.Relationship
|
rels []*summary.Relationship
|
||||||
summary = summary.Summarized(obj)
|
summary = summary.SummarizedWithOptions(obj, getSummarizeOptions(obj, s.schemas))
|
||||||
)
|
)
|
||||||
|
|
||||||
for _, rel := range summary.Relationships {
|
for _, rel := range summary.Relationships {
|
||||||
@@ -339,6 +341,19 @@ func (s *SummaryCache) OnChange(_ runtimeschema.GroupVersionKind, key string, ob
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getSummarizeOptions(obj runtime.Object, schemas *schema.Collection) *summary.SummarizeOptions {
|
||||||
|
gvk := obj.GetObjectKind().GroupVersionKind()
|
||||||
|
schemaID := converter.GVKToSchemaID(gvk)
|
||||||
|
schema := schemas.Schema(schemaID)
|
||||||
|
|
||||||
|
opts := &summary.SummarizeOptions{HasObservedGeneration: false}
|
||||||
|
if schema != nil && schema.Attributes != nil {
|
||||||
|
opts.HasObservedGeneration = wranglerSchemas.HasObservedGeneration(schema.Schema)
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
func toKeyFrom(namespace, name string, gvk runtimeschema.GroupVersionKind, other ...string) string {
|
func toKeyFrom(namespace, name string, gvk runtimeschema.GroupVersionKind, other ...string) string {
|
||||||
parts := []string{
|
parts := []string{
|
||||||
gvk.Group,
|
gvk.Group,
|
||||||
|
Reference in New Issue
Block a user