diff --git a/staging/src/k8s.io/client-go/tools/cache/object-names.go b/staging/src/k8s.io/client-go/tools/cache/object-names.go new file mode 100644 index 00000000000..aa8dbb19937 --- /dev/null +++ b/staging/src/k8s.io/client-go/tools/cache/object-names.go @@ -0,0 +1,65 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cache + +import ( + "k8s.io/apimachinery/pkg/types" +) + +// ObjectName is a reference to an object of some implicit kind +type ObjectName struct { + Namespace string + Name string +} + +// NewObjectName constructs a new one +func NewObjectName(namespace, name string) ObjectName { + return ObjectName{Namespace: namespace, Name: name} +} + +// Parts is the inverse of the constructor +func (objName ObjectName) Parts() (namespace, name string) { + return objName.Namespace, objName.Name +} + +// String returns the standard string encoding, +// which is designed to match the historical behavior of MetaNamespaceKeyFunc. +// Note this behavior is different from the String method of types.NamespacedName. +func (objName ObjectName) String() string { + if len(objName.Namespace) > 0 { + return objName.Namespace + "/" + objName.Name + } + return objName.Name +} + +// ParseObjectName tries to parse the standard encoding +func ParseObjectName(str string) (ObjectName, error) { + var objName ObjectName + var err error + objName.Namespace, objName.Name, err = SplitMetaNamespaceKey(str) + return objName, err +} + +// NamespacedNameAsObjectName rebrands the given NamespacedName as an ObjectName +func NamespacedNameAsObjectName(nn types.NamespacedName) ObjectName { + return NewObjectName(nn.Namespace, nn.Name) +} + +// AsNamespacedName rebrands as a NamespacedName +func (objName ObjectName) AsNamespacedName() types.NamespacedName { + return types.NamespacedName{Namespace: objName.Namespace, Name: objName.Name} +} diff --git a/staging/src/k8s.io/client-go/tools/cache/object-names_test.go b/staging/src/k8s.io/client-go/tools/cache/object-names_test.go new file mode 100644 index 00000000000..532b346b3db --- /dev/null +++ b/staging/src/k8s.io/client-go/tools/cache/object-names_test.go @@ -0,0 +1,59 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cache + +import ( + "math/rand" + "strings" + "testing" +) + +func TestObjectNames(t *testing.T) { + chars := "abcdefghi/" + for count := 1; count <= 100; count++ { + var encodedB strings.Builder + for index := 0; index < 10; index++ { + encodedB.WriteByte(chars[rand.Intn(len(chars))]) + } + encodedS := encodedB.String() + parts := strings.Split(encodedS, "/") + on, err := ParseObjectName(encodedS) + expectError := len(parts) > 2 + if expectError != (err != nil) { + t.Errorf("Wrong error; expected=%v, got=%v", expectError, err) + } + if expectError || err != nil { + continue + } + var expectedObjectName ObjectName + if len(parts) == 2 { + expectedObjectName = ObjectName{Namespace: parts[0], Name: parts[1]} + } else { + expectedObjectName = ObjectName{Name: encodedS} + } + if on != expectedObjectName { + t.Errorf("Parse failed, expected=%+v, got=%+v", expectedObjectName, on) + } + recoded := on.String() + if encodedS[0] == '/' { + recoded = "/" + recoded + } + if encodedS != recoded { + t.Errorf("Parse().String() was not identity, original=%q, final=%q", encodedS, recoded) + } + } +} diff --git a/staging/src/k8s.io/client-go/tools/cache/store.go b/staging/src/k8s.io/client-go/tools/cache/store.go index 5308ea74800..5cc3f42ec17 100644 --- a/staging/src/k8s.io/client-go/tools/cache/store.go +++ b/staging/src/k8s.io/client-go/tools/cache/store.go @@ -21,6 +21,7 @@ import ( "strings" "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // Store is a generic object storage and processing interface. A @@ -99,20 +100,38 @@ type ExplicitKey string // The key uses the format / unless is empty, then // it's just . // -// TODO: replace key-as-string with a key-as-struct so that this -// packing/unpacking won't be necessary. +// Clients that want a structured alternative can use ObjectToName or MetaObjectToName. +// Note: this would not be a client that wants a key for a Store because those are +// necessarily strings. +// +// TODO maybe some day?: change Store to be keyed differently func MetaNamespaceKeyFunc(obj interface{}) (string, error) { if key, ok := obj.(ExplicitKey); ok { return string(key), nil } + objName, err := ObjectToName(obj) + if err != nil { + return "", err + } + return objName.String(), nil +} + +// ObjectToName returns the structured name for the given object, +// if indeed it can be viewed as a metav1.Object. +func ObjectToName(obj interface{}) (ObjectName, error) { meta, err := meta.Accessor(obj) if err != nil { - return "", fmt.Errorf("object has no meta: %v", err) + return ObjectName{}, fmt.Errorf("object has no meta: %v", err) } - if len(meta.GetNamespace()) > 0 { - return meta.GetNamespace() + "/" + meta.GetName(), nil + return MetaObjectToName(meta), nil +} + +// MetaObjectToName returns the structured name for the given object +func MetaObjectToName(obj metav1.Object) ObjectName { + if len(obj.GetNamespace()) > 0 { + return ObjectName{Namespace: obj.GetNamespace(), Name: obj.GetName()} } - return meta.GetName(), nil + return ObjectName{Namespace: "", Name: obj.GetName()} } // SplitMetaNamespaceKey returns the namespace and name that