Add glide.yaml and vendor deps

This commit is contained in:
Dalton Hubble 2016-12-03 22:43:32 -08:00
parent db918f12ad
commit 5b3d5e81bd
18880 changed files with 5166045 additions and 1 deletions

View file

@ -0,0 +1,78 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
"go_test",
"cgo_library",
)
go_library(
name = "go_default_library",
srcs = [
"backoff.go",
"cluster_util.go",
"configmap.go",
"delaying_deliverer.go",
"deployment.go",
"federated_informer.go",
"federated_updater.go",
"handlers.go",
"meta.go",
"secret.go",
"versionize_listoptions.go",
],
tags = ["automanaged"],
deps = [
"//federation/apis/federation/v1beta1:go_default_library",
"//federation/client/clientset_generated/federation_release_1_5:go_default_library",
"//pkg/api:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/apis/extensions/v1beta1:go_default_library",
"//pkg/client/cache:go_default_library",
"//pkg/client/clientset_generated/internalclientset:go_default_library",
"//pkg/client/clientset_generated/release_1_5:go_default_library",
"//pkg/client/restclient:go_default_library",
"//pkg/client/unversioned/clientcmd:go_default_library",
"//pkg/client/unversioned/clientcmd/api:go_default_library",
"//pkg/controller/deployment/util:go_default_library",
"//pkg/conversion:go_default_library",
"//pkg/runtime:go_default_library",
"//pkg/util/flowcontrol:go_default_library",
"//pkg/util/net:go_default_library",
"//pkg/util/wait:go_default_library",
"//pkg/watch:go_default_library",
"//vendor:github.com/golang/glog",
],
)
go_test(
name = "go_default_test",
srcs = [
"delaying_deliverer_test.go",
"deployment_test.go",
"federated_informer_test.go",
"federated_updater_test.go",
"handlers_test.go",
"meta_test.go",
],
library = "go_default_library",
tags = ["automanaged"],
deps = [
"//federation/apis/federation/v1beta1:go_default_library",
"//federation/client/clientset_generated/federation_release_1_5/fake:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/apis/extensions/v1beta1:go_default_library",
"//pkg/client/cache:go_default_library",
"//pkg/client/clientset_generated/release_1_5:go_default_library",
"//pkg/client/clientset_generated/release_1_5/fake:go_default_library",
"//pkg/client/testing/core:go_default_library",
"//pkg/controller/deployment/util:go_default_library",
"//pkg/runtime:go_default_library",
"//pkg/watch:go_default_library",
"//vendor:github.com/stretchr/testify/assert",
],
)

View file

@ -0,0 +1,36 @@
/*
Copyright 2016 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 util
import (
"time"
"k8s.io/kubernetes/pkg/util/flowcontrol"
)
func StartBackoffGC(backoff *flowcontrol.Backoff, stopCh <-chan struct{}) {
go func() {
for {
select {
case <-time.After(time.Minute):
backoff.GC()
case <-stopCh:
return
}
}
}()
}

View file

@ -0,0 +1,147 @@
/*
Copyright 2016 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 util
import (
"fmt"
"net"
"os"
"time"
"github.com/golang/glog"
federation_v1beta1 "k8s.io/kubernetes/federation/apis/federation/v1beta1"
fedclientset "k8s.io/kubernetes/federation/client/clientset_generated/federation_release_1_5"
"k8s.io/kubernetes/pkg/api"
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
"k8s.io/kubernetes/pkg/client/restclient"
"k8s.io/kubernetes/pkg/client/unversioned/clientcmd"
clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api"
utilnet "k8s.io/kubernetes/pkg/util/net"
"k8s.io/kubernetes/pkg/util/wait"
)
const (
KubeAPIQPS = 20.0
KubeAPIBurst = 30
KubeconfigSecretDataKey = "kubeconfig"
getSecretTimeout = 1 * time.Minute
)
func BuildClusterConfig(c *federation_v1beta1.Cluster) (*restclient.Config, error) {
var serverAddress string
var clusterConfig *restclient.Config
hostIP, err := utilnet.ChooseHostInterface()
if err != nil {
return nil, err
}
for _, item := range c.Spec.ServerAddressByClientCIDRs {
_, cidrnet, err := net.ParseCIDR(item.ClientCIDR)
if err != nil {
return nil, err
}
myaddr := net.ParseIP(hostIP.String())
if cidrnet.Contains(myaddr) == true {
serverAddress = item.ServerAddress
break
}
}
if serverAddress != "" {
if c.Spec.SecretRef == nil {
glog.Infof("didn't find secretRef for cluster %s. Trying insecure access", c.Name)
clusterConfig, err = clientcmd.BuildConfigFromFlags(serverAddress, "")
} else {
kubeconfigGetter := KubeconfigGetterForCluster(c)
clusterConfig, err = clientcmd.BuildConfigFromKubeconfigGetter(serverAddress, kubeconfigGetter)
}
if err != nil {
return nil, err
}
clusterConfig.QPS = KubeAPIQPS
clusterConfig.Burst = KubeAPIBurst
}
return clusterConfig, nil
}
// This is to inject a different kubeconfigGetter in tests.
// We don't use the standard one which calls NewInCluster in tests to avoid having to setup service accounts and mount files with secret tokens.
var KubeconfigGetterForCluster = func(c *federation_v1beta1.Cluster) clientcmd.KubeconfigGetter {
return func() (*clientcmdapi.Config, error) {
secretRefName := ""
if c.Spec.SecretRef != nil {
secretRefName = c.Spec.SecretRef.Name
} else {
glog.Infof("didn't find secretRef for cluster %s. Trying insecure access", c.Name)
}
return KubeconfigGetterForSecret(secretRefName)()
}
}
// KubeconfigGettterForSecret is used to get the kubeconfig from the given secret.
var KubeconfigGetterForSecret = func(secretName string) clientcmd.KubeconfigGetter {
return func() (*clientcmdapi.Config, error) {
var data []byte
if secretName != "" {
// Get the namespace this is running in from the env variable.
namespace := os.Getenv("POD_NAMESPACE")
if namespace == "" {
return nil, fmt.Errorf("unexpected: POD_NAMESPACE env var returned empty string")
}
// Get a client to talk to the k8s apiserver, to fetch secrets from it.
cc, err := restclient.InClusterConfig()
if err != nil {
return nil, fmt.Errorf("error in creating in-cluster client: %s", err)
}
client, err := clientset.NewForConfig(cc)
if err != nil {
return nil, fmt.Errorf("error in creating in-cluster client: %s", err)
}
data = []byte{}
var secret *api.Secret
err = wait.PollImmediate(1*time.Second, getSecretTimeout, func() (bool, error) {
secret, err = client.Core().Secrets(namespace).Get(secretName)
if err == nil {
return true, nil
}
glog.Warningf("error in fetching secret: %s", err)
return false, nil
})
if err != nil {
return nil, fmt.Errorf("timed out waiting for secret: %s", err)
}
if secret == nil {
return nil, fmt.Errorf("unexpected: received null secret %s", secretName)
}
ok := false
data, ok = secret.Data[KubeconfigSecretDataKey]
if !ok {
return nil, fmt.Errorf("secret does not have data with key: %s", KubeconfigSecretDataKey)
}
}
return clientcmd.Load(data)
}
}
// Retruns Clientset for the given cluster.
func GetClientsetForCluster(cluster *federation_v1beta1.Cluster) (*fedclientset.Clientset, error) {
clusterConfig, err := BuildClusterConfig(cluster)
if err != nil && clusterConfig != nil {
clientset := fedclientset.NewForConfigOrDie(restclient.AddUserAgent(clusterConfig, userAgentName))
return clientset, nil
}
return nil, err
}

View file

@ -0,0 +1,31 @@
/*
Copyright 2016 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 util
import (
"reflect"
api_v1 "k8s.io/kubernetes/pkg/api/v1"
)
// Checks if cluster-independent, user provided data in two given ConfigMapss are eqaul. If in
// the future the ConfigMap structure is expanded then any field that is not populated.
// by the api server should be included here.
func ConfigMapEquivalent(s1, s2 *api_v1.ConfigMap) bool {
return ObjectMetaEquivalent(s1.ObjectMeta, s2.ObjectMeta) &&
reflect.DeepEqual(s1.Data, s2.Data)
}

View file

@ -0,0 +1,183 @@
/*
Copyright 2016 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.
*/
// TODO: consider moving it to a more generic package.
package util
import (
"container/heap"
"time"
)
const (
// TODO: Investigate what capacity is right.
delayingDelivererUpdateChanCapacity = 1000
)
// DelayingDelivererItem is structure delivered by DelayingDeliverer to the
// target channel.
type DelayingDelivererItem struct {
// Key under which the value was added to deliverer.
Key string
// Value of the item.
Value interface{}
// When the item should be delivered.
DeliveryTime time.Time
}
type delivererHeap struct {
keyPosition map[string]int
data []*DelayingDelivererItem
}
// Functions required by container.Heap.
func (dh *delivererHeap) Len() int { return len(dh.data) }
func (dh *delivererHeap) Less(i, j int) bool {
return dh.data[i].DeliveryTime.Before(dh.data[j].DeliveryTime)
}
func (dh *delivererHeap) Swap(i, j int) {
dh.keyPosition[dh.data[i].Key] = j
dh.keyPosition[dh.data[j].Key] = i
dh.data[i], dh.data[j] = dh.data[j], dh.data[i]
}
func (dh *delivererHeap) Push(x interface{}) {
item := x.(*DelayingDelivererItem)
dh.data = append(dh.data, item)
dh.keyPosition[item.Key] = len(dh.data) - 1
}
func (dh *delivererHeap) Pop() interface{} {
n := len(dh.data)
item := dh.data[n-1]
dh.data = dh.data[:n-1]
delete(dh.keyPosition, item.Key)
return item
}
// A structure that pushes the items to the target channel at a given time.
type DelayingDeliverer struct {
// Channel to deliver the data when their time comes.
targetChannel chan *DelayingDelivererItem
// Store for data
heap *delivererHeap
// Channel to feed the main goroutine with updates.
updateChannel chan *DelayingDelivererItem
// To stop the main goroutine.
stopChannel chan struct{}
}
func NewDelayingDeliverer() *DelayingDeliverer {
return NewDelayingDelivererWithChannel(make(chan *DelayingDelivererItem, 100))
}
func NewDelayingDelivererWithChannel(targetChannel chan *DelayingDelivererItem) *DelayingDeliverer {
return &DelayingDeliverer{
targetChannel: targetChannel,
heap: &delivererHeap{
keyPosition: make(map[string]int),
data: make([]*DelayingDelivererItem, 0),
},
updateChannel: make(chan *DelayingDelivererItem, delayingDelivererUpdateChanCapacity),
stopChannel: make(chan struct{}),
}
}
// Deliver all items due before or equal to timestamp.
func (d *DelayingDeliverer) deliver(timestamp time.Time) {
for d.heap.Len() > 0 {
if timestamp.Before(d.heap.data[0].DeliveryTime) {
return
}
item := heap.Pop(d.heap).(*DelayingDelivererItem)
d.targetChannel <- item
}
}
func (d *DelayingDeliverer) run() {
for {
now := time.Now()
d.deliver(now)
nextWakeUp := now.Add(time.Hour)
if d.heap.Len() > 0 {
nextWakeUp = d.heap.data[0].DeliveryTime
}
sleepTime := nextWakeUp.Sub(now)
select {
case <-time.After(sleepTime):
break // just wake up and process the data
case item := <-d.updateChannel:
if position, found := d.heap.keyPosition[item.Key]; found {
if item.DeliveryTime.Before(d.heap.data[position].DeliveryTime) {
d.heap.data[position] = item
heap.Fix(d.heap, position)
}
// Ignore if later.
} else {
heap.Push(d.heap, item)
}
case <-d.stopChannel:
return
}
}
}
// Starts the DelayingDeliverer.
func (d *DelayingDeliverer) Start() {
go d.run()
}
// Stops the DelayingDeliverer. Undelivered items are discarded.
func (d *DelayingDeliverer) Stop() {
close(d.stopChannel)
}
// Delivers value at the given time.
func (d *DelayingDeliverer) DeliverAt(key string, value interface{}, deliveryTime time.Time) {
d.updateChannel <- &DelayingDelivererItem{
Key: key,
Value: value,
DeliveryTime: deliveryTime,
}
}
// Delivers value after the given delay.
func (d *DelayingDeliverer) DeliverAfter(key string, value interface{}, delay time.Duration) {
d.DeliverAt(key, value, time.Now().Add(delay))
}
// Gets target channel of the deliverer.
func (d *DelayingDeliverer) GetTargetChannel() chan *DelayingDelivererItem {
return d.targetChannel
}
// Starts Delaying deliverer with a handler listening on the target channel.
func (d *DelayingDeliverer) StartWithHandler(handler func(*DelayingDelivererItem)) {
go func() {
for {
select {
case item := <-d.targetChannel:
handler(item)
case <-d.stopChannel:
return
}
}
}()
d.Start()
}

View file

@ -0,0 +1,63 @@
/*
Copyright 2016 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 util
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestDelayingDeliverer(t *testing.T) {
targetChannel := make(chan *DelayingDelivererItem)
now := time.Now()
d := NewDelayingDelivererWithChannel(targetChannel)
d.Start()
defer d.Stop()
startupDelay := time.Second
d.DeliverAt("a", "aaa", now.Add(startupDelay+2*time.Millisecond))
d.DeliverAt("b", "bbb", now.Add(startupDelay+3*time.Millisecond))
d.DeliverAt("c", "ccc", now.Add(startupDelay+1*time.Millisecond))
d.DeliverAt("e", "eee", now.Add(time.Hour))
d.DeliverAt("e", "eee", now)
d.DeliverAt("d", "ddd", now.Add(time.Hour))
i0 := <-targetChannel
assert.Equal(t, "e", i0.Key)
assert.Equal(t, "eee", i0.Value.(string))
assert.Equal(t, now, i0.DeliveryTime)
i1 := <-targetChannel
received1 := time.Now()
assert.True(t, received1.Sub(now).Nanoseconds() > startupDelay.Nanoseconds())
assert.Equal(t, "c", i1.Key)
i2 := <-targetChannel
assert.Equal(t, "a", i2.Key)
i3 := <-targetChannel
assert.Equal(t, "b", i3.Key)
select {
case <-targetChannel:
t.Fatalf("Nothing should be received")
case <-time.After(time.Second):
// Ok. Expected
}
}

View file

@ -0,0 +1,25 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
"go_test",
"cgo_library",
)
go_library(
name = "go_default_library",
srcs = ["deletion_helper.go"],
tags = ["automanaged"],
deps = [
"//federation/pkg/federation-controller/util:go_default_library",
"//pkg/api:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/client/record:go_default_library",
"//pkg/runtime:go_default_library",
"//vendor:github.com/golang/glog",
],
)

View file

@ -0,0 +1,190 @@
/*
Copyright 2016 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 to help federation controllers to delete federated resources from
// underlying clusters when the resource is deleted from federation control
// plane.
package deletionhelper
import (
"fmt"
"strings"
"time"
"k8s.io/kubernetes/federation/pkg/federation-controller/util"
"k8s.io/kubernetes/pkg/api"
api_v1 "k8s.io/kubernetes/pkg/api/v1"
"k8s.io/kubernetes/pkg/client/record"
"k8s.io/kubernetes/pkg/runtime"
"github.com/golang/glog"
)
const (
// Add this finalizer to a federation resource if the resource should be
// deleted from all underlying clusters before being deleted from
// federation control plane.
// This is ignored if FinalizerOrphan is also present on the resource.
// In that case, both finalizers are removed from the resource and the
// resource is deleted from federation control plane without affecting
// the underlying clusters.
FinalizerDeleteFromUnderlyingClusters string = "federation.kubernetes.io/delete-from-underlying-clusters"
)
type HasFinalizerFunc func(runtime.Object, string) bool
type RemoveFinalizerFunc func(runtime.Object, string) (runtime.Object, error)
type AddFinalizerFunc func(runtime.Object, string) (runtime.Object, error)
type ObjNameFunc func(runtime.Object) string
type DeletionHelper struct {
hasFinalizerFunc HasFinalizerFunc
removeFinalizerFunc RemoveFinalizerFunc
addFinalizerFunc AddFinalizerFunc
objNameFunc ObjNameFunc
updateTimeout time.Duration
eventRecorder record.EventRecorder
informer util.FederatedInformer
updater util.FederatedUpdater
}
func NewDeletionHelper(
hasFinalizerFunc HasFinalizerFunc, removeFinalizerFunc RemoveFinalizerFunc,
addFinalizerFunc AddFinalizerFunc, objNameFunc ObjNameFunc,
updateTimeout time.Duration, eventRecorder record.EventRecorder,
informer util.FederatedInformer,
updater util.FederatedUpdater) *DeletionHelper {
return &DeletionHelper{
hasFinalizerFunc: hasFinalizerFunc,
removeFinalizerFunc: removeFinalizerFunc,
addFinalizerFunc: addFinalizerFunc,
objNameFunc: objNameFunc,
updateTimeout: updateTimeout,
eventRecorder: eventRecorder,
informer: informer,
updater: updater,
}
}
// Ensures that the given object has both FinalizerDeleteFromUnderlyingClusters
// and FinalizerOrphan finalizers.
// We do this so that the controller is always notified when a federation resource is deleted.
// If user deletes the resource with nil DeleteOptions or
// DeletionOptions.OrphanDependents = true then the apiserver removes the orphan finalizer
// and deletion helper does a cascading deletion.
// Otherwise, deletion helper just removes the federation resource and orphans
// the corresponding resources in underlying clusters.
// This method should be called before creating objects in underlying clusters.
func (dh *DeletionHelper) EnsureFinalizers(obj runtime.Object) (
runtime.Object, error) {
if !dh.hasFinalizerFunc(obj, FinalizerDeleteFromUnderlyingClusters) {
glog.V(2).Infof("Adding finalizer %s to %s", FinalizerDeleteFromUnderlyingClusters, dh.objNameFunc(obj))
obj, err := dh.addFinalizerFunc(obj, FinalizerDeleteFromUnderlyingClusters)
if err != nil {
return obj, err
}
}
if !dh.hasFinalizerFunc(obj, api_v1.FinalizerOrphan) {
glog.V(2).Infof("Adding finalizer %s to %s", api_v1.FinalizerOrphan, dh.objNameFunc(obj))
obj, err := dh.addFinalizerFunc(obj, api_v1.FinalizerOrphan)
if err != nil {
return obj, err
}
}
return obj, nil
}
// Deletes the resources corresponding to the given federated resource from
// all underlying clusters, unless it has the FinalizerOrphan finalizer.
// Removes FinalizerOrphan and FinalizerDeleteFromUnderlyingClusters finalizers
// when done.
// Callers are expected to keep calling this (with appropriate backoff) until
// it succeeds.
func (dh *DeletionHelper) HandleObjectInUnderlyingClusters(obj runtime.Object) (
runtime.Object, error) {
objName := dh.objNameFunc(obj)
glog.V(2).Infof("Handling deletion of federated dependents for object: %s", objName)
if !dh.hasFinalizerFunc(obj, FinalizerDeleteFromUnderlyingClusters) {
glog.V(2).Infof("obj does not have %s finalizer. Nothing to do", FinalizerDeleteFromUnderlyingClusters)
return obj, nil
}
hasOrphanFinalizer := dh.hasFinalizerFunc(obj, api_v1.FinalizerOrphan)
if hasOrphanFinalizer {
glog.V(2).Infof("Found finalizer orphan. Nothing to do, just remove the finalizer")
// If the obj has FinalizerOrphan finalizer, then we need to orphan the
// corresponding objects in underlying clusters.
// Just remove both the finalizers in that case.
obj, err := dh.removeFinalizerFunc(obj, api_v1.FinalizerOrphan)
if err != nil {
return obj, err
}
return dh.removeFinalizerFunc(obj, FinalizerDeleteFromUnderlyingClusters)
}
glog.V(2).Infof("Deleting obj %s from underlying clusters", objName)
// Else, we need to delete the obj from all underlying clusters.
unreadyClusters, err := dh.informer.GetUnreadyClusters()
if err != nil {
return nil, fmt.Errorf("failed to get a list of unready clusters: %v", err)
}
// TODO: Handle the case when cluster resource is watched after this is executed.
// This can happen if a namespace is deleted before its creation had been
// observed in all underlying clusters.
storeKey := dh.informer.GetTargetStore().GetKeyFor(obj)
clusterNsObjs, err := dh.informer.GetTargetStore().GetFromAllClusters(storeKey)
glog.V(3).Infof("Found %d objects in underlying clusters", len(clusterNsObjs))
if err != nil {
return nil, fmt.Errorf("failed to get object %s from underlying clusters: %v", objName, err)
}
operations := make([]util.FederatedOperation, 0)
for _, clusterNsObj := range clusterNsObjs {
operations = append(operations, util.FederatedOperation{
Type: util.OperationTypeDelete,
ClusterName: clusterNsObj.ClusterName,
Obj: clusterNsObj.Object.(runtime.Object),
})
}
err = dh.updater.UpdateWithOnError(operations, dh.updateTimeout, func(op util.FederatedOperation, operror error) {
objName := dh.objNameFunc(op.Obj)
dh.eventRecorder.Eventf(obj, api.EventTypeNormal, "DeleteInClusterFailed",
"Failed to delete obj %s in cluster %s: %v", objName, op.ClusterName, operror)
})
if err != nil {
return nil, fmt.Errorf("failed to execute updates for obj %s: %v", objName, err)
}
if len(operations) > 0 {
// We have deleted a bunch of resources.
// Wait for the store to observe all the deletions.
var clusterNames []string
for _, op := range operations {
clusterNames = append(clusterNames, op.ClusterName)
}
return nil, fmt.Errorf("waiting for object %s to be deleted from clusters: %s", objName, strings.Join(clusterNames, ", "))
}
// We have now deleted the object from all *ready* clusters.
// But still need to wait for clusters that are not ready to ensure that
// the object has been deleted from *all* clusters.
if len(unreadyClusters) != 0 {
var clusterNames []string
for _, cluster := range unreadyClusters {
clusterNames = append(clusterNames, cluster.Name)
}
return nil, fmt.Errorf("waiting for clusters %s to become ready to verify that obj %s has been deleted", strings.Join(clusterNames, ", "), objName)
}
// All done. Just remove the finalizer.
return dh.removeFinalizerFunc(obj, FinalizerDeleteFromUnderlyingClusters)
}

View file

@ -0,0 +1,75 @@
/*
Copyright 2016 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 util
import (
"reflect"
api_v1 "k8s.io/kubernetes/pkg/api/v1"
extensions_v1 "k8s.io/kubernetes/pkg/apis/extensions/v1beta1"
deputils "k8s.io/kubernetes/pkg/controller/deployment/util"
)
// Checks if cluster-independent, user provided data in two given Deployment are eqaul.
// This function assumes that revisions are not kept in sync across the clusters.
func DeploymentEquivalent(a, b *extensions_v1.Deployment) bool {
if a.Name != b.Name {
return false
}
if a.Namespace != b.Namespace {
return false
}
if !reflect.DeepEqual(a.Labels, b.Labels) && (len(a.Labels) != 0 || len(b.Labels) != 0) {
return false
}
hasKeysAndVals := func(x, y map[string]string) bool {
if x == nil {
x = map[string]string{}
}
if y == nil {
y = map[string]string{}
}
for k, v := range x {
if k == deputils.RevisionAnnotation {
continue
}
v2, found := y[k]
if !found || v != v2 {
return false
}
}
return true
}
return hasKeysAndVals(a.Annotations, b.Annotations) &&
hasKeysAndVals(b.Annotations, a.Annotations) &&
reflect.DeepEqual(a.Spec, b.Spec)
}
// Copies object meta for Deployment, skipping revision information.
func DeepCopyDeploymentObjectMeta(meta api_v1.ObjectMeta) api_v1.ObjectMeta {
meta = DeepCopyRelevantObjectMeta(meta)
delete(meta.Annotations, deputils.RevisionAnnotation)
return meta
}
// Copies object meta for Deployment, skipping revision information.
func DeepCopyDeployment(a *extensions_v1.Deployment) *extensions_v1.Deployment {
return &extensions_v1.Deployment{
ObjectMeta: DeepCopyDeploymentObjectMeta(a.ObjectMeta),
Spec: DeepCopyApiTypeOrPanic(a.Spec).(extensions_v1.DeploymentSpec),
}
}

View file

@ -0,0 +1,70 @@
/*
Copyright 2016 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 util
import (
"testing"
apiv1 "k8s.io/kubernetes/pkg/api/v1"
extensionsv1 "k8s.io/kubernetes/pkg/apis/extensions/v1beta1"
deputils "k8s.io/kubernetes/pkg/controller/deployment/util"
"github.com/stretchr/testify/assert"
)
func TestDeploymentEquivalent(t *testing.T) {
d1 := newDeployment()
d2 := newDeployment()
d2.Annotations = make(map[string]string)
d3 := newDeployment()
d3.Annotations = map[string]string{"a": "b"}
d4 := newDeployment()
d4.Annotations = map[string]string{deputils.RevisionAnnotation: "9"}
assert.True(t, DeploymentEquivalent(d1, d2))
assert.True(t, DeploymentEquivalent(d1, d2))
assert.True(t, DeploymentEquivalent(d1, d4))
assert.True(t, DeploymentEquivalent(d4, d1))
assert.False(t, DeploymentEquivalent(d3, d4))
assert.False(t, DeploymentEquivalent(d3, d1))
assert.True(t, DeploymentEquivalent(d3, d3))
}
func TestDeploymentCopy(t *testing.T) {
d1 := newDeployment()
d1.Annotations = map[string]string{deputils.RevisionAnnotation: "9", "a": "b"}
d2 := DeepCopyDeployment(d1)
assert.True(t, DeploymentEquivalent(d1, d2))
assert.Contains(t, d2.Annotations, "a")
assert.NotContains(t, d2.Annotations, deputils.RevisionAnnotation)
}
func newDeployment() *extensionsv1.Deployment {
replicas := int32(5)
return &extensionsv1.Deployment{
ObjectMeta: apiv1.ObjectMeta{
Name: "wrr",
Namespace: apiv1.NamespaceDefault,
SelfLink: "/api/v1/namespaces/default/deployments/name123",
},
Spec: extensionsv1.DeploymentSpec{
Replicas: &replicas,
},
}
}

View file

@ -0,0 +1,38 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
"go_test",
"cgo_library",
)
go_library(
name = "go_default_library",
srcs = ["eventsink.go"],
tags = ["automanaged"],
deps = [
"//federation/client/clientset_generated/federation_release_1_5:go_default_library",
"//pkg/api:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/client/record:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["eventsink_test.go"],
library = "go_default_library",
tags = ["automanaged"],
deps = [
"//federation/client/clientset_generated/federation_release_1_5/fake:go_default_library",
"//federation/pkg/federation-controller/util/test:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/client/testing/core:go_default_library",
"//pkg/runtime:go_default_library",
"//vendor:github.com/stretchr/testify/assert",
],
)

View file

@ -0,0 +1,50 @@
/*
Copyright 2016 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 eventsink
import (
fedclientset "k8s.io/kubernetes/federation/client/clientset_generated/federation_release_1_5"
api "k8s.io/kubernetes/pkg/api"
api_v1 "k8s.io/kubernetes/pkg/api/v1"
"k8s.io/kubernetes/pkg/client/record"
)
// Implemnts k8s.io/kubernetes/pkg/client/record.EventSink.
type FederatedEventSink struct {
clientset fedclientset.Interface
}
// To check if all required functions are implemented.
var _ record.EventSink = &FederatedEventSink{}
func NewFederatedEventSink(clientset fedclientset.Interface) *FederatedEventSink {
return &FederatedEventSink{
clientset: clientset,
}
}
func (fes *FederatedEventSink) Create(event *api_v1.Event) (*api_v1.Event, error) {
return fes.clientset.Core().Events(event.Namespace).Create(event)
}
func (fes *FederatedEventSink) Update(event *api_v1.Event) (*api_v1.Event, error) {
return fes.clientset.Core().Events(event.Namespace).Update(event)
}
func (fes *FederatedEventSink) Patch(event *api_v1.Event, data []byte) (*api_v1.Event, error) {
return fes.clientset.Core().Events(event.Namespace).Patch(event.Name, api.StrategicMergePatchType, data)
}

View file

@ -0,0 +1,70 @@
/*
Copyright 2016 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 eventsink
import (
"testing"
fakefedclientset "k8s.io/kubernetes/federation/client/clientset_generated/federation_release_1_5/fake"
. "k8s.io/kubernetes/federation/pkg/federation-controller/util/test"
apiv1 "k8s.io/kubernetes/pkg/api/v1"
"k8s.io/kubernetes/pkg/client/testing/core"
"k8s.io/kubernetes/pkg/runtime"
"github.com/stretchr/testify/assert"
)
func TestEventSink(t *testing.T) {
fakeFederationClient := &fakefedclientset.Clientset{}
createdChan := make(chan runtime.Object, 100)
fakeFederationClient.AddReactor("create", "events", func(action core.Action) (bool, runtime.Object, error) {
createAction := action.(core.CreateAction)
obj := createAction.GetObject()
createdChan <- obj
return true, obj, nil
})
updateChan := make(chan runtime.Object, 100)
fakeFederationClient.AddReactor("update", "events", func(action core.Action) (bool, runtime.Object, error) {
updateAction := action.(core.UpdateAction)
obj := updateAction.GetObject()
updateChan <- obj
return true, obj, nil
})
event := apiv1.Event{
ObjectMeta: apiv1.ObjectMeta{
Name: "bzium",
Namespace: "ns",
},
}
sink := NewFederatedEventSink(fakeFederationClient)
eventUpdated, err := sink.Create(&event)
assert.NoError(t, err)
eventV1 := GetObjectFromChan(createdChan).(*apiv1.Event)
assert.NotNil(t, eventV1)
// Just some simple sanity checks.
assert.Equal(t, event.Name, eventV1.Name)
assert.Equal(t, event.Name, eventUpdated.Name)
eventUpdated, err = sink.Update(&event)
assert.NoError(t, err)
eventV1 = GetObjectFromChan(updateChan).(*apiv1.Event)
assert.NotNil(t, eventV1)
// Just some simple sanity checks.
assert.Equal(t, event.Name, eventV1.Name)
assert.Equal(t, event.Name, eventUpdated.Name)
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,149 @@
/*
Copyright 2016 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 util
import (
"testing"
"time"
federationapi "k8s.io/kubernetes/federation/apis/federation/v1beta1"
fakefederationclientset "k8s.io/kubernetes/federation/client/clientset_generated/federation_release_1_5/fake"
apiv1 "k8s.io/kubernetes/pkg/api/v1"
"k8s.io/kubernetes/pkg/client/cache"
kubeclientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5"
fakekubeclientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5/fake"
"k8s.io/kubernetes/pkg/client/testing/core"
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/watch"
"github.com/stretchr/testify/assert"
)
// Basic test for Federated Informer. Checks whether the subinformer are added and deleted
// when the corresponding cluster entries appear and disappear from etcd.
func TestFederatedInformer(t *testing.T) {
fakeFederationClient := &fakefederationclientset.Clientset{}
// Add a single cluster to federation and remove it when needed.
cluster := federationapi.Cluster{
ObjectMeta: apiv1.ObjectMeta{
Name: "mycluster",
},
Status: federationapi.ClusterStatus{
Conditions: []federationapi.ClusterCondition{
{Type: federationapi.ClusterReady, Status: apiv1.ConditionTrue},
},
},
}
fakeFederationClient.AddReactor("list", "clusters", func(action core.Action) (bool, runtime.Object, error) {
return true, &federationapi.ClusterList{Items: []federationapi.Cluster{cluster}}, nil
})
deleteChan := make(chan struct{})
fakeFederationClient.AddWatchReactor("clusters", func(action core.Action) (bool, watch.Interface, error) {
fakeWatch := watch.NewFake()
go func() {
<-deleteChan
fakeWatch.Delete(&cluster)
}()
return true, fakeWatch, nil
})
fakeKubeClient := &fakekubeclientset.Clientset{}
// There is a single service ns1/s1 in cluster mycluster.
service := apiv1.Service{
ObjectMeta: apiv1.ObjectMeta{
Namespace: "ns1",
Name: "s1",
},
}
fakeKubeClient.AddReactor("list", "services", func(action core.Action) (bool, runtime.Object, error) {
return true, &apiv1.ServiceList{Items: []apiv1.Service{service}}, nil
})
fakeKubeClient.AddWatchReactor("services", func(action core.Action) (bool, watch.Interface, error) {
return true, watch.NewFake(), nil
})
targetInformerFactory := func(cluster *federationapi.Cluster, clientset kubeclientset.Interface) (cache.Store, cache.ControllerInterface) {
return cache.NewInformer(
&cache.ListWatch{
ListFunc: func(options apiv1.ListOptions) (runtime.Object, error) {
return clientset.Core().Services(apiv1.NamespaceAll).List(options)
},
WatchFunc: func(options apiv1.ListOptions) (watch.Interface, error) {
return clientset.Core().Services(apiv1.NamespaceAll).Watch(options)
},
},
&apiv1.Service{},
10*time.Second,
cache.ResourceEventHandlerFuncs{})
}
addedClusters := make(chan string, 1)
deletedClusters := make(chan string, 1)
lifecycle := ClusterLifecycleHandlerFuncs{
ClusterAvailable: func(cluster *federationapi.Cluster) {
addedClusters <- cluster.Name
close(addedClusters)
},
ClusterUnavailable: func(cluster *federationapi.Cluster, _ []interface{}) {
deletedClusters <- cluster.Name
close(deletedClusters)
},
}
informer := NewFederatedInformer(fakeFederationClient, targetInformerFactory, &lifecycle).(*federatedInformerImpl)
informer.clientFactory = func(cluster *federationapi.Cluster) (kubeclientset.Interface, error) {
return fakeKubeClient, nil
}
assert.NotNil(t, informer)
informer.Start()
// Wait until mycluster is synced.
for !informer.GetTargetStore().ClustersSynced([]*federationapi.Cluster{&cluster}) {
time.Sleep(time.Millisecond * 100)
}
readyClusters, err := informer.GetReadyClusters()
assert.NoError(t, err)
assert.Contains(t, readyClusters, &cluster)
serviceList, err := informer.GetTargetStore().List()
assert.NoError(t, err)
federatedService := FederatedObject{ClusterName: "mycluster", Object: &service}
assert.Contains(t, serviceList, federatedService)
service1, found, err := informer.GetTargetStore().GetByKey("mycluster", "ns1/s1")
assert.NoError(t, err)
assert.True(t, found)
assert.EqualValues(t, &service, service1)
assert.Equal(t, "mycluster", <-addedClusters)
// All checked, lets delete the cluster.
deleteChan <- struct{}{}
for !informer.GetTargetStore().ClustersSynced([]*federationapi.Cluster{}) {
time.Sleep(time.Millisecond * 100)
}
readyClusters, err = informer.GetReadyClusters()
assert.NoError(t, err)
assert.Empty(t, readyClusters)
serviceList, err = informer.GetTargetStore().List()
assert.NoError(t, err)
assert.Empty(t, serviceList)
assert.Equal(t, "mycluster", <-deletedClusters)
// Test complete.
informer.Stop()
}

View file

@ -0,0 +1,122 @@
/*
Copyright 2016 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 util
import (
"fmt"
"time"
kubeclientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5"
pkgruntime "k8s.io/kubernetes/pkg/runtime"
)
// Type of the operation that can be executed in Federated.
type FederatedOperationType string
const (
OperationTypeAdd = "add"
OperationTypeUpdate = "update"
OperationTypeDelete = "delete"
)
// FederatedOperation definition contains type (add/update/delete) and the object itself.
type FederatedOperation struct {
Type FederatedOperationType
ClusterName string
Obj pkgruntime.Object
}
// A helper that executes the given set of updates on federation, in parallel.
type FederatedUpdater interface {
// Executes the given set of operations within the specified timeout.
// Timeout is best-effort. There is no guarantee that the underlying operations are
// stopped when it is reached. However the function will return after the timeout
// with a non-nil error.
Update([]FederatedOperation, time.Duration) error
UpdateWithOnError([]FederatedOperation, time.Duration, func(FederatedOperation, error)) error
}
// A function that executes some operation using the passed client and object.
type FederatedOperationHandler func(kubeclientset.Interface, pkgruntime.Object) error
type federatedUpdaterImpl struct {
federation FederationView
addFunction FederatedOperationHandler
updateFunction FederatedOperationHandler
deleteFunction FederatedOperationHandler
}
func NewFederatedUpdater(federation FederationView, add, update, del FederatedOperationHandler) FederatedUpdater {
return &federatedUpdaterImpl{
federation: federation,
addFunction: add,
updateFunction: update,
deleteFunction: del,
}
}
func (fu *federatedUpdaterImpl) Update(ops []FederatedOperation, timeout time.Duration) error {
return fu.UpdateWithOnError(ops, timeout, nil)
}
func (fu *federatedUpdaterImpl) UpdateWithOnError(ops []FederatedOperation, timeout time.Duration, onError func(FederatedOperation, error)) error {
done := make(chan error, len(ops))
for _, op := range ops {
go func(op FederatedOperation) {
clusterName := op.ClusterName
// TODO: Ensure that the clientset has reasonable timeout.
clientset, err := fu.federation.GetClientsetForCluster(clusterName)
if err != nil {
done <- err
return
}
switch op.Type {
case OperationTypeAdd:
err = fu.addFunction(clientset, op.Obj)
case OperationTypeUpdate:
err = fu.updateFunction(clientset, op.Obj)
case OperationTypeDelete:
err = fu.deleteFunction(clientset, op.Obj)
}
if err != nil && onError != nil {
onError(op, err)
}
done <- err
}(op)
}
start := time.Now()
for i := 0; i < len(ops); i++ {
now := time.Now()
if !now.Before(start.Add(timeout)) {
return fmt.Errorf("failed to finish all operations in %v", timeout)
}
select {
case err := <-done:
if err != nil {
return err
}
case <-time.After(start.Add(timeout).Sub(now)):
return fmt.Errorf("failed to finish all operations in %v", timeout)
}
}
// All operations finished in time.
return nil
}

View file

@ -0,0 +1,148 @@
/*
Copyright 2016 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 util
import (
"fmt"
"testing"
"time"
federationapi "k8s.io/kubernetes/federation/apis/federation/v1beta1"
apiv1 "k8s.io/kubernetes/pkg/api/v1"
kubeclientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5"
fakekubeclientset "k8s.io/kubernetes/pkg/client/clientset_generated/release_1_5/fake"
pkgruntime "k8s.io/kubernetes/pkg/runtime"
"github.com/stretchr/testify/assert"
)
// Fake federation view.
type fakeFederationView struct {
}
// Verify that fakeFederationView implements FederationView interface
var _ FederationView = &fakeFederationView{}
func (f *fakeFederationView) GetClientsetForCluster(clusterName string) (kubeclientset.Interface, error) {
return &fakekubeclientset.Clientset{}, nil
}
func (f *fakeFederationView) GetReadyClusters() ([]*federationapi.Cluster, error) {
return []*federationapi.Cluster{}, nil
}
func (f *fakeFederationView) GetUnreadyClusters() ([]*federationapi.Cluster, error) {
return []*federationapi.Cluster{}, nil
}
func (f *fakeFederationView) GetReadyCluster(name string) (*federationapi.Cluster, bool, error) {
return nil, false, nil
}
func (f *fakeFederationView) ClustersSynced() bool {
return true
}
func TestFederatedUpdaterOK(t *testing.T) {
addChan := make(chan string, 5)
updateChan := make(chan string, 5)
updater := NewFederatedUpdater(&fakeFederationView{},
func(_ kubeclientset.Interface, obj pkgruntime.Object) error {
service := obj.(*apiv1.Service)
addChan <- service.Name
return nil
},
func(_ kubeclientset.Interface, obj pkgruntime.Object) error {
service := obj.(*apiv1.Service)
updateChan <- service.Name
return nil
},
noop)
err := updater.Update([]FederatedOperation{
{
Type: OperationTypeAdd,
Obj: makeService("A", "s1"),
},
{
Type: OperationTypeUpdate,
Obj: makeService("B", "s2"),
},
}, time.Minute)
assert.NoError(t, err)
add := <-addChan
update := <-updateChan
assert.Equal(t, "s1", add)
assert.Equal(t, "s2", update)
}
func TestFederatedUpdaterError(t *testing.T) {
updater := NewFederatedUpdater(&fakeFederationView{},
func(_ kubeclientset.Interface, obj pkgruntime.Object) error {
return fmt.Errorf("boom")
}, noop, noop)
err := updater.Update([]FederatedOperation{
{
Type: OperationTypeAdd,
Obj: makeService("A", "s1"),
},
{
Type: OperationTypeUpdate,
Obj: makeService("B", "s1"),
},
}, time.Minute)
assert.Error(t, err)
}
func TestFederatedUpdaterTimeout(t *testing.T) {
start := time.Now()
updater := NewFederatedUpdater(&fakeFederationView{},
func(_ kubeclientset.Interface, obj pkgruntime.Object) error {
time.Sleep(time.Minute)
return nil
},
noop, noop)
err := updater.Update([]FederatedOperation{
{
Type: OperationTypeAdd,
Obj: makeService("A", "s1"),
},
{
Type: OperationTypeUpdate,
Obj: makeService("B", "s1"),
},
}, time.Second)
end := time.Now()
assert.Error(t, err)
assert.True(t, start.Add(10*time.Second).After(end))
}
func makeService(cluster, name string) *apiv1.Service {
return &apiv1.Service{
ObjectMeta: apiv1.ObjectMeta{
Namespace: "ns1",
Name: name,
},
}
}
func noop(_ kubeclientset.Interface, _ pkgruntime.Object) error {
return nil
}

View file

@ -0,0 +1,79 @@
/*
Copyright 2016 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 util
import (
"fmt"
"reflect"
apiv1 "k8s.io/kubernetes/pkg/api/v1"
"k8s.io/kubernetes/pkg/client/cache"
pkgruntime "k8s.io/kubernetes/pkg/runtime"
)
// Returns cache.ResourceEventHandlerFuncs that trigger the given function
// on all object changes.
func NewTriggerOnAllChanges(triggerFunc func(pkgruntime.Object)) *cache.ResourceEventHandlerFuncs {
return &cache.ResourceEventHandlerFuncs{
DeleteFunc: func(old interface{}) {
oldObj := old.(pkgruntime.Object)
triggerFunc(oldObj)
},
AddFunc: func(cur interface{}) {
curObj := cur.(pkgruntime.Object)
triggerFunc(curObj)
},
UpdateFunc: func(old, cur interface{}) {
curObj := cur.(pkgruntime.Object)
if !reflect.DeepEqual(old, cur) {
triggerFunc(curObj)
}
},
}
}
// Returns cache.ResourceEventHandlerFuncs that trigger the given function
// on object add and delete as well as spec/object meta on update.
func NewTriggerOnMetaAndSpecChanges(triggerFunc func(pkgruntime.Object)) *cache.ResourceEventHandlerFuncs {
getFieldOrPanic := func(obj interface{}, fieldName string) interface{} {
val := reflect.ValueOf(obj).Elem().FieldByName(fieldName)
if val.IsValid() {
return val.Interface()
} else {
panic(fmt.Errorf("field not found: %s", fieldName))
}
}
return &cache.ResourceEventHandlerFuncs{
DeleteFunc: func(old interface{}) {
oldObj := old.(pkgruntime.Object)
triggerFunc(oldObj)
},
AddFunc: func(cur interface{}) {
curObj := cur.(pkgruntime.Object)
triggerFunc(curObj)
},
UpdateFunc: func(old, cur interface{}) {
curObj := cur.(pkgruntime.Object)
oldMeta := getFieldOrPanic(old, "ObjectMeta").(apiv1.ObjectMeta)
curMeta := getFieldOrPanic(cur, "ObjectMeta").(apiv1.ObjectMeta)
if !ObjectMetaEquivalent(oldMeta, curMeta) ||
!reflect.DeepEqual(getFieldOrPanic(old, "Spec"), getFieldOrPanic(cur, "Spec")) {
triggerFunc(curObj)
}
},
}
}

View file

@ -0,0 +1,99 @@
/*
Copyright 2016 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 util
import (
"testing"
apiv1 "k8s.io/kubernetes/pkg/api/v1"
pkgruntime "k8s.io/kubernetes/pkg/runtime"
"github.com/stretchr/testify/assert"
)
func TestHandlers(t *testing.T) {
// There is a single service ns1/s1 in cluster mycluster.
service := apiv1.Service{
ObjectMeta: apiv1.ObjectMeta{
Namespace: "ns1",
Name: "s1",
},
}
service2 := apiv1.Service{
ObjectMeta: apiv1.ObjectMeta{
Namespace: "ns1",
Name: "s1",
Annotations: map[string]string{
"A": "B",
},
},
}
triggerChan := make(chan struct{}, 1)
triggered := func() bool {
select {
case <-triggerChan:
return true
default:
return false
}
}
trigger := NewTriggerOnAllChanges(
func(obj pkgruntime.Object) {
triggerChan <- struct{}{}
})
trigger.OnAdd(&service)
assert.True(t, triggered())
trigger.OnDelete(&service)
assert.True(t, triggered())
trigger.OnUpdate(&service, &service)
assert.False(t, triggered())
trigger.OnUpdate(&service, &service2)
assert.True(t, triggered())
trigger2 := NewTriggerOnMetaAndSpecChanges(
func(obj pkgruntime.Object) {
triggerChan <- struct{}{}
},
)
trigger2.OnAdd(&service)
assert.True(t, triggered())
trigger2.OnDelete(&service)
assert.True(t, triggered())
trigger2.OnUpdate(&service, &service)
assert.False(t, triggered())
trigger2.OnUpdate(&service, &service2)
assert.True(t, triggered())
service3 := apiv1.Service{
ObjectMeta: apiv1.ObjectMeta{
Namespace: "ns1",
Name: "s1",
},
Status: apiv1.ServiceStatus{
LoadBalancer: apiv1.LoadBalancerStatus{
Ingress: []apiv1.LoadBalancerIngress{{
Hostname: "A",
}},
},
},
}
trigger2.OnUpdate(&service, &service3)
assert.False(t, triggered())
}

View file

@ -0,0 +1,94 @@
/*
Copyright 2016 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 util
import (
"reflect"
api_v1 "k8s.io/kubernetes/pkg/api/v1"
"k8s.io/kubernetes/pkg/conversion"
"k8s.io/kubernetes/pkg/runtime"
)
// Copies cluster-independent, user provided data from the given ObjectMeta struct. If in
// the future the ObjectMeta structure is expanded then any field that is not populated
// by the api server should be included here.
func copyObjectMeta(obj api_v1.ObjectMeta) api_v1.ObjectMeta {
return api_v1.ObjectMeta{
Name: obj.Name,
Namespace: obj.Namespace,
Labels: obj.Labels,
Annotations: obj.Annotations,
}
}
// Deep copies cluster-independent, user provided data from the given ObjectMeta struct. If in
// the future the ObjectMeta structure is expanded then any field that is not populated
// by the api server should be included here.
func DeepCopyRelevantObjectMeta(obj api_v1.ObjectMeta) api_v1.ObjectMeta {
copyMeta := copyObjectMeta(obj)
if obj.Labels != nil {
copyMeta.Labels = make(map[string]string)
for key, val := range obj.Labels {
copyMeta.Labels[key] = val
}
}
if obj.Annotations != nil {
copyMeta.Annotations = make(map[string]string)
for key, val := range obj.Annotations {
copyMeta.Annotations[key] = val
}
}
return copyMeta
}
// Checks if cluster-independent, user provided data in two given ObjectMeta are equal. If in
// the future the ObjectMeta structure is expanded then any field that is not populated
// by the api server should be included here.
func ObjectMetaEquivalent(a, b api_v1.ObjectMeta) bool {
if a.Name != b.Name {
return false
}
if a.Namespace != b.Namespace {
return false
}
if !reflect.DeepEqual(a.Labels, b.Labels) && (len(a.Labels) != 0 || len(b.Labels) != 0) {
return false
}
if !reflect.DeepEqual(a.Annotations, b.Annotations) && (len(a.Annotations) != 0 || len(b.Annotations) != 0) {
return false
}
return true
}
// Checks if cluster-independent, user provided data in ObjectMeta and Spec in two given top
// level api objects are equivalent.
func ObjectMetaAndSpecEquivalent(a, b runtime.Object) bool {
objectMetaA := reflect.ValueOf(a).Elem().FieldByName("ObjectMeta").Interface().(api_v1.ObjectMeta)
objectMetaB := reflect.ValueOf(b).Elem().FieldByName("ObjectMeta").Interface().(api_v1.ObjectMeta)
specA := reflect.ValueOf(a).Elem().FieldByName("Spec").Interface()
specB := reflect.ValueOf(b).Elem().FieldByName("Spec").Interface()
return ObjectMetaEquivalent(objectMetaA, objectMetaB) && reflect.DeepEqual(specA, specB)
}
func DeepCopyApiTypeOrPanic(item interface{}) interface{} {
result, err := conversion.NewCloner().DeepCopy(item)
if err != nil {
panic(err)
}
return result
}

View file

@ -0,0 +1,116 @@
/*
Copyright 2016 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 util
import (
"testing"
api_v1 "k8s.io/kubernetes/pkg/api/v1"
"github.com/stretchr/testify/assert"
)
func TestObjectMeta(t *testing.T) {
o1 := api_v1.ObjectMeta{
Namespace: "ns1",
Name: "s1",
UID: "1231231412",
ResourceVersion: "999",
}
o2 := copyObjectMeta(o1)
o3 := api_v1.ObjectMeta{
Namespace: "ns1",
Name: "s1",
UID: "1231231412",
Annotations: map[string]string{"A": "B"},
}
o4 := api_v1.ObjectMeta{
Namespace: "ns1",
Name: "s1",
UID: "1231255531412",
Annotations: map[string]string{"A": "B"},
}
o5 := api_v1.ObjectMeta{
Namespace: "ns1",
Name: "s1",
ResourceVersion: "1231231412",
Annotations: map[string]string{"A": "B"},
}
o6 := api_v1.ObjectMeta{
Namespace: "ns1",
Name: "s1",
ResourceVersion: "1231255531412",
Annotations: map[string]string{"A": "B"},
}
o7 := api_v1.ObjectMeta{
Namespace: "ns1",
Name: "s1",
ResourceVersion: "1231255531412",
Annotations: map[string]string{},
Labels: map[string]string{},
}
o8 := api_v1.ObjectMeta{
Namespace: "ns1",
Name: "s1",
ResourceVersion: "1231255531412",
}
assert.Equal(t, 0, len(o2.UID))
assert.Equal(t, 0, len(o2.ResourceVersion))
assert.Equal(t, o1.Name, o2.Name)
assert.True(t, ObjectMetaEquivalent(o1, o2))
assert.False(t, ObjectMetaEquivalent(o1, o3))
assert.True(t, ObjectMetaEquivalent(o3, o4))
assert.True(t, ObjectMetaEquivalent(o5, o6))
assert.True(t, ObjectMetaEquivalent(o3, o5))
assert.True(t, ObjectMetaEquivalent(o7, o8))
assert.True(t, ObjectMetaEquivalent(o8, o7))
}
func TestObjectMetaAndSpec(t *testing.T) {
s1 := api_v1.Service{
ObjectMeta: api_v1.ObjectMeta{
Namespace: "ns1",
Name: "s1",
},
Spec: api_v1.ServiceSpec{
ExternalName: "Service1",
},
}
s1b := s1
s2 := api_v1.Service{
ObjectMeta: api_v1.ObjectMeta{
Namespace: "ns1",
Name: "s2",
},
Spec: api_v1.ServiceSpec{
ExternalName: "Service1",
},
}
s3 := api_v1.Service{
ObjectMeta: api_v1.ObjectMeta{
Namespace: "ns1",
Name: "s1",
},
Spec: api_v1.ServiceSpec{
ExternalName: "Service2",
},
}
assert.True(t, ObjectMetaAndSpecEquivalent(&s1, &s1b))
assert.False(t, ObjectMetaAndSpecEquivalent(&s1, &s2))
assert.False(t, ObjectMetaAndSpecEquivalent(&s1, &s3))
assert.False(t, ObjectMetaAndSpecEquivalent(&s2, &s3))
}

View file

@ -0,0 +1,29 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
"go_test",
"cgo_library",
)
go_library(
name = "go_default_library",
srcs = ["planner.go"],
tags = ["automanaged"],
deps = ["//federation/apis/federation:go_default_library"],
)
go_test(
name = "go_default_test",
srcs = ["planner_test.go"],
library = "go_default_library",
tags = ["automanaged"],
deps = [
"//federation/apis/federation:go_default_library",
"//vendor:github.com/stretchr/testify/assert",
],
)

View file

@ -0,0 +1,238 @@
/*
Copyright 2016 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 planner
import (
"hash/fnv"
"sort"
fedapi "k8s.io/kubernetes/federation/apis/federation"
)
// Planner decides how many out of the given replicas should be placed in each of the
// federated clusters.
type Planner struct {
preferences *fedapi.FederatedReplicaSetPreferences
}
type namedClusterReplicaSetPreferences struct {
clusterName string
hash uint32
fedapi.ClusterReplicaSetPreferences
}
type byWeight []*namedClusterReplicaSetPreferences
func (a byWeight) Len() int { return len(a) }
func (a byWeight) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
// Preferences are sorted according by decreasing weight and increasing hash (built on top of cluster name and rs name).
// Sorting is made by a hash to avoid assigning single-replica rs to the alphabetically smallest cluster.
func (a byWeight) Less(i, j int) bool {
return (a[i].Weight > a[j].Weight) || (a[i].Weight == a[j].Weight && a[i].hash < a[j].hash)
}
func NewPlanner(preferences *fedapi.FederatedReplicaSetPreferences) *Planner {
return &Planner{
preferences: preferences,
}
}
// Distribute the desired number of replicas among the given cluster according to the planner preferences.
// The function tries its best to assign each cluster the preferred number of replicas, however if
// sum of MinReplicas for all cluster is bigger thant replicasToDistribute then some cluster will not
// have all of the replicas assigned. In such case a cluster with higher weight has priority over
// cluster with lower weight (or with lexicographically smaller name in case of draw).
// It can also use the current replica count and estimated capacity to provide better planning and
// adhere to rebalance policy. To avoid prioritization of clusters with smaller lexiconographical names
// a semi-random string (like replica set name) can be provided.
// Two maps are returned:
// * a map that contains information how many replicas will be possible to run in a cluster.
// * a map that contains information how many extra replicas would be nice to schedule in a cluster so,
// if by chance, they are scheudled we will be closer to the desired replicas layout.
func (p *Planner) Plan(replicasToDistribute int64, availableClusters []string, currentReplicaCount map[string]int64,
estimatedCapacity map[string]int64, replicaSetKey string) (map[string]int64, map[string]int64) {
preferences := make([]*namedClusterReplicaSetPreferences, 0, len(availableClusters))
plan := make(map[string]int64, len(preferences))
overflow := make(map[string]int64, len(preferences))
named := func(name string, pref fedapi.ClusterReplicaSetPreferences) *namedClusterReplicaSetPreferences {
// Seems to work better than addler for our case.
hasher := fnv.New32()
hasher.Write([]byte(name))
hasher.Write([]byte(replicaSetKey))
return &namedClusterReplicaSetPreferences{
clusterName: name,
hash: hasher.Sum32(),
ClusterReplicaSetPreferences: pref,
}
}
for _, cluster := range availableClusters {
if localRSP, found := p.preferences.Clusters[cluster]; found {
preferences = append(preferences, named(cluster, localRSP))
} else {
if localRSP, found := p.preferences.Clusters["*"]; found {
preferences = append(preferences, named(cluster, localRSP))
} else {
plan[cluster] = int64(0)
}
}
}
sort.Sort(byWeight(preferences))
remainingReplicas := replicasToDistribute
// Assign each cluster the minimum number of replicas it requested.
for _, preference := range preferences {
min := minInt64(preference.MinReplicas, remainingReplicas)
if capacity, hasCapacity := estimatedCapacity[preference.clusterName]; hasCapacity {
min = minInt64(min, capacity)
}
remainingReplicas -= min
plan[preference.clusterName] = min
}
// This map contains information how many replicas were assigned to
// the cluster based only on the current replica count and
// rebalance=false preference. It will be later used in remaining replica
// distribution code.
preallocated := make(map[string]int64)
if p.preferences.Rebalance == false {
for _, preference := range preferences {
planned := plan[preference.clusterName]
count, hasSome := currentReplicaCount[preference.clusterName]
if hasSome && count > planned {
target := count
if preference.MaxReplicas != nil {
target = minInt64(*preference.MaxReplicas, target)
}
if capacity, hasCapacity := estimatedCapacity[preference.clusterName]; hasCapacity {
target = minInt64(capacity, target)
}
extra := minInt64(target-planned, remainingReplicas)
if extra < 0 {
extra = 0
}
remainingReplicas -= extra
preallocated[preference.clusterName] = extra
plan[preference.clusterName] = extra + planned
}
}
}
modified := true
// It is possible single pass of the loop is not enough to distribue all replicas among clusters due
// to weight, max and rounding corner cases. In such case we iterate until either
// there is no replicas or no cluster gets any more replicas or the number
// of attempts is less than available cluster count. If there is no preallocated pods
// every loop either distributes all remainingReplicas or maxes out at least one cluster.
// If there are preallocated then the replica spreading may take longer.
// We reduce the number of pending preallocated replicas by at least half with each iteration so
// we may need log(replicasAtStart) iterations.
// TODO: Prove that clusterCount * log(replicas) iterations solves the problem or adjust the number.
// TODO: This algorithm is O(clusterCount^2 * log(replicas)) which is good for up to 100 clusters.
// Find something faster.
for trial := 0; modified && remainingReplicas > 0; trial++ {
modified = false
weightSum := int64(0)
for _, preference := range preferences {
weightSum += preference.Weight
}
newPreferences := make([]*namedClusterReplicaSetPreferences, 0, len(preferences))
distributeInThisLoop := remainingReplicas
for _, preference := range preferences {
if weightSum > 0 {
start := plan[preference.clusterName]
// Distribute the remaining replicas, rounding fractions always up.
extra := (distributeInThisLoop*preference.Weight + weightSum - 1) / weightSum
extra = minInt64(extra, remainingReplicas)
// Account preallocated.
prealloc := preallocated[preference.clusterName]
usedPrealloc := minInt64(extra, prealloc)
preallocated[preference.clusterName] = prealloc - usedPrealloc
extra = extra - usedPrealloc
if usedPrealloc > 0 {
modified = true
}
// In total there should be the amount that was there at start plus whatever is due
// in this iteration
total := start + extra
// Check if we don't overflow the cluster, and if yes don't consider this cluster
// in any of the following iterations.
full := false
if preference.MaxReplicas != nil && total > *preference.MaxReplicas {
total = *preference.MaxReplicas
full = true
}
if capacity, hasCapacity := estimatedCapacity[preference.clusterName]; hasCapacity && total > capacity {
overflow[preference.clusterName] = total - capacity
total = capacity
full = true
}
if !full {
newPreferences = append(newPreferences, preference)
}
// Only total-start replicas were actually taken.
remainingReplicas -= (total - start)
plan[preference.clusterName] = total
// Something extra got scheduled on this cluster.
if total > start {
modified = true
}
} else {
break
}
}
preferences = newPreferences
}
if p.preferences.Rebalance {
return plan, overflow
} else {
// If rebalance = false then overflow is trimmed at the level
// of replicas that it failed to place somewhere.
newOverflow := make(map[string]int64)
for key, value := range overflow {
value = minInt64(value, remainingReplicas)
if value > 0 {
newOverflow[key] = value
}
}
return plan, newOverflow
}
}
func minInt64(a int64, b int64) int64 {
if a < b {
return a
}
return b
}

View file

@ -0,0 +1,348 @@
/*
Copyright 2016 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 planner
import (
"testing"
fedapi "k8s.io/kubernetes/federation/apis/federation"
"github.com/stretchr/testify/assert"
)
func doCheck(t *testing.T, pref map[string]fedapi.ClusterReplicaSetPreferences, replicas int64, clusters []string, expected map[string]int64) {
planer := NewPlanner(&fedapi.FederatedReplicaSetPreferences{
Clusters: pref,
})
plan, overflow := planer.Plan(replicas, clusters, map[string]int64{}, map[string]int64{}, "")
assert.EqualValues(t, expected, plan)
assert.Equal(t, 0, len(overflow))
}
func doCheckWithExisting(t *testing.T, pref map[string]fedapi.ClusterReplicaSetPreferences, replicas int64, clusters []string,
existing map[string]int64, expected map[string]int64) {
planer := NewPlanner(&fedapi.FederatedReplicaSetPreferences{
Clusters: pref,
})
plan, overflow := planer.Plan(replicas, clusters, existing, map[string]int64{}, "")
assert.Equal(t, 0, len(overflow))
assert.EqualValues(t, expected, plan)
}
func doCheckWithExistingAndCapacity(t *testing.T, rebalance bool, pref map[string]fedapi.ClusterReplicaSetPreferences, replicas int64, clusters []string,
existing map[string]int64,
capacity map[string]int64,
expected map[string]int64,
expectedOverflow map[string]int64) {
planer := NewPlanner(&fedapi.FederatedReplicaSetPreferences{
Rebalance: rebalance,
Clusters: pref,
})
plan, overflow := planer.Plan(replicas, clusters, existing, capacity, "")
assert.EqualValues(t, expected, plan)
assert.Equal(t, expectedOverflow, overflow)
}
func pint(val int64) *int64 {
return &val
}
func TestEqual(t *testing.T) {
doCheck(t, map[string]fedapi.ClusterReplicaSetPreferences{
"*": {Weight: 1}},
50, []string{"A", "B", "C"},
// hash dependent
map[string]int64{"A": 16, "B": 17, "C": 17})
doCheck(t, map[string]fedapi.ClusterReplicaSetPreferences{
"*": {Weight: 1}},
50, []string{"A", "B"},
map[string]int64{"A": 25, "B": 25})
doCheck(t, map[string]fedapi.ClusterReplicaSetPreferences{
"*": {Weight: 1}},
1, []string{"A", "B"},
// hash dependent
map[string]int64{"A": 0, "B": 1})
doCheck(t, map[string]fedapi.ClusterReplicaSetPreferences{
"*": {Weight: 1}},
1, []string{"A", "B", "C", "D"},
// hash dependent
map[string]int64{"A": 0, "B": 0, "C": 0, "D": 1})
doCheck(t, map[string]fedapi.ClusterReplicaSetPreferences{
"*": {Weight: 1}},
1, []string{"A"},
map[string]int64{"A": 1})
doCheck(t, map[string]fedapi.ClusterReplicaSetPreferences{
"*": {Weight: 1}},
1, []string{},
map[string]int64{})
}
func TestEqualWithExisting(t *testing.T) {
doCheckWithExisting(t, map[string]fedapi.ClusterReplicaSetPreferences{
"*": {Weight: 1}},
50, []string{"A", "B", "C"},
map[string]int64{"C": 30},
map[string]int64{"A": 10, "B": 10, "C": 30})
doCheckWithExisting(t, map[string]fedapi.ClusterReplicaSetPreferences{
"*": {Weight: 1}},
50, []string{"A", "B"},
map[string]int64{"A": 30},
map[string]int64{"A": 30, "B": 20})
doCheckWithExisting(t, map[string]fedapi.ClusterReplicaSetPreferences{
"*": {Weight: 1}},
15, []string{"A", "B"},
map[string]int64{"A": 0, "B": 8},
map[string]int64{"A": 7, "B": 8})
doCheckWithExisting(t, map[string]fedapi.ClusterReplicaSetPreferences{
"*": {Weight: 1}},
15, []string{"A", "B"},
map[string]int64{"A": 1, "B": 8},
map[string]int64{"A": 7, "B": 8})
doCheckWithExisting(t, map[string]fedapi.ClusterReplicaSetPreferences{
"*": {Weight: 1}},
15, []string{"A", "B"},
map[string]int64{"A": 4, "B": 8},
map[string]int64{"A": 7, "B": 8})
doCheckWithExisting(t, map[string]fedapi.ClusterReplicaSetPreferences{
"*": {Weight: 1}},
15, []string{"A", "B"},
map[string]int64{"A": 5, "B": 8},
map[string]int64{"A": 7, "B": 8})
doCheckWithExisting(t, map[string]fedapi.ClusterReplicaSetPreferences{
"*": {Weight: 1}},
15, []string{"A", "B"},
map[string]int64{"A": 6, "B": 8},
map[string]int64{"A": 7, "B": 8})
doCheckWithExisting(t, map[string]fedapi.ClusterReplicaSetPreferences{
"*": {Weight: 1}},
15, []string{"A", "B"},
map[string]int64{"A": 7, "B": 8},
map[string]int64{"A": 7, "B": 8})
doCheckWithExisting(t, map[string]fedapi.ClusterReplicaSetPreferences{
"*": {Weight: 1}},
500000, []string{"A", "B"},
map[string]int64{"A": 300000},
map[string]int64{"A": 300000, "B": 200000})
doCheckWithExisting(t, map[string]fedapi.ClusterReplicaSetPreferences{
"*": {Weight: 1}},
50, []string{"A", "B"},
map[string]int64{"A": 10},
map[string]int64{"A": 25, "B": 25})
doCheckWithExisting(t, map[string]fedapi.ClusterReplicaSetPreferences{
"*": {Weight: 1}},
50, []string{"A", "B"},
map[string]int64{"A": 10, "B": 70},
// hash dependent
// TODO: Should be 10:40, update algorithm. Issue: #31816
map[string]int64{"A": 0, "B": 50})
doCheckWithExisting(t, map[string]fedapi.ClusterReplicaSetPreferences{
"*": {Weight: 1}},
1, []string{"A", "B"},
map[string]int64{"A": 30},
map[string]int64{"A": 1, "B": 0})
doCheckWithExisting(t, map[string]fedapi.ClusterReplicaSetPreferences{
"*": {Weight: 1}},
50, []string{"A", "B"},
map[string]int64{"A": 10, "B": 20},
map[string]int64{"A": 25, "B": 25})
}
func TestWithExistingAndCapacity(t *testing.T) {
// desired without capacity: map[string]int64{"A": 17, "B": 17, "C": 16})
doCheckWithExistingAndCapacity(t, true, map[string]fedapi.ClusterReplicaSetPreferences{
"*": {Weight: 1}},
50, []string{"A", "B", "C"},
map[string]int64{},
map[string]int64{"C": 10},
map[string]int64{"A": 20, "B": 20, "C": 10},
map[string]int64{"C": 7})
// desired B:50 C:0
doCheckWithExistingAndCapacity(t, true, map[string]fedapi.ClusterReplicaSetPreferences{
"A": {Weight: 10000},
"B": {Weight: 1}},
50, []string{"B", "C"},
map[string]int64{},
map[string]int64{"B": 10},
map[string]int64{"B": 10, "C": 0},
map[string]int64{"B": 40},
)
// desired A:20 B:40
doCheckWithExistingAndCapacity(t, true, map[string]fedapi.ClusterReplicaSetPreferences{
"A": {Weight: 1},
"B": {Weight: 2}},
60, []string{"A", "B", "C"},
map[string]int64{},
map[string]int64{"B": 10},
map[string]int64{"A": 50, "B": 10, "C": 0},
map[string]int64{"B": 30})
// map[string]int64{"A": 10, "B": 30, "C": 21, "D": 10})
doCheckWithExistingAndCapacity(t, true, map[string]fedapi.ClusterReplicaSetPreferences{
"A": {Weight: 10000, MaxReplicas: pint(10)},
"B": {Weight: 1},
"C": {Weight: 1, MaxReplicas: pint(21)},
"D": {Weight: 1, MaxReplicas: pint(10)}},
71, []string{"A", "B", "C", "D"},
map[string]int64{},
map[string]int64{"C": 10},
map[string]int64{"A": 10, "B": 41, "C": 10, "D": 10},
map[string]int64{"C": 11},
)
// desired A:20 B:20
doCheckWithExistingAndCapacity(t, false, map[string]fedapi.ClusterReplicaSetPreferences{
"A": {Weight: 1},
"B": {Weight: 1}},
60, []string{"A", "B", "C"},
map[string]int64{},
map[string]int64{"A": 10, "B": 10},
map[string]int64{"A": 10, "B": 10, "C": 0},
map[string]int64{"A": 20, "B": 20})
// desired A:10 B:50 although A:50 B:10 is fuly acceptable because rebalance = false
doCheckWithExistingAndCapacity(t, false, map[string]fedapi.ClusterReplicaSetPreferences{
"A": {Weight: 1},
"B": {Weight: 5}},
60, []string{"A", "B", "C"},
map[string]int64{},
map[string]int64{"B": 10},
map[string]int64{"A": 50, "B": 10, "C": 0},
map[string]int64{})
doCheckWithExistingAndCapacity(t, false, map[string]fedapi.ClusterReplicaSetPreferences{
"*": {MinReplicas: 20, Weight: 0}},
50, []string{"A", "B", "C"},
map[string]int64{},
map[string]int64{"B": 10},
map[string]int64{"A": 20, "B": 10, "C": 20},
map[string]int64{})
// Actually we would like to have extra 20 in B but 15 is also good.
doCheckWithExistingAndCapacity(t, true, map[string]fedapi.ClusterReplicaSetPreferences{
"*": {MinReplicas: 20, Weight: 1}},
60, []string{"A", "B"},
map[string]int64{},
map[string]int64{"B": 10},
map[string]int64{"A": 50, "B": 10},
map[string]int64{"B": 15})
}
func TestMin(t *testing.T) {
doCheck(t, map[string]fedapi.ClusterReplicaSetPreferences{
"*": {MinReplicas: 2, Weight: 0}},
50, []string{"A", "B", "C"},
map[string]int64{"A": 2, "B": 2, "C": 2})
doCheck(t, map[string]fedapi.ClusterReplicaSetPreferences{
"*": {MinReplicas: 20, Weight: 0}},
50, []string{"A", "B", "C"},
// hash dependant.
map[string]int64{"A": 10, "B": 20, "C": 20})
doCheck(t, map[string]fedapi.ClusterReplicaSetPreferences{
"*": {MinReplicas: 20, Weight: 0},
"A": {MinReplicas: 100, Weight: 1}},
50, []string{"A", "B", "C"},
map[string]int64{"A": 50, "B": 0, "C": 0})
doCheck(t, map[string]fedapi.ClusterReplicaSetPreferences{
"*": {MinReplicas: 10, Weight: 1, MaxReplicas: pint(12)}},
50, []string{"A", "B", "C"},
map[string]int64{"A": 12, "B": 12, "C": 12})
}
func TestMax(t *testing.T) {
doCheck(t, map[string]fedapi.ClusterReplicaSetPreferences{
"*": {Weight: 1, MaxReplicas: pint(2)}},
50, []string{"A", "B", "C"},
map[string]int64{"A": 2, "B": 2, "C": 2})
doCheck(t, map[string]fedapi.ClusterReplicaSetPreferences{
"*": {Weight: 0, MaxReplicas: pint(2)}},
50, []string{"A", "B", "C"},
map[string]int64{"A": 0, "B": 0, "C": 0})
}
func TestWeight(t *testing.T) {
doCheck(t, map[string]fedapi.ClusterReplicaSetPreferences{
"A": {Weight: 1},
"B": {Weight: 2}},
60, []string{"A", "B", "C"},
map[string]int64{"A": 20, "B": 40, "C": 0})
doCheck(t, map[string]fedapi.ClusterReplicaSetPreferences{
"A": {Weight: 10000},
"B": {Weight: 1}},
50, []string{"A", "B", "C"},
map[string]int64{"A": 50, "B": 0, "C": 0})
doCheck(t, map[string]fedapi.ClusterReplicaSetPreferences{
"A": {Weight: 10000},
"B": {Weight: 1}},
50, []string{"B", "C"},
map[string]int64{"B": 50, "C": 0})
doCheck(t, map[string]fedapi.ClusterReplicaSetPreferences{
"A": {Weight: 10000, MaxReplicas: pint(10)},
"B": {Weight: 1},
"C": {Weight: 1}},
50, []string{"A", "B", "C"},
map[string]int64{"A": 10, "B": 20, "C": 20})
doCheck(t, map[string]fedapi.ClusterReplicaSetPreferences{
"A": {Weight: 10000, MaxReplicas: pint(10)},
"B": {Weight: 1},
"C": {Weight: 1, MaxReplicas: pint(10)}},
50, []string{"A", "B", "C"},
map[string]int64{"A": 10, "B": 30, "C": 10})
doCheck(t, map[string]fedapi.ClusterReplicaSetPreferences{
"A": {Weight: 10000, MaxReplicas: pint(10)},
"B": {Weight: 1},
"C": {Weight: 1, MaxReplicas: pint(21)},
"D": {Weight: 1, MaxReplicas: pint(10)}},
71, []string{"A", "B", "C", "D"},
map[string]int64{"A": 10, "B": 30, "C": 21, "D": 10})
doCheck(t, map[string]fedapi.ClusterReplicaSetPreferences{
"A": {Weight: 10000, MaxReplicas: pint(10)},
"B": {Weight: 1},
"C": {Weight: 1, MaxReplicas: pint(21)},
"D": {Weight: 1, MaxReplicas: pint(10)},
"E": {Weight: 1}},
91, []string{"A", "B", "C", "D", "E"},
map[string]int64{"A": 10, "B": 25, "C": 21, "D": 10, "E": 25})
}

View file

@ -0,0 +1,37 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
"go_test",
"cgo_library",
)
go_library(
name = "go_default_library",
srcs = ["pod_helper.go"],
tags = ["automanaged"],
deps = [
"//federation/pkg/federation-controller/util:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/apis/meta/v1:go_default_library",
"//pkg/labels:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["pod_helper_test.go"],
library = "go_default_library",
tags = ["automanaged"],
deps = [
"//federation/pkg/federation-controller/util:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/apis/extensions/v1beta1:go_default_library",
"//pkg/apis/meta/v1:go_default_library",
"//vendor:github.com/stretchr/testify/assert",
],
)

View file

@ -0,0 +1,82 @@
/*
Copyright 2016 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 podanalyzer
import (
"fmt"
"time"
"k8s.io/kubernetes/federation/pkg/federation-controller/util"
api_v1 "k8s.io/kubernetes/pkg/api/v1"
metav1 "k8s.io/kubernetes/pkg/apis/meta/v1"
"k8s.io/kubernetes/pkg/labels"
)
type PodAnalysisResult struct {
// Total number of pods created.
Total int
// Number of pods that are running and ready.
RunningAndReady int
// Number of pods that have been in unschedulable state for UnshedulableThreshold seconds.
Unschedulable int
// TODO: Handle other scenarios like pod waiting too long for scheduler etc.
}
const (
// TODO: make it configurable
UnschedulableThreshold = 60 * time.Second
)
// A function that calculates how many pods from the list are in one of
// the meaningful (from the replica set perspective) states. This function is
// a temporary workaround against the current lack of ownerRef in pods.
func AnalysePods(selectorv1 *metav1.LabelSelector, allPods []util.FederatedObject, currentTime time.Time) (map[string]PodAnalysisResult, error) {
selector, err := metav1.LabelSelectorAsSelector(selectorv1)
if err != nil {
return nil, fmt.Errorf("invalid selector: %v", err)
}
result := make(map[string]PodAnalysisResult)
for _, fedObject := range allPods {
pod, isPod := fedObject.Object.(*api_v1.Pod)
if !isPod {
return nil, fmt.Errorf("invalid arg content - not a *pod")
}
if !selector.Empty() && selector.Matches(labels.Set(pod.Labels)) {
status := result[fedObject.ClusterName]
status.Total++
for _, condition := range pod.Status.Conditions {
if pod.Status.Phase == api_v1.PodRunning {
if condition.Type == api_v1.PodReady {
status.RunningAndReady++
}
} else {
if condition.Type == api_v1.PodScheduled &&
condition.Status == api_v1.ConditionFalse &&
condition.Reason == "Unschedulable" &&
condition.LastTransitionTime.Add(UnschedulableThreshold).Before(currentTime) {
status.Unschedulable++
}
}
}
result[fedObject.ClusterName] = status
}
}
return result, nil
}

View file

@ -0,0 +1,119 @@
/*
Copyright 2016 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 podanalyzer
import (
"testing"
"time"
"k8s.io/kubernetes/federation/pkg/federation-controller/util"
api_v1 "k8s.io/kubernetes/pkg/api/v1"
"k8s.io/kubernetes/pkg/apis/extensions/v1beta1"
metav1 "k8s.io/kubernetes/pkg/apis/meta/v1"
"github.com/stretchr/testify/assert"
)
func TestAnalyze(t *testing.T) {
now := time.Now()
replicaSet := newReplicaSet(map[string]string{"A": "B"})
replicaSet2 := newReplicaSet(map[string]string{"C": "D"})
podRunning := newPod("p1", replicaSet,
api_v1.PodStatus{
Phase: api_v1.PodRunning,
Conditions: []api_v1.PodCondition{
{
Type: api_v1.PodReady,
Status: api_v1.ConditionTrue,
},
},
})
podUnschedulable := newPod("pU", replicaSet,
api_v1.PodStatus{
Phase: api_v1.PodPending,
Conditions: []api_v1.PodCondition{
{
Type: api_v1.PodScheduled,
Status: api_v1.ConditionFalse,
Reason: "Unschedulable",
LastTransitionTime: metav1.Time{Time: now.Add(-10 * time.Minute)},
},
},
})
podOther := newPod("pO", replicaSet,
api_v1.PodStatus{
Phase: api_v1.PodPending,
Conditions: []api_v1.PodCondition{},
})
podOtherRS := newPod("pO", replicaSet2,
api_v1.PodStatus{
Phase: api_v1.PodPending,
Conditions: []api_v1.PodCondition{},
})
federatedObjects := []util.FederatedObject{
{ClusterName: "c1", Object: podRunning},
{ClusterName: "c1", Object: podRunning},
{ClusterName: "c1", Object: podRunning},
{ClusterName: "c1", Object: podUnschedulable},
{ClusterName: "c1", Object: podUnschedulable},
{ClusterName: "c2", Object: podOther},
{ClusterName: "c2", Object: podOtherRS},
}
raport, err := AnalysePods(replicaSet.Spec.Selector, federatedObjects, now)
assert.NoError(t, err)
assert.Equal(t, 2, len(raport))
c1Raport := raport["c1"]
c2Raport := raport["c2"]
assert.Equal(t, PodAnalysisResult{
Total: 5,
RunningAndReady: 3,
Unschedulable: 2,
}, c1Raport)
assert.Equal(t, PodAnalysisResult{
Total: 1,
RunningAndReady: 0,
Unschedulable: 0,
}, c2Raport)
}
func newReplicaSet(selectorMap map[string]string) *v1beta1.ReplicaSet {
replicas := int32(3)
rs := &v1beta1.ReplicaSet{
ObjectMeta: api_v1.ObjectMeta{
Name: "foobar",
Namespace: "default",
},
Spec: v1beta1.ReplicaSetSpec{
Replicas: &replicas,
Selector: &metav1.LabelSelector{MatchLabels: selectorMap},
},
}
return rs
}
func newPod(name string, rs *v1beta1.ReplicaSet, status api_v1.PodStatus) *api_v1.Pod {
return &api_v1.Pod{
ObjectMeta: api_v1.ObjectMeta{
Name: name,
Namespace: rs.Namespace,
Labels: rs.Spec.Selector.MatchLabels,
},
Status: status,
}
}

View file

@ -0,0 +1,32 @@
/*
Copyright 2016 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 util
import (
"reflect"
api_v1 "k8s.io/kubernetes/pkg/api/v1"
)
// Checks if cluster-independent, user provided data in two given Secrets are eqaul. If in
// the future the Secret structure is expanded then any field that is not populated.
// by the api server should be included here.
func SecretEquivalent(s1, s2 api_v1.Secret) bool {
return ObjectMetaEquivalent(s1.ObjectMeta, s2.ObjectMeta) &&
reflect.DeepEqual(s1.Data, s2.Data) &&
reflect.DeepEqual(s1.Type, s2.Type)
}

View file

@ -0,0 +1,28 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
"go_test",
"cgo_library",
)
go_library(
name = "go_default_library",
srcs = ["test_helper.go"],
tags = ["automanaged"],
deps = [
"//federation/apis/federation/v1beta1:go_default_library",
"//federation/pkg/federation-controller/util:go_default_library",
"//pkg/api:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/client/testing/core:go_default_library",
"//pkg/runtime:go_default_library",
"//pkg/util/wait:go_default_library",
"//pkg/watch:go_default_library",
"//vendor:github.com/golang/glog",
],
)

View file

@ -0,0 +1,324 @@
/*
Copyright 2016 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 testutil
import (
"fmt"
"os"
"reflect"
"runtime/pprof"
"sync"
"time"
federationapi "k8s.io/kubernetes/federation/apis/federation/v1beta1"
"k8s.io/kubernetes/federation/pkg/federation-controller/util"
"k8s.io/kubernetes/pkg/api"
apiv1 "k8s.io/kubernetes/pkg/api/v1"
"k8s.io/kubernetes/pkg/client/testing/core"
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/util/wait"
"k8s.io/kubernetes/pkg/watch"
"github.com/golang/glog"
)
// A structure that distributes eventes to multiple watchers.
type WatcherDispatcher struct {
sync.Mutex
watchers []*watch.RaceFreeFakeWatcher
eventsSoFar []*watch.Event
orderExecution chan func()
stopChan chan struct{}
}
func (wd *WatcherDispatcher) register(watcher *watch.RaceFreeFakeWatcher) {
wd.Lock()
defer wd.Unlock()
wd.watchers = append(wd.watchers, watcher)
for _, event := range wd.eventsSoFar {
watcher.Action(event.Type, event.Object)
}
}
func (wd *WatcherDispatcher) Stop() {
wd.Lock()
defer wd.Unlock()
close(wd.stopChan)
for _, watcher := range wd.watchers {
watcher.Stop()
}
}
func copy(obj runtime.Object) runtime.Object {
objCopy, err := api.Scheme.DeepCopy(obj)
if err != nil {
panic(err)
}
return objCopy.(runtime.Object)
}
// Add sends an add event.
func (wd *WatcherDispatcher) Add(obj runtime.Object) {
wd.Lock()
defer wd.Unlock()
wd.eventsSoFar = append(wd.eventsSoFar, &watch.Event{Type: watch.Added, Object: copy(obj)})
for _, watcher := range wd.watchers {
if !watcher.IsStopped() {
watcher.Add(copy(obj))
}
}
}
// Modify sends a modify event.
func (wd *WatcherDispatcher) Modify(obj runtime.Object) {
wd.Lock()
defer wd.Unlock()
glog.V(4).Infof("->WatcherDispatcher.Modify(%v)", obj)
wd.eventsSoFar = append(wd.eventsSoFar, &watch.Event{Type: watch.Modified, Object: copy(obj)})
for i, watcher := range wd.watchers {
if !watcher.IsStopped() {
glog.V(4).Infof("->Watcher(%d).Modify(%v)", i, obj)
watcher.Modify(copy(obj))
} else {
glog.V(4).Infof("->Watcher(%d) is stopped. Not calling Modify(%v)", i, obj)
}
}
}
// Delete sends a delete event.
func (wd *WatcherDispatcher) Delete(lastValue runtime.Object) {
wd.Lock()
defer wd.Unlock()
wd.eventsSoFar = append(wd.eventsSoFar, &watch.Event{Type: watch.Deleted, Object: copy(lastValue)})
for _, watcher := range wd.watchers {
if !watcher.IsStopped() {
watcher.Delete(copy(lastValue))
}
}
}
// Error sends an Error event.
func (wd *WatcherDispatcher) Error(errValue runtime.Object) {
wd.Lock()
defer wd.Unlock()
wd.eventsSoFar = append(wd.eventsSoFar, &watch.Event{Type: watch.Error, Object: copy(errValue)})
for _, watcher := range wd.watchers {
if !watcher.IsStopped() {
watcher.Error(copy(errValue))
}
}
}
// Action sends an event of the requested type, for table-based testing.
func (wd *WatcherDispatcher) Action(action watch.EventType, obj runtime.Object) {
wd.Lock()
defer wd.Unlock()
wd.eventsSoFar = append(wd.eventsSoFar, &watch.Event{Type: action, Object: copy(obj)})
for _, watcher := range wd.watchers {
if !watcher.IsStopped() {
watcher.Action(action, copy(obj))
}
}
}
// RegisterFakeWatch adds a new fake watcher for the specified resource in the given fake client.
// All subsequent requests for a watch on the client will result in returning this fake watcher.
func RegisterFakeWatch(resource string, client *core.Fake) *WatcherDispatcher {
dispatcher := &WatcherDispatcher{
watchers: make([]*watch.RaceFreeFakeWatcher, 0),
eventsSoFar: make([]*watch.Event, 0),
orderExecution: make(chan func()),
stopChan: make(chan struct{}),
}
go func() {
for {
select {
case fun := <-dispatcher.orderExecution:
fun()
case <-dispatcher.stopChan:
return
}
}
}()
client.AddWatchReactor(resource, func(action core.Action) (bool, watch.Interface, error) {
watcher := watch.NewRaceFreeFake()
dispatcher.register(watcher)
return true, watcher, nil
})
return dispatcher
}
// RegisterFakeList registers a list response for the specified resource inside the given fake client.
// The passed value will be returned with every list call.
func RegisterFakeList(resource string, client *core.Fake, obj runtime.Object) {
client.AddReactor("list", resource, func(action core.Action) (bool, runtime.Object, error) {
return true, obj, nil
})
}
// RegisterFakeCopyOnCreate registers a reactor in the given fake client that passes
// all created objects to the given watcher and also copies them to a channel for
// in-test inspection.
func RegisterFakeCopyOnCreate(resource string, client *core.Fake, watcher *WatcherDispatcher) chan runtime.Object {
objChan := make(chan runtime.Object, 100)
client.AddReactor("create", resource, func(action core.Action) (bool, runtime.Object, error) {
createAction := action.(core.CreateAction)
originalObj := createAction.GetObject()
// Create a copy of the object here to prevent data races while reading the object in go routine.
obj := copy(originalObj)
watcher.orderExecution <- func() {
glog.V(4).Infof("Object created. Writing to channel: %v", obj)
watcher.Add(obj)
objChan <- obj
}
return true, originalObj, nil
})
return objChan
}
// RegisterFakeCopyOnUpdate registers a reactor in the given fake client that passes
// all updated objects to the given watcher and also copies them to a channel for
// in-test inspection.
func RegisterFakeCopyOnUpdate(resource string, client *core.Fake, watcher *WatcherDispatcher) chan runtime.Object {
objChan := make(chan runtime.Object, 100)
client.AddReactor("update", resource, func(action core.Action) (bool, runtime.Object, error) {
updateAction := action.(core.UpdateAction)
originalObj := updateAction.GetObject()
// Create a copy of the object here to prevent data races while reading the object in go routine.
obj := copy(originalObj)
watcher.orderExecution <- func() {
glog.V(4).Infof("Object updated. Writing to channel: %v", obj)
watcher.Modify(obj)
objChan <- obj
}
return true, originalObj, nil
})
return objChan
}
// GetObjectFromChan tries to get an api object from the given channel
// within a reasonable time.
func GetObjectFromChan(c chan runtime.Object) runtime.Object {
select {
case obj := <-c:
return obj
case <-time.After(wait.ForeverTestTimeout):
pprof.Lookup("goroutine").WriteTo(os.Stderr, 1)
return nil
}
}
type CheckingFunction func(runtime.Object) error
// CheckObjectFromChan tries to get an object matching the given check function
// within a reasonable time.
func CheckObjectFromChan(c chan runtime.Object, checkFunction CheckingFunction) error {
delay := 20 * time.Second
var lastError error
for {
select {
case obj := <-c:
if lastError = checkFunction(obj); lastError == nil {
return nil
}
glog.Infof("Check function failed with %v", lastError)
delay = 5 * time.Second
case <-time.After(delay):
pprof.Lookup("goroutine").WriteTo(os.Stderr, 1)
if lastError == nil {
return fmt.Errorf("Failed to get an object from channel")
} else {
return lastError
}
}
}
}
// CompareObjectMeta returns an error when the given objects are not equivalent.
func CompareObjectMeta(a, b apiv1.ObjectMeta) error {
if a.Namespace != b.Namespace {
return fmt.Errorf("Different namespace expected:%s observed:%s", a.Namespace, b.Namespace)
}
if a.Name != b.Name {
return fmt.Errorf("Different name expected:%s observed:%s", a.Namespace, b.Namespace)
}
if !reflect.DeepEqual(a.Labels, b.Labels) && (len(a.Labels) != 0 || len(b.Labels) != 0) {
return fmt.Errorf("Labels are different expected:%v observerd:%v", a.Labels, b.Labels)
}
if !reflect.DeepEqual(a.Annotations, b.Annotations) && (len(a.Annotations) != 0 || len(b.Annotations) != 0) {
return fmt.Errorf("Annotations are different expected:%v observerd:%v", a.Annotations, b.Annotations)
}
return nil
}
func ToFederatedInformerForTestOnly(informer util.FederatedInformer) util.FederatedInformerForTestOnly {
inter := informer.(interface{})
return inter.(util.FederatedInformerForTestOnly)
}
// NewCluster builds a new cluster object.
func NewCluster(name string, readyStatus apiv1.ConditionStatus) *federationapi.Cluster {
return &federationapi.Cluster{
ObjectMeta: apiv1.ObjectMeta{
Name: name,
Annotations: map[string]string{},
},
Status: federationapi.ClusterStatus{
Conditions: []federationapi.ClusterCondition{
{Type: federationapi.ClusterReady, Status: readyStatus},
},
},
}
}
// Ensure a key is in the store before returning (or timeout w/ error)
func WaitForStoreUpdate(store util.FederatedReadOnlyStore, clusterName, key string, timeout time.Duration) error {
retryInterval := 100 * time.Millisecond
err := wait.PollImmediate(retryInterval, timeout, func() (bool, error) {
_, found, err := store.GetByKey(clusterName, key)
return found, err
})
return err
}
// Ensure a key is in the store before returning (or timeout w/ error)
func WaitForStoreUpdateChecking(store util.FederatedReadOnlyStore, clusterName, key string, timeout time.Duration,
checkFunction CheckingFunction) error {
retryInterval := 500 * time.Millisecond
var lastError error
err := wait.PollImmediate(retryInterval, timeout, func() (bool, error) {
item, found, err := store.GetByKey(clusterName, key)
if err != nil || !found {
return found, err
}
runtimeObj := item.(runtime.Object)
lastError = checkFunction(runtimeObj)
glog.V(2).Infof("Check function failed for %s %v %v", key, runtimeObj, lastError)
return lastError == nil, nil
})
return err
}
func MetaAndSpecCheckingFunction(expected runtime.Object) CheckingFunction {
return func(obj runtime.Object) error {
if util.ObjectMetaAndSpecEquivalent(obj, expected) {
return nil
}
return fmt.Errorf("Object different expected=%#v received=%#v", expected, obj)
}
}

View file

@ -0,0 +1,40 @@
/*
Copyright 2016 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 util
import (
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/v1"
)
// TODO: remove this when Reflector takes an interface rather than a particular ListOptions as input parameter.
func VersionizeV1ListOptions(in api.ListOptions) (out v1.ListOptions) {
if in.LabelSelector != nil {
out.LabelSelector = in.LabelSelector.String()
} else {
out.LabelSelector = ""
}
if in.FieldSelector != nil {
out.FieldSelector = in.FieldSelector.String()
} else {
out.FieldSelector = ""
}
out.Watch = in.Watch
out.ResourceVersion = in.ResourceVersion
out.TimeoutSeconds = in.TimeoutSeconds
return out
}