-
Notifications
You must be signed in to change notification settings - Fork 352
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #450 from pohly/storage-capacity
storage capacity producer
Showing
50 changed files
with
7,661 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
# This YAML file demonstrates how to enable the | ||
# storage capacity feature when deploying the | ||
# external provisioner, in this example together | ||
# with the mock CSI driver. | ||
# | ||
# It depends on the RBAC definitions from rbac.yaml. | ||
--- | ||
kind: Deployment | ||
apiVersion: apps/v1 | ||
metadata: | ||
name: csi-provisioner | ||
spec: | ||
replicas: 3 | ||
selector: | ||
matchLabels: | ||
app: csi-provisioner | ||
template: | ||
metadata: | ||
labels: | ||
app: csi-provisioner | ||
spec: | ||
serviceAccount: csi-provisioner | ||
containers: | ||
- name: csi-provisioner | ||
image: k8s.gcr.io/sig-storage/csi-provisioner:v2.0.0 | ||
args: | ||
- "--csi-address=$(ADDRESS)" | ||
- "--leader-election" | ||
- "--enable-capacity=central" | ||
- "--capacity-ownerref-level=2" | ||
env: | ||
- name: ADDRESS | ||
value: /var/lib/csi/sockets/pluginproxy/mock.socket | ||
- name: POD_NAMESPACE | ||
valueFrom: | ||
fieldRef: | ||
fieldPath: metadata.namespace | ||
- name: POD_NAME | ||
valueFrom: | ||
fieldRef: | ||
fieldPath: metadata.name | ||
imagePullPolicy: "IfNotPresent" | ||
volumeMounts: | ||
- name: socket-dir | ||
mountPath: /var/lib/csi/sockets/pluginproxy/ | ||
|
||
- name: mock-driver | ||
image: quay.io/k8scsi/mock-driver:canary | ||
env: | ||
- name: CSI_ENDPOINT | ||
value: /var/lib/csi/sockets/pluginproxy/mock.socket | ||
volumeMounts: | ||
- name: socket-dir | ||
mountPath: /var/lib/csi/sockets/pluginproxy/ | ||
volumes: | ||
- name: socket-dir | ||
emptyDir: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
/* | ||
Copyright 2020 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 capacity contains the code which controls the CSIStorageCapacity | ||
// objects owned by the external-provisioner. | ||
package capacity |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
/* | ||
Copyright 2020 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 capacity | ||
|
||
import ( | ||
"errors" | ||
"strings" | ||
|
||
flag "github.com/spf13/pflag" | ||
) | ||
|
||
// DeploymentMode determines how the capacity controller operates. | ||
type DeploymentMode string | ||
|
||
const ( | ||
// DeploymentModeCentral enables the mode where there is only one | ||
// external-provisioner actively running in the cluster which | ||
// talks to the CSI driver's controller service. | ||
DeploymentModeCentral = DeploymentMode("central") | ||
|
||
// DeploymentModeLocal enables the mode where external-provisioner | ||
// is deployed on each node. Not implemented yet. | ||
DeploymentModeLocal = DeploymentMode("local") | ||
|
||
// DeploymentModeNone disables capacity support. | ||
DeploymentModeNone = DeploymentMode("none") | ||
) | ||
|
||
// Set enables the named features. Multiple features can be listed, separated by commas, | ||
// with optional whitespace. | ||
func (mode *DeploymentMode) Set(value string) error { | ||
switch DeploymentMode(value) { | ||
case DeploymentModeCentral, DeploymentModeNone: | ||
*mode = DeploymentMode(value) | ||
default: | ||
return errors.New("invalid value") | ||
} | ||
return nil | ||
} | ||
|
||
func (mode *DeploymentMode) String() string { | ||
return string(*mode) | ||
} | ||
|
||
func (mode *DeploymentMode) Type() string { | ||
return strings.Join([]string{string(DeploymentModeCentral), string(DeploymentModeNone)}, "|") | ||
} | ||
|
||
var _ flag.Value = new(DeploymentMode) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
/* | ||
Copyright 2020 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 topology contains an abstract interface for discovering | ||
// topology segments for a storage backend and a specific implementation | ||
// which does that based on the CSINodeDriver.TopologyKeys and the | ||
// corresponding labels for the nodes. | ||
package topology |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,336 @@ | ||
/* | ||
Copyright 2020 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 topology | ||
|
||
import ( | ||
"context" | ||
"reflect" | ||
"sort" | ||
"sync" | ||
|
||
v1 "k8s.io/api/core/v1" | ||
storagev1 "k8s.io/api/storage/v1" | ||
apierrs "k8s.io/apimachinery/pkg/api/errors" | ||
"k8s.io/apimachinery/pkg/labels" | ||
utilruntime "k8s.io/apimachinery/pkg/util/runtime" | ||
coreinformersv1 "k8s.io/client-go/informers/core/v1" | ||
storageinformersv1 "k8s.io/client-go/informers/storage/v1" | ||
"k8s.io/client-go/kubernetes" | ||
"k8s.io/client-go/tools/cache" | ||
"k8s.io/client-go/util/workqueue" | ||
"k8s.io/klog" | ||
) | ||
|
||
// NewNodeTopology returns an informer that synthesizes storage | ||
// topology segments based on the accessible topology that each CSI | ||
// driver node instance reports. See | ||
// https://github.com/kubernetes/enhancements/tree/master/keps/sig-storage/1472-storage-capacity-tracking#with-central-controller | ||
// for details. | ||
func NewNodeTopology( | ||
driverName string, | ||
client kubernetes.Interface, | ||
nodeInformer coreinformersv1.NodeInformer, | ||
csiNodeInformer storageinformersv1.CSINodeInformer, | ||
queue workqueue.RateLimitingInterface, | ||
) Informer { | ||
nt := &nodeTopology{ | ||
driverName: driverName, | ||
client: client, | ||
nodeInformer: nodeInformer, | ||
csiNodeInformer: csiNodeInformer, | ||
queue: queue, | ||
} | ||
|
||
// Whenever Node or CSINode objects change, we need to | ||
// recalculate the new topology segments. We could do that | ||
// immediately, but it is better to let the input data settle | ||
// a bit and just remember that there is work to be done. | ||
nodeHandler := cache.ResourceEventHandlerFuncs{ | ||
AddFunc: func(obj interface{}) { | ||
node, ok := obj.(*v1.Node) | ||
if !ok { | ||
klog.Errorf("added object: expected Node, got %T -> ignoring it", obj) | ||
return | ||
} | ||
klog.V(5).Infof("capacity topology: new node: %s", node.Name) | ||
queue.Add("") | ||
}, | ||
UpdateFunc: func(oldObj interface{}, newObj interface{}) { | ||
oldNode, ok := oldObj.(*v1.Node) | ||
if !ok { | ||
klog.Errorf("original object: expected Node, got %T -> ignoring it", oldObj) | ||
return | ||
} | ||
newNode, ok := newObj.(*v1.Node) | ||
if !ok { | ||
klog.Errorf("updated object: expected Node, got %T -> ignoring it", newObj) | ||
return | ||
} | ||
if reflect.DeepEqual(oldNode.Labels, newNode.Labels) { | ||
// Shortcut: labels haven't changed, no need to sync. | ||
return | ||
} | ||
klog.V(5).Infof("capacity topology: updated node: %s", newNode.Name) | ||
queue.Add("") | ||
}, | ||
DeleteFunc: func(obj interface{}) { | ||
// Beware of "xxx deleted" events | ||
if unknown, ok := obj.(cache.DeletedFinalStateUnknown); ok && unknown.Obj != nil { | ||
obj = unknown.Obj | ||
} | ||
node, ok := obj.(*v1.Node) | ||
if !ok { | ||
klog.Errorf("deleted object: expected Node, got %T -> ignoring it", obj) | ||
return | ||
} | ||
klog.V(5).Infof("capacity topology: removed node: %s", node.Name) | ||
queue.Add("") | ||
}, | ||
} | ||
nodeInformer.Informer().AddEventHandler(nodeHandler) | ||
csiNodeHandler := cache.ResourceEventHandlerFuncs{ | ||
AddFunc: func(obj interface{}) { | ||
csiNode, ok := obj.(*storagev1.CSINode) | ||
if !ok { | ||
klog.Errorf("added object: expected CSINode, got %T -> ignoring it", obj) | ||
return | ||
} | ||
klog.V(5).Infof("capacity topology: new CSINode: %s", csiNode.Name) | ||
queue.Add("") | ||
}, | ||
UpdateFunc: func(oldObj interface{}, newObj interface{}) { | ||
oldCSINode, ok := oldObj.(*storagev1.CSINode) | ||
if !ok { | ||
klog.Errorf("original object: expected CSINode, got %T -> ignoring it", oldObj) | ||
return | ||
} | ||
newCSINode, ok := newObj.(*storagev1.CSINode) | ||
if !ok { | ||
klog.Errorf("updated object: expected CSINode, got %T -> ignoring it", newObj) | ||
return | ||
} | ||
oldKeys := nt.driverTopologyKeys(oldCSINode) | ||
newKeys := nt.driverTopologyKeys(newCSINode) | ||
if reflect.DeepEqual(oldKeys, newKeys) { | ||
// Shortcut: keys haven't changed, no need to sync. | ||
return | ||
} | ||
klog.V(5).Infof("capacity topology: updated CSINode: %s", newCSINode.Name) | ||
queue.Add("") | ||
}, | ||
DeleteFunc: func(obj interface{}) { | ||
// Beware of "xxx deleted" events | ||
if unknown, ok := obj.(cache.DeletedFinalStateUnknown); ok && unknown.Obj != nil { | ||
obj = unknown.Obj | ||
} | ||
csiNode, ok := obj.(*storagev1.CSINode) | ||
if !ok { | ||
klog.Errorf("deleted object: expected CSINode, got %T -> ignoring it", obj) | ||
return | ||
} | ||
klog.V(5).Infof("capacity topology: removed CSINode: %s", csiNode.Name) | ||
queue.Add("") | ||
}, | ||
} | ||
csiNodeInformer.Informer().AddEventHandler(csiNodeHandler) | ||
|
||
return nt | ||
} | ||
|
||
var _ Informer = &nodeTopology{} | ||
|
||
type nodeTopology struct { | ||
driverName string | ||
client kubernetes.Interface | ||
nodeInformer coreinformersv1.NodeInformer | ||
csiNodeInformer storageinformersv1.CSINodeInformer | ||
queue workqueue.RateLimitingInterface | ||
|
||
mutex sync.Mutex | ||
// segments hold a list of all currently known topology segments. | ||
segments []*Segment | ||
// callbacks contains all callbacks that need to be invoked | ||
// after making changes to the list of known segments. | ||
callbacks []Callback | ||
} | ||
|
||
// driverTopologyKeys returns nil if the driver is not running | ||
// on the node, otherwise at least an empty slice of topology keys. | ||
func (nt *nodeTopology) driverTopologyKeys(csiNode *storagev1.CSINode) []string { | ||
for _, csiNodeDriver := range csiNode.Spec.Drivers { | ||
if csiNodeDriver.Name == nt.driverName { | ||
if csiNodeDriver.TopologyKeys == nil { | ||
return []string{} | ||
} | ||
return csiNodeDriver.TopologyKeys | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func (nt *nodeTopology) AddCallback(cb Callback) { | ||
nt.mutex.Lock() | ||
defer nt.mutex.Unlock() | ||
|
||
nt.callbacks = append(nt.callbacks, cb) | ||
} | ||
|
||
func (nt *nodeTopology) List() []*Segment { | ||
nt.mutex.Lock() | ||
defer nt.mutex.Unlock() | ||
|
||
// We need to return a new slice to protect against future | ||
// changes in nt.segments. The segments themselves are | ||
// immutable and shared. | ||
segments := make([]*Segment, len(nt.segments)) | ||
copy(segments, nt.segments) | ||
return segments | ||
} | ||
|
||
func (nt *nodeTopology) Run(ctx context.Context) { | ||
go nt.nodeInformer.Informer().Run(ctx.Done()) | ||
go nt.csiNodeInformer.Informer().Run(ctx.Done()) | ||
go nt.runWorker(ctx) | ||
|
||
klog.Info("Started node topology informer") | ||
<-ctx.Done() | ||
klog.Info("Shutting node topology informer") | ||
} | ||
|
||
func (nt *nodeTopology) HasSynced() bool { | ||
if nt.nodeInformer.Informer().HasSynced() && | ||
nt.csiNodeInformer.Informer().HasSynced() { | ||
// Now that both informers are up-to-date, use that | ||
// information to update our own view of the world. | ||
nt.sync(context.Background()) | ||
return true | ||
} | ||
return false | ||
} | ||
|
||
func (nt *nodeTopology) runWorker(ctx context.Context) { | ||
for nt.processNextWorkItem(ctx) { | ||
} | ||
} | ||
|
||
func (nt *nodeTopology) processNextWorkItem(ctx context.Context) bool { | ||
obj, shutdown := nt.queue.Get() | ||
if shutdown { | ||
return false | ||
} | ||
defer nt.queue.Done(obj) | ||
nt.sync(ctx) | ||
return true | ||
} | ||
|
||
func (nt *nodeTopology) sync(ctx context.Context) { | ||
// For all nodes on which the driver is registered, collect the topology key/value pairs | ||
// and sort them by key name to make the result deterministic. Skip all segments that have | ||
// been seen before. | ||
segments := nt.List() | ||
removalCandidates := map[*Segment]bool{} | ||
var addedSegments, removedSegments []*Segment | ||
for _, segment := range segments { | ||
// Assume that the segment is removed. Will be set to | ||
// false if we find out otherwise. | ||
removalCandidates[segment] = true | ||
} | ||
|
||
csiNodes, err := nt.csiNodeInformer.Lister().List(labels.Everything()) | ||
if err != nil { | ||
utilruntime.HandleError(err) | ||
return | ||
} | ||
existingSegments := make([]*Segment, 0, len(segments)) | ||
node: | ||
for _, csiNode := range csiNodes { | ||
topologyKeys := nt.driverTopologyKeys(csiNode) | ||
if topologyKeys == nil { | ||
// Driver not running on node, ignore it. | ||
continue | ||
} | ||
node, err := nt.nodeInformer.Lister().Get(csiNode.Name) | ||
if err != nil { | ||
if apierrs.IsNotFound(err) { | ||
// Obsolete CSINode object? Ignore it. | ||
continue | ||
} | ||
// This shouldn't happen. If it does, | ||
// something is very wrong and we give up. | ||
utilruntime.HandleError(err) | ||
return | ||
} | ||
|
||
newSegment := Segment{} | ||
sort.Strings(topologyKeys) | ||
for _, key := range topologyKeys { | ||
value, ok := node.Labels[key] | ||
if !ok { | ||
// The driver announced some topology key and kubelet recorded | ||
// it in CSINode, but we haven't seen the corresponding | ||
// node update yet as the label is not set. Ignore the node | ||
// for now, we'll sync up when we get the node update. | ||
continue node | ||
} | ||
newSegment = append(newSegment, SegmentEntry{key, value}) | ||
} | ||
|
||
// Add it only if new, otherwise look at the next node. | ||
for _, segment := range segments { | ||
if newSegment.Compare(*segment) == 0 { | ||
// Reuse a segment instead of using the new one. This keeps pointers stable. | ||
removalCandidates[segment] = false | ||
existingSegments = append(existingSegments, segment) | ||
continue node | ||
} | ||
} | ||
for _, segment := range addedSegments { | ||
if newSegment.Compare(*segment) == 0 { | ||
// We already discovered this new segment. | ||
continue node | ||
} | ||
} | ||
|
||
// A completely new segment. | ||
addedSegments = append(addedSegments, &newSegment) | ||
existingSegments = append(existingSegments, &newSegment) | ||
} | ||
|
||
// Lock while making changes, but unlock before actually invoking callbacks. | ||
nt.mutex.Lock() | ||
nt.segments = existingSegments | ||
|
||
// Theoretically callbacks could change while we don't have | ||
// the lock, so make a copy. | ||
callbacks := make([]Callback, len(nt.callbacks)) | ||
copy(callbacks, nt.callbacks) | ||
nt.mutex.Unlock() | ||
|
||
for segment, wasRemoved := range removalCandidates { | ||
if wasRemoved { | ||
removedSegments = append(removedSegments, segment) | ||
} | ||
} | ||
if len(addedSegments) > 0 || len(removedSegments) > 0 { | ||
klog.V(5).Infof("topology changed: added %v, removed %v", addedSegments, removedSegments) | ||
for _, cb := range callbacks { | ||
cb(addedSegments, removedSegments) | ||
} | ||
} else { | ||
klog.V(5).Infof("topology unchanged") | ||
} | ||
} |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
/* | ||
Copyright 2020 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 topology | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"sort" | ||
"strings" | ||
|
||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
) | ||
|
||
// Segment represents a topology segment. Entries are always sorted by | ||
// key and keys are unique. In contrast to a map, segments therefore | ||
// can be compared efficiently. A nil segment matches no nodes | ||
// in a cluster, an empty segment all of them. | ||
type Segment []SegmentEntry | ||
|
||
var _ sort.Interface = Segment{} | ||
|
||
// String returns the address *and* the content of the segment; the address | ||
// is how the segment is identified when used as a hash key. | ||
func (s *Segment) String() string { | ||
return fmt.Sprintf("%p = %s", s, s.SimpleString()) | ||
} | ||
|
||
// SimpleString only returns the content. | ||
func (s *Segment) SimpleString() string { | ||
var parts []string | ||
for _, entry := range *s { | ||
parts = append(parts, entry.String()) | ||
} | ||
return strings.Join(parts, "+ ") | ||
} | ||
|
||
// Compare returns -1 if s is considered smaller than the other segment (less keys, | ||
// keys and/or values smaller), 0 if equal and 1 otherwise. | ||
func (s Segment) Compare(other Segment) int { | ||
if len(s) < len(other) { | ||
return -1 | ||
} | ||
if len(s) > len(other) { | ||
return 1 | ||
} | ||
for i := 0; i < len(s); i++ { | ||
cmp := s[i].Compare(other[i]) | ||
if cmp != 0 { | ||
return cmp | ||
} | ||
} | ||
return 0 | ||
} | ||
|
||
func (s Segment) Len() int { return len(s) } | ||
func (s Segment) Less(i, j int) bool { return s[i].Compare(s[j]) < 0 } | ||
func (s Segment) Swap(i, j int) { | ||
entry := s[i] | ||
s[i] = s[j] | ||
s[j] = entry | ||
} | ||
|
||
// SegmentEntry represents one topology key/value pair. | ||
type SegmentEntry struct { | ||
Key, Value string | ||
} | ||
|
||
func (se SegmentEntry) String() string { | ||
return se.Key + ": " + se.Value | ||
} | ||
|
||
// Compare returns -1 if se is considered smaller than the other segment entry (key or value smaller), | ||
// 0 if equal and 1 otherwise. | ||
func (se SegmentEntry) Compare(other SegmentEntry) int { | ||
cmp := strings.Compare(se.Key, other.Key) | ||
if cmp != 0 { | ||
return cmp | ||
} | ||
return strings.Compare(se.Value, other.Value) | ||
} | ||
|
||
// GetLabelSelector returns a LabelSelector with the key/value entries | ||
// as label match criteria. | ||
func (s Segment) GetLabelSelector() *metav1.LabelSelector { | ||
return &metav1.LabelSelector{ | ||
MatchLabels: s.GetLabelMap(), | ||
} | ||
} | ||
|
||
// GetLabelMap returns nil if the Segment itself is nil, | ||
// otherwise a map with all key/value pairs. | ||
func (s Segment) GetLabelMap() map[string]string { | ||
if s == nil { | ||
return nil | ||
} | ||
labels := map[string]string{} | ||
for _, entry := range s { | ||
labels[entry.Key] = entry.Value | ||
} | ||
return labels | ||
} | ||
|
||
// Informer keeps a list of discovered topology segments and can | ||
// notify one or more clients when it discovers changes. Segments | ||
// are identified by their address and guaranteed to be unique. | ||
type Informer interface { | ||
// AddCallback ensures that the function is called each time | ||
// changes to the list of segments are detected. It also gets | ||
// called immediately when adding the callback and there are | ||
// already some known segments. | ||
AddCallback(cb Callback) | ||
|
||
// List returns all known segments, in no particular order. | ||
List() []*Segment | ||
|
||
// Run starts watching for changes. | ||
Run(ctx context.Context) | ||
|
||
// HasSynced returns true once all segments have been found. | ||
HasSynced() bool | ||
} | ||
|
||
type Callback func(added []*Segment, removed []*Segment) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
/* | ||
Copyright 2020 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 owner contains code for walking up the ownership chain, | ||
// starting with an arbitrary object. RBAC rules must allow GET access | ||
// to each object on the chain, at least including the starting | ||
// object, more when walking up more than one level. | ||
package owner | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | ||
"k8s.io/apimachinery/pkg/runtime/schema" | ||
"k8s.io/client-go/rest" | ||
"sigs.k8s.io/controller-runtime/pkg/client" | ||
) | ||
|
||
// Lookup walks up the ownership chain zero or more levels and returns an OwnerReference for the | ||
// object. The object identified by name, namespace and type is the starting point and is | ||
// returned when levels is zero. Only APIVersion, Kind, Name, and UID will be set. | ||
// IsController is always true. | ||
func Lookup(config *rest.Config, namespace, name string, gkv schema.GroupVersionKind, levels int) (*metav1.OwnerReference, error) { | ||
c, err := client.New(config, client.Options{}) | ||
if err != nil { | ||
return nil, fmt.Errorf("build client: %v", err) | ||
} | ||
|
||
return lookupRecursive(c, namespace, name, gkv.Group, gkv.Version, gkv.Kind, levels) | ||
} | ||
|
||
func lookupRecursive(c client.Client, namespace, name, group, version, kind string, levels int) (*metav1.OwnerReference, error) { | ||
u := &unstructured.Unstructured{} | ||
apiVersion := metav1.GroupVersion{Group: group, Version: version}.String() | ||
u.SetAPIVersion(apiVersion) | ||
u.SetKind(kind) | ||
|
||
if err := c.Get(context.Background(), client.ObjectKey{ | ||
Namespace: namespace, | ||
Name: name, | ||
}, u); err != nil { | ||
return nil, fmt.Errorf("get object: %v", err) | ||
} | ||
|
||
if levels == 0 { | ||
isTrue := true | ||
return &metav1.OwnerReference{ | ||
APIVersion: apiVersion, | ||
Kind: kind, | ||
Name: name, | ||
UID: u.GetUID(), | ||
Controller: &isTrue, | ||
}, nil | ||
} | ||
owners := u.GetOwnerReferences() | ||
for _, owner := range owners { | ||
if owner.Controller != nil && *owner.Controller { | ||
gv, err := schema.ParseGroupVersion(owner.APIVersion) | ||
if err != nil { | ||
return nil, fmt.Errorf("parse OwnerReference.APIVersion: %v", err) | ||
} | ||
// With this special case here we avoid one lookup and thus the need for | ||
// RBAC GET permission for the parent. For example, when a Pod is controlled | ||
// by a StatefulSet, we only need GET permission for Pods (for the c.Get above) | ||
// but not for StatefulSets. | ||
if levels == 1 { | ||
isTrue := true | ||
return &metav1.OwnerReference{ | ||
APIVersion: owner.APIVersion, | ||
Kind: owner.Kind, | ||
Name: owner.Name, | ||
UID: owner.UID, | ||
Controller: &isTrue, | ||
}, nil | ||
} | ||
|
||
return lookupRecursive(c, namespace, owner.Name, | ||
gv.Group, gv.Version, owner.Kind, | ||
levels-1) | ||
} | ||
} | ||
return nil, fmt.Errorf("%s/%s %q in namespace %q has no controlling owner, cannot unwind the ownership further", | ||
apiVersion, kind, name, namespace) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
/* | ||
Copyright 2020 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 owner | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
|
||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | ||
"k8s.io/apimachinery/pkg/runtime" | ||
"k8s.io/apimachinery/pkg/runtime/schema" | ||
"k8s.io/apimachinery/pkg/types" | ||
"sigs.k8s.io/controller-runtime/pkg/client/fake" | ||
) | ||
|
||
var ( | ||
testNamespace = "test-namespace" | ||
otherNamespace = "other-namespace" | ||
statefulsetGkv = schema.GroupVersionKind{ | ||
Group: "apps", | ||
Version: "v1", | ||
Kind: "StatefulSet", | ||
} | ||
deploymentGkv = schema.GroupVersionKind{ | ||
Group: "apps", | ||
Version: "v1", | ||
Kind: "Deployment", | ||
} | ||
replicasetGkv = schema.GroupVersionKind{ | ||
Group: "apps", | ||
Version: "v1", | ||
Kind: "ReplicaSet", | ||
} | ||
podGkv = schema.GroupVersionKind{ | ||
Group: "", | ||
Version: "v1", | ||
Kind: "Pod", | ||
} | ||
|
||
pod = makeObject(testNamespace, "foo", podGkv, nil) | ||
statefulset = makeObject(testNamespace, "foo", statefulsetGkv, nil) | ||
statefulsetPod = makeObject(testNamespace, "foo", podGkv, &statefulset) | ||
deployment = makeObject(testNamespace, "foo", deploymentGkv, nil) | ||
replicaset = makeObject(testNamespace, "foo", replicasetGkv, &deployment) | ||
otherReplicaset = makeObject(testNamespace, "bar", replicasetGkv, &deployment) | ||
yetAnotherReplicaset = makeObject(otherNamespace, "foo", replicasetGkv, &deployment) | ||
deploymentsetPod = makeObject(testNamespace, "foo", podGkv, &replicaset) | ||
) | ||
|
||
// TestNodeTopology checks that node labels are correctly transformed | ||
// into topology segments. | ||
func TestNodeTopology(t *testing.T) { | ||
testcases := map[string]struct { | ||
objects []runtime.Object | ||
start unstructured.Unstructured | ||
levels int | ||
expectError bool | ||
expectOwner unstructured.Unstructured | ||
}{ | ||
"empty": { | ||
start: pod, | ||
expectError: true, | ||
}, | ||
"pod-itself": { | ||
objects: []runtime.Object{&pod}, | ||
start: pod, | ||
levels: 0, | ||
expectOwner: pod, | ||
}, | ||
"no-parent": { | ||
objects: []runtime.Object{&pod}, | ||
start: pod, | ||
levels: 1, | ||
expectError: true, | ||
}, | ||
"parent": { | ||
objects: []runtime.Object{&statefulsetPod}, | ||
start: statefulsetPod, | ||
levels: 1, | ||
// The object doesn't have to exist. | ||
expectOwner: statefulset, | ||
}, | ||
"missing-parent": { | ||
objects: []runtime.Object{&deploymentsetPod}, | ||
start: deploymentsetPod, | ||
levels: 2, | ||
expectError: true, | ||
}, | ||
"wrong-parent": { | ||
objects: []runtime.Object{&deploymentsetPod, &otherReplicaset}, | ||
start: deploymentsetPod, | ||
levels: 2, | ||
expectError: true, | ||
}, | ||
"another-wrong-parent": { | ||
objects: []runtime.Object{&deploymentsetPod, &yetAnotherReplicaset}, | ||
start: deploymentsetPod, | ||
levels: 2, | ||
expectError: true, | ||
}, | ||
"grandparent": { | ||
objects: []runtime.Object{&deploymentsetPod, &replicaset}, | ||
start: deploymentsetPod, | ||
levels: 2, | ||
// The object doesn't have to exist. | ||
expectOwner: deployment, | ||
}, | ||
} | ||
|
||
for name, tc := range testcases { | ||
tc := tc | ||
t.Run(name, func(t *testing.T) { | ||
c := fake.NewFakeClient(tc.objects...) | ||
gkv := tc.start.GroupVersionKind() | ||
ownerRef, err := lookupRecursive(c, | ||
tc.start.GetNamespace(), | ||
tc.start.GetName(), | ||
gkv.Group, | ||
gkv.Version, | ||
gkv.Kind, | ||
tc.levels) | ||
if err != nil && !tc.expectError { | ||
t.Fatalf("unexpected error: %v", err) | ||
} | ||
if err == nil && tc.expectError { | ||
t.Fatal("unexpected success") | ||
} | ||
if err == nil { | ||
if ownerRef == nil { | ||
t.Fatal("unexpected nil owner") | ||
} | ||
gkv := tc.expectOwner.GroupVersionKind() | ||
apiVersion := metav1.GroupVersion{Group: gkv.Group, Version: gkv.Version}.String() | ||
if ownerRef.APIVersion != apiVersion { | ||
t.Errorf("expected APIVersion %q, got %q", apiVersion, ownerRef.APIVersion) | ||
} | ||
if ownerRef.Kind != gkv.Kind { | ||
t.Errorf("expected Kind %q, got %q", gkv.Kind, ownerRef.Kind) | ||
} | ||
if ownerRef.Name != tc.expectOwner.GetName() { | ||
t.Errorf("expected Name %q, got %q", tc.expectOwner.GetName(), ownerRef.Name) | ||
} | ||
if ownerRef.UID != tc.expectOwner.GetUID() { | ||
t.Errorf("expected UID %q, got %q", tc.expectOwner.GetUID(), ownerRef.UID) | ||
} | ||
if ownerRef.Controller == nil || !*ownerRef.Controller { | ||
t.Error("Controller field should true") | ||
} | ||
if ownerRef.BlockOwnerDeletion != nil && *ownerRef.BlockOwnerDeletion { | ||
t.Error("BlockOwnerDeletion field should false") | ||
} | ||
} | ||
}) | ||
} | ||
} | ||
|
||
var uidCounter int | ||
|
||
func makeObject(namespace, name string, gkv schema.GroupVersionKind, owner *unstructured.Unstructured) unstructured.Unstructured { | ||
u := unstructured.Unstructured{} | ||
u.SetNamespace(namespace) | ||
u.SetName(name) | ||
u.SetGroupVersionKind(gkv) | ||
uidCounter++ | ||
u.SetUID(types.UID(fmt.Sprintf("FAKE-UID-%d", uidCounter))) | ||
if owner != nil { | ||
isTrue := true | ||
u.SetOwnerReferences([]metav1.OwnerReference{ | ||
{ | ||
APIVersion: owner.GetAPIVersion(), | ||
Kind: owner.GetKind(), | ||
Name: owner.GetName(), | ||
UID: owner.GetUID(), | ||
Controller: &isTrue, | ||
}, | ||
}) | ||
} | ||
return u | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
7 changes: 5 additions & 2 deletions
7
vendor/github.com/hashicorp/golang-lru/simplelru/lru_interface.go
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
119 changes: 119 additions & 0 deletions
119
vendor/k8s.io/client-go/restmapper/category_expansion.go
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
97 changes: 97 additions & 0 deletions
97
vendor/sigs.k8s.io/controller-runtime/pkg/client/apiutil/apimachinery.go
Oops, something went wrong.
Oops, something went wrong.