Replace godep with dep

This commit is contained in:
Manuel de Brito Fontes 2017-10-06 17:26:14 -03:00
parent 1e7489927c
commit bf5616c65b
14883 changed files with 3937406 additions and 361781 deletions

View file

@ -0,0 +1,98 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
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",
],
deps = [
"//federation/apis/federation/v1beta1:go_default_library",
"//federation/client/clientset_generated/federation_clientset:go_default_library",
"//pkg/api:go_default_library",
"//pkg/client/clientset_generated/internalclientset:go_default_library",
"//pkg/controller/deployment/util:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/api/extensions/v1beta1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/watch:go_default_library",
"//vendor/k8s.io/client-go/kubernetes:go_default_library",
"//vendor/k8s.io/client-go/rest:go_default_library",
"//vendor/k8s.io/client-go/tools/cache:go_default_library",
"//vendor/k8s.io/client-go/tools/clientcmd:go_default_library",
"//vendor/k8s.io/client-go/tools/clientcmd/api:go_default_library",
"//vendor/k8s.io/client-go/tools/record:go_default_library",
"//vendor/k8s.io/client-go/util/flowcontrol:go_default_library",
],
)
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",
deps = [
"//federation/apis/federation/v1beta1:go_default_library",
"//federation/client/clientset_generated/federation_clientset/fake:go_default_library",
"//pkg/controller/deployment/util:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/api/extensions/v1beta1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/watch:go_default_library",
"//vendor/k8s.io/client-go/kubernetes:go_default_library",
"//vendor/k8s.io/client-go/kubernetes/fake:go_default_library",
"//vendor/k8s.io/client-go/testing:go_default_library",
"//vendor/k8s.io/client-go/tools/cache:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//federation/pkg/federation-controller/util/clusterselector:all-srcs",
"//federation/pkg/federation-controller/util/deletionhelper:all-srcs",
"//federation/pkg/federation-controller/util/eventsink:all-srcs",
"//federation/pkg/federation-controller/util/finalizers:all-srcs",
"//federation/pkg/federation-controller/util/hpa:all-srcs",
"//federation/pkg/federation-controller/util/planner:all-srcs",
"//federation/pkg/federation-controller/util/podanalyzer:all-srcs",
"//federation/pkg/federation-controller/util/replicapreferences:all-srcs",
"//federation/pkg/federation-controller/util/test:all-srcs",
],
tags = ["automanaged"],
)

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/client-go/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"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
utilnet "k8s.io/apimachinery/pkg/util/net"
"k8s.io/apimachinery/pkg/util/wait"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
federation_v1beta1 "k8s.io/kubernetes/federation/apis/federation/v1beta1"
"k8s.io/kubernetes/pkg/api"
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
)
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 {
if c.Spec.SecretRef.Name == "" {
return nil, fmt.Errorf("found secretRef but no secret name for cluster %s", c.Name)
}
secret, err := getSecret(c.Spec.SecretRef.Name)
if err != nil {
return nil, err
}
// Pre-1.7, the secret contained a serialized kubeconfig which contained appropriate credentials.
// Post-1.7, the secret contains credentials for a service account.
// Check for the service account credentials, and use them if they exist; if not, use the
// serialized kubeconfig.
token, tokenFound := secret.Data["token"]
ca, caFound := secret.Data["ca.crt"]
if tokenFound != caFound {
return nil, fmt.Errorf("secret should have values for either both 'ca.crt' and 'token' in its Data, or neither: %v", secret)
} else if tokenFound && caFound {
clusterConfig, err = clientcmd.BuildConfigFromFlags(serverAddress, "")
clusterConfig.CAData = ca
clusterConfig.BearerToken = string(token)
} else {
kubeconfigGetter := KubeconfigGetterForSecret(secret)
clusterConfig, err = clientcmd.BuildConfigFromKubeconfigGetter(serverAddress, kubeconfigGetter)
}
}
if err != nil {
return nil, err
}
clusterConfig.QPS = KubeAPIQPS
clusterConfig.Burst = KubeAPIBurst
}
return clusterConfig, nil
}
// getSecret gets a secret from the cluster.
func getSecret(secretName string) (*api.Secret, error) {
// 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 config: %s", err)
}
client, err := clientset.NewForConfig(cc)
if err != nil {
return nil, fmt.Errorf("error in creating in-cluster client: %s", err)
}
var secret *api.Secret
err = wait.PollImmediate(1*time.Second, getSecretTimeout, func() (bool, error) {
secret, err = client.Core().Secrets(namespace).Get(secretName, metav1.GetOptions{})
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)
}
return secret, nil
}
// KubeconfigGetterForSecret gets the kubeconfig from the given secret.
// 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
// set up service accounts and mount files with secret tokens.
var KubeconfigGetterForSecret = func(secret *api.Secret) clientcmd.KubeconfigGetter {
return func() (*clientcmdapi.Config, error) {
data, ok := secret.Data[KubeconfigSecretDataKey]
if !ok {
return nil, fmt.Errorf("secret does not have data with key %s", KubeconfigSecretDataKey)
}
return clientcmd.Load(data)
}
}

View file

@ -0,0 +1,40 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_test(
name = "go_default_test",
srcs = ["clusterselector_test.go"],
library = ":go_default_library",
deps = [
"//federation/apis/federation/v1beta1:go_default_library",
"//vendor/github.com/stretchr/testify/require:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = ["clusterselector.go"],
deps = [
"//federation/apis/federation/v1beta1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/labels:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/selection:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View file

@ -0,0 +1,86 @@
/*
Copyright 2017 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 clusterselector
import (
"encoding/json"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
federation_v1beta1 "k8s.io/kubernetes/federation/apis/federation/v1beta1"
)
// Parses the cluster selector annotation to find out if the object with that annotation should be forwarded to a cluster with the given clusterLabels.
func SendToCluster(clusterLabels map[string]string, annotations map[string]string) (bool, error) {
// Check if a ClusterSelector annotation exists and send to all clusters when it does not exist
val, ok := annotations[federation_v1beta1.FederationClusterSelectorAnnotation]
if !ok {
return true, nil
}
selector, err := getSelector(val)
if err != nil {
return false, err
}
return selector.Matches(labels.Set(clusterLabels)), nil
}
func getSelector(annotation string) (labels.Selector, error) {
selector := labels.NewSelector()
requirements := make([]federation_v1beta1.ClusterSelectorRequirement, 0)
err := json.Unmarshal([]byte(annotation), &requirements)
if err != nil {
return nil, err
}
for _, requirement := range requirements {
r, err := labels.NewRequirement(requirement.Key, ConvertOperator(requirement.Operator), requirement.Values)
if err != nil {
// Stop processing and assume failure since we have no way of knowing the end users intent for this or any other clusters.
return nil, err
}
selector = selector.Add(*r)
}
return selector, nil
}
// ConvertOperator converts a string operator into selection.Operator type
func ConvertOperator(source string) selection.Operator {
var op selection.Operator
switch source {
case "!", "DoesNotExist":
op = selection.DoesNotExist
case "=":
op = selection.Equals
case "==":
op = selection.DoubleEquals
case "in", "In":
op = selection.In
case "!=":
op = selection.NotEquals
case "notin", "NotIn":
op = selection.NotIn
case "exists", "Exists":
op = selection.Exists
case "gt", "Gt", ">":
op = selection.GreaterThan
case "lt", "Lt", "<":
op = selection.LessThan
}
return op
}

View file

@ -0,0 +1,98 @@
/*
Copyright 2017 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 clusterselector
import (
"testing"
"github.com/stretchr/testify/require"
federationapi "k8s.io/kubernetes/federation/apis/federation/v1beta1"
)
func TestSendToCluster(t *testing.T) {
clusterLabels := map[string]string{
"location": "europe",
"environment": "prod",
"version": "15",
}
testCases := map[string]struct {
objectAnnotations map[string]string
expectedResult bool
expectedErr bool
}{
"match with single annotation": {
objectAnnotations: map[string]string{
federationapi.FederationClusterSelectorAnnotation: `[{"key": "location", "operator": "in", "values": ["europe"]}]`,
},
expectedResult: true,
},
"match on multiple annotations": {
objectAnnotations: map[string]string{
federationapi.FederationClusterSelectorAnnotation: `[{"key": "location", "operator": "in", "values": ["europe"]}, {"key": "environment", "operator": "==", "values": ["prod"]}]`,
},
expectedResult: true,
},
"mismatch on one annotation": {
objectAnnotations: map[string]string{
federationapi.FederationClusterSelectorAnnotation: `[{"key": "location", "operator": "in", "values": ["europe"]}, {"key": "environment", "operator": "==", "values": ["test"]}]`,
},
expectedResult: false,
},
"match on not equal annotation": {
objectAnnotations: map[string]string{
federationapi.FederationClusterSelectorAnnotation: `[{"key": "location", "operator": "!=", "values": ["usa"]}, {"key": "environment", "operator": "in", "values": ["prod"]}]`,
},
expectedResult: true,
},
"match on greater than annotation": {
objectAnnotations: map[string]string{
federationapi.FederationClusterSelectorAnnotation: `[{"key": "version", "operator": ">", "values": ["14"]}]`,
},
expectedResult: true,
},
"mismatch on greater than annotation": {
objectAnnotations: map[string]string{
federationapi.FederationClusterSelectorAnnotation: `[{"key": "version", "operator": ">", "values": ["15"]}]`,
},
expectedResult: false,
},
"unable to parse annotation": {
objectAnnotations: map[string]string{
federationapi.FederationClusterSelectorAnnotation: `[{"not able to parse",}]`,
},
expectedResult: false,
expectedErr: true,
},
}
for testName, testCase := range testCases {
t.Run(testName, func(t *testing.T) {
result, err := SendToCluster(clusterLabels, testCase.objectAnnotations)
if testCase.expectedErr {
require.Error(t, err, "An error was expected")
} else {
require.NoError(t, err, "An error was not expected")
}
require.Equal(t, testCase.expectedResult, result, "Unexpected response from SendToCluster")
})
}
}

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/api/core/v1"
)
// Checks if cluster-independent, user provided data in two given ConfigMaps are equal. 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,32 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["deletion_helper.go"],
deps = [
"//federation/pkg/federation-controller/util:go_default_library",
"//federation/pkg/federation-controller/util/finalizers:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View file

@ -0,0 +1,210 @@
/*
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"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/kubernetes/federation/pkg/federation-controller/util"
finalizersutil "k8s.io/kubernetes/federation/pkg/federation-controller/util/finalizers"
"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 UpdateObjFunc func(runtime.Object) (runtime.Object, error)
type ObjNameFunc func(runtime.Object) string
type DeletionHelper struct {
updateObjFunc UpdateObjFunc
objNameFunc ObjNameFunc
informer util.FederatedInformer
updater util.FederatedUpdater
}
func NewDeletionHelper(
updateObjFunc UpdateObjFunc, objNameFunc ObjNameFunc,
informer util.FederatedInformer, updater util.FederatedUpdater) *DeletionHelper {
return &DeletionHelper{
updateObjFunc: updateObjFunc,
objNameFunc: objNameFunc,
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) {
finalizers := sets.String{}
hasFinalizer, err := finalizersutil.HasFinalizer(obj, FinalizerDeleteFromUnderlyingClusters)
if err != nil {
return obj, err
}
if !hasFinalizer {
finalizers.Insert(FinalizerDeleteFromUnderlyingClusters)
}
hasFinalizer, err = finalizersutil.HasFinalizer(obj, metav1.FinalizerOrphanDependents)
if err != nil {
return obj, err
}
if !hasFinalizer {
finalizers.Insert(metav1.FinalizerOrphanDependents)
}
if finalizers.Len() != 0 {
glog.V(2).Infof("Adding finalizers %v to %s", finalizers.List(), dh.objNameFunc(obj))
return dh.addFinalizers(obj, finalizers)
}
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)
hasFinalizer, err := finalizersutil.HasFinalizer(obj, FinalizerDeleteFromUnderlyingClusters)
if err != nil {
return obj, err
}
if !hasFinalizer {
glog.V(2).Infof("obj does not have %s finalizer. Nothing to do", FinalizerDeleteFromUnderlyingClusters)
return obj, nil
}
hasOrphanFinalizer, err := finalizersutil.HasFinalizer(obj, metav1.FinalizerOrphanDependents)
if err != nil {
return obj, err
}
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.
finalizers := sets.NewString(FinalizerDeleteFromUnderlyingClusters, metav1.FinalizerOrphanDependents)
return dh.removeFinalizers(obj, finalizers)
}
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),
Key: objName,
})
}
err = dh.updater.Update(operations)
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.removeFinalizers(obj, sets.NewString(FinalizerDeleteFromUnderlyingClusters))
}
// Adds the given finalizers to the given objects ObjectMeta.
func (dh *DeletionHelper) addFinalizers(obj runtime.Object, finalizers sets.String) (runtime.Object, error) {
isUpdated, err := finalizersutil.AddFinalizers(obj, finalizers)
if err != nil || !isUpdated {
return obj, err
}
// Send the update to apiserver.
updatedObj, err := dh.updateObjFunc(obj)
if err != nil {
return nil, fmt.Errorf("failed to add finalizers %v to object %s: %v", finalizers, dh.objNameFunc(obj), err)
}
return updatedObj, nil
}
// Removes the given finalizers from the given objects ObjectMeta.
func (dh *DeletionHelper) removeFinalizers(obj runtime.Object, finalizers sets.String) (runtime.Object, error) {
isUpdated, err := finalizersutil.RemoveFinalizers(obj, finalizers)
if err != nil || !isUpdated {
return obj, err
}
// Send the update to apiserver.
updatedObj, err := dh.updateObjFunc(obj)
if err != nil {
return nil, fmt.Errorf("failed to remove finalizers %v from object %s: %v", finalizers, dh.objNameFunc(obj), err)
}
return updatedObj, nil
}

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"
extensions_v1 "k8s.io/api/extensions/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
deputils "k8s.io/kubernetes/pkg/controller/deployment/util"
)
// Checks if cluster-independent, user provided data in two given Deployment are equal.
// 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 metav1.ObjectMeta) metav1.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"
extensionsv1 "k8s.io/api/extensions/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
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: metav1.ObjectMeta{
Name: "wrr",
Namespace: metav1.NamespaceDefault,
SelfLink: "/api/v1/namespaces/default/deployments/name123",
},
Spec: extensionsv1.DeploymentSpec{
Replicas: &replicas,
},
}
}

View file

@ -0,0 +1,50 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = ["eventsink.go"],
deps = [
"//federation/client/clientset_generated/federation_clientset:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/conversion:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
"//vendor/k8s.io/client-go/tools/record:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["eventsink_test.go"],
library = ":go_default_library",
deps = [
"//federation/client/clientset_generated/federation_clientset/fake:go_default_library",
"//federation/pkg/federation-controller/util/test:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/client-go/testing:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View file

@ -0,0 +1,111 @@
/*
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 (
"reflect"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/conversion"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
fedclientset "k8s.io/kubernetes/federation/client/clientset_generated/federation_clientset"
)
// Implements k8s.io/client-go/tools/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,
}
}
// TODO this is uses a reflection conversion path and is very expensive. federation should update to use client-go
var scheme = runtime.NewScheme()
func init() {
// register client-go's and kube's Event type under two different GroupVersions
// TODO: switch to client-go client for events
scheme.AddKnownTypes(v1.SchemeGroupVersion, &v1.Event{})
scheme.AddKnownTypes(schema.GroupVersion{Group: "fake-kube-" + v1.SchemeGroupVersion.Group, Version: v1.SchemeGroupVersion.Version}, &v1.Event{})
if err := scheme.AddConversionFuncs(
metav1.Convert_unversioned_Time_To_unversioned_Time,
); err != nil {
panic(err)
}
if err := scheme.AddGeneratedDeepCopyFuncs(
conversion.GeneratedDeepCopyFunc{
Fn: func(in, out interface{}, c *conversion.Cloner) error {
in.(*metav1.Time).DeepCopyInto(out.(*metav1.Time))
return nil
},
InType: reflect.TypeOf(&metav1.Time{}),
},
); err != nil {
panic(err)
}
}
func (fes *FederatedEventSink) Create(event *v1.Event) (*v1.Event, error) {
ret, err := fes.clientset.Core().Events(event.Namespace).Create(event)
if err != nil {
return nil, err
}
retEvent := &v1.Event{}
if err := scheme.Convert(ret, retEvent, nil); err != nil {
return nil, err
}
return retEvent, nil
}
func (fes *FederatedEventSink) Update(event *v1.Event) (*v1.Event, error) {
ret, err := fes.clientset.Core().Events(event.Namespace).Update(event)
if err != nil {
return nil, err
}
retEvent := &v1.Event{}
if err := scheme.Convert(ret, retEvent, nil); err != nil {
return nil, err
}
return retEvent, nil
}
func (fes *FederatedEventSink) Patch(event *v1.Event, data []byte) (*v1.Event, error) {
ret, err := fes.clientset.Core().Events(event.Namespace).Patch(event.Name, types.StrategicMergePatchType, data)
if err != nil {
return nil, err
}
retEvent := &v1.Event{}
if err := scheme.Convert(ret, retEvent, nil); err != nil {
return nil, err
}
return retEvent, nil
}

View file

@ -0,0 +1,71 @@
/*
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"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
core "k8s.io/client-go/testing"
fakefedclientset "k8s.io/kubernetes/federation/client/clientset_generated/federation_clientset/fake"
. "k8s.io/kubernetes/federation/pkg/federation-controller/util/test"
"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 := v1.Event{
ObjectMeta: metav1.ObjectMeta{
Name: "bzium",
Namespace: "ns",
},
}
sink := NewFederatedEventSink(fakeFederationClient)
eventUpdated, err := sink.Create(&event)
assert.NoError(t, err)
eventV1 := GetObjectFromChan(createdChan).(*v1.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).(*v1.Event)
assert.NotNil(t, eventV1)
// Just some simple sanity checks.
assert.Equal(t, event.Name, eventV1.Name)
assert.Equal(t, event.Name, eventUpdated.Name)
}

View file

@ -0,0 +1,524 @@
/*
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"
"sync"
"time"
apiv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
pkgruntime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/watch"
kubeclientset "k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
federationapi "k8s.io/kubernetes/federation/apis/federation/v1beta1"
federationclientset "k8s.io/kubernetes/federation/client/clientset_generated/federation_clientset"
"github.com/golang/glog"
)
const (
clusterSyncPeriod = 10 * time.Minute
userAgentName = "federation-controller"
)
// An object with an origin information.
type FederatedObject struct {
Object interface{}
ClusterName string
}
// FederatedReadOnlyStore is an overlay over multiple stores created in federated clusters.
type FederatedReadOnlyStore interface {
// Returns all items in the store.
List() ([]FederatedObject, error)
// Returns all items from a cluster.
ListFromCluster(clusterName string) ([]interface{}, error)
// GetKeyFor returns the key under which the item would be put in the store.
GetKeyFor(item interface{}) string
// GetByKey returns the item stored under the given key in the specified cluster (if exist).
GetByKey(clusterName string, key string) (interface{}, bool, error)
// Returns the items stored under the given key in all clusters.
GetFromAllClusters(key string) ([]FederatedObject, error)
// Checks whether stores for all clusters form the lists (and only these) are there and
// are synced. This is only a basic check whether the data inside of the store is usable.
// It is not a full synchronization/locking mechanism it only tries to ensure that out-of-sync
// issues occur less often. All users of the interface should assume
// that there may be significant delays in content updates of all kinds and write their
// code that it doesn't break if something is slightly out-of-sync.
ClustersSynced(clusters []*federationapi.Cluster) bool
}
// An interface to access federation members and clients.
type FederationView interface {
// GetClientsetForCluster returns a clientset for the cluster, if present.
GetClientsetForCluster(clusterName string) (kubeclientset.Interface, error)
// GetUnreadyClusters returns a list of all clusters that are not ready yet.
GetUnreadyClusters() ([]*federationapi.Cluster, error)
// GetReadyClusters returns all clusters for which the sub-informers are run.
GetReadyClusters() ([]*federationapi.Cluster, error)
// GetReadyCluster returns the cluster with the given name, if found.
GetReadyCluster(name string) (*federationapi.Cluster, bool, error)
// ClustersSynced returns true if the view is synced (for the first time).
ClustersSynced() bool
}
// A structure that combines an informer running against federated api server and listening for cluster updates
// with multiple Kubernetes API informers (called target informers) running against federation members. Whenever a new
// cluster is added to the federation an informer is created for it using TargetInformerFactory. Informers are stopped
// when a cluster is either put offline of deleted. It is assumed that some controller keeps an eye on the cluster list
// and thus the clusters in ETCD are up to date.
type FederatedInformer interface {
FederationView
// Returns a store created over all stores from target informers.
GetTargetStore() FederatedReadOnlyStore
// Starts all the processes.
Start()
// Stops all the processes inside the informer.
Stop()
}
// FederatedInformer with extra method for setting fake clients.
type FederatedInformerForTestOnly interface {
FederatedInformer
SetClientFactory(func(*federationapi.Cluster) (kubeclientset.Interface, error))
}
// A function that should be used to create an informer on the target object. Store should use
// cache.DeletionHandlingMetaNamespaceKeyFunc as a keying function.
type TargetInformerFactory func(*federationapi.Cluster, kubeclientset.Interface) (cache.Store, cache.Controller)
// A structure with cluster lifecycle handler functions. Cluster is available (and ClusterAvailable is fired)
// when it is created in federated etcd and ready. Cluster becomes unavailable (and ClusterUnavailable is fired)
// when it is either deleted or becomes not ready. When cluster spec (IP)is modified both ClusterAvailable
// and ClusterUnavailable are fired.
type ClusterLifecycleHandlerFuncs struct {
// Fired when the cluster becomes available.
ClusterAvailable func(*federationapi.Cluster)
// Fired when the cluster becomes unavailable. The second arg contains data that was present
// in the cluster before deletion.
ClusterUnavailable func(*federationapi.Cluster, []interface{})
}
// Builds a FederatedInformer for the given federation client and factory.
func NewFederatedInformer(
federationClient federationclientset.Interface,
targetInformerFactory TargetInformerFactory,
clusterLifecycle *ClusterLifecycleHandlerFuncs) FederatedInformer {
federatedInformer := &federatedInformerImpl{
targetInformerFactory: targetInformerFactory,
clientFactory: func(cluster *federationapi.Cluster) (kubeclientset.Interface, error) {
clusterConfig, err := BuildClusterConfig(cluster)
if err == nil && clusterConfig != nil {
clientset := kubeclientset.NewForConfigOrDie(restclient.AddUserAgent(clusterConfig, userAgentName))
return clientset, nil
}
return nil, err
},
targetInformers: make(map[string]informer),
}
getClusterData := func(name string) []interface{} {
data, err := federatedInformer.GetTargetStore().ListFromCluster(name)
if err != nil {
glog.Errorf("Failed to list %s content: %v", name, err)
return make([]interface{}, 0)
}
return data
}
federatedInformer.clusterInformer.store, federatedInformer.clusterInformer.controller = cache.NewInformer(
&cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (pkgruntime.Object, error) {
return federationClient.Federation().Clusters().List(options)
},
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
return federationClient.Federation().Clusters().Watch(options)
},
},
&federationapi.Cluster{},
clusterSyncPeriod,
cache.ResourceEventHandlerFuncs{
DeleteFunc: func(old interface{}) {
oldCluster, ok := old.(*federationapi.Cluster)
if ok {
var data []interface{}
if clusterLifecycle.ClusterUnavailable != nil {
data = getClusterData(oldCluster.Name)
}
federatedInformer.deleteCluster(oldCluster)
if clusterLifecycle.ClusterUnavailable != nil {
clusterLifecycle.ClusterUnavailable(oldCluster, data)
}
}
},
AddFunc: func(cur interface{}) {
curCluster, ok := cur.(*federationapi.Cluster)
if ok && isClusterReady(curCluster) {
federatedInformer.addCluster(curCluster)
if clusterLifecycle.ClusterAvailable != nil {
clusterLifecycle.ClusterAvailable(curCluster)
}
} else {
glog.Errorf("Cluster %v not added. Not of correct type, or cluster not ready.", cur)
}
},
UpdateFunc: func(old, cur interface{}) {
oldCluster, ok := old.(*federationapi.Cluster)
if !ok {
glog.Errorf("Internal error: Cluster %v not updated. Old cluster not of correct type.", old)
return
}
curCluster, ok := cur.(*federationapi.Cluster)
if !ok {
glog.Errorf("Internal error: Cluster %v not updated. New cluster not of correct type.", cur)
return
}
if isClusterReady(oldCluster) != isClusterReady(curCluster) || !reflect.DeepEqual(oldCluster.Spec, curCluster.Spec) || !reflect.DeepEqual(oldCluster.ObjectMeta.Annotations, curCluster.ObjectMeta.Annotations) {
var data []interface{}
if clusterLifecycle.ClusterUnavailable != nil {
data = getClusterData(oldCluster.Name)
}
federatedInformer.deleteCluster(oldCluster)
if clusterLifecycle.ClusterUnavailable != nil {
clusterLifecycle.ClusterUnavailable(oldCluster, data)
}
if isClusterReady(curCluster) {
federatedInformer.addCluster(curCluster)
if clusterLifecycle.ClusterAvailable != nil {
clusterLifecycle.ClusterAvailable(curCluster)
}
}
} else {
glog.V(4).Infof("Cluster %v not updated to %v as ready status and specs are identical", oldCluster, curCluster)
}
},
},
)
return federatedInformer
}
func isClusterReady(cluster *federationapi.Cluster) bool {
for _, condition := range cluster.Status.Conditions {
if condition.Type == federationapi.ClusterReady {
if condition.Status == apiv1.ConditionTrue {
return true
}
}
}
return false
}
type informer struct {
controller cache.Controller
store cache.Store
stopChan chan struct{}
}
type federatedInformerImpl struct {
sync.Mutex
// Informer on federated clusters.
clusterInformer informer
// Target informers factory
targetInformerFactory TargetInformerFactory
// Structures returned by targetInformerFactory
targetInformers map[string]informer
// A function to build clients.
clientFactory func(*federationapi.Cluster) (kubeclientset.Interface, error)
}
// *federatedInformerImpl implements FederatedInformer interface.
var _ FederatedInformer = &federatedInformerImpl{}
type federatedStoreImpl struct {
federatedInformer *federatedInformerImpl
}
func (f *federatedInformerImpl) Stop() {
glog.V(4).Infof("Stopping federated informer.")
f.Lock()
defer f.Unlock()
glog.V(4).Infof("... Closing cluster informer channel.")
close(f.clusterInformer.stopChan)
for key, informer := range f.targetInformers {
glog.V(4).Infof("... Closing informer channel for %q.", key)
close(informer.stopChan)
// Remove each informer after it has been stopped to prevent
// subsequent cluster deletion from attempting to double close
// an informer's stop channel.
delete(f.targetInformers, key)
}
}
func (f *federatedInformerImpl) Start() {
f.Lock()
defer f.Unlock()
f.clusterInformer.stopChan = make(chan struct{})
go f.clusterInformer.controller.Run(f.clusterInformer.stopChan)
}
func (f *federatedInformerImpl) SetClientFactory(clientFactory func(*federationapi.Cluster) (kubeclientset.Interface, error)) {
f.Lock()
defer f.Unlock()
f.clientFactory = clientFactory
}
// GetClientsetForCluster returns a clientset for the cluster, if present.
func (f *federatedInformerImpl) GetClientsetForCluster(clusterName string) (kubeclientset.Interface, error) {
f.Lock()
defer f.Unlock()
return f.getClientsetForClusterUnlocked(clusterName)
}
func (f *federatedInformerImpl) getClientsetForClusterUnlocked(clusterName string) (kubeclientset.Interface, error) {
// No locking needed. Will happen in f.GetCluster.
glog.V(4).Infof("Getting clientset for cluster %q", clusterName)
if cluster, found, err := f.getReadyClusterUnlocked(clusterName); found && err == nil {
glog.V(4).Infof("Got clientset for cluster %q", clusterName)
return f.clientFactory(cluster)
} else {
if err != nil {
return nil, err
}
}
return nil, fmt.Errorf("cluster %q not found", clusterName)
}
func (f *federatedInformerImpl) GetUnreadyClusters() ([]*federationapi.Cluster, error) {
f.Lock()
defer f.Unlock()
items := f.clusterInformer.store.List()
result := make([]*federationapi.Cluster, 0, len(items))
for _, item := range items {
if cluster, ok := item.(*federationapi.Cluster); ok {
if !isClusterReady(cluster) {
result = append(result, cluster)
}
} else {
return nil, fmt.Errorf("wrong data in FederatedInformerImpl cluster store: %v", item)
}
}
return result, nil
}
// GetReadyClusters returns all clusters for which the sub-informers are run.
func (f *federatedInformerImpl) GetReadyClusters() ([]*federationapi.Cluster, error) {
f.Lock()
defer f.Unlock()
items := f.clusterInformer.store.List()
result := make([]*federationapi.Cluster, 0, len(items))
for _, item := range items {
if cluster, ok := item.(*federationapi.Cluster); ok {
if isClusterReady(cluster) {
result = append(result, cluster)
}
} else {
return nil, fmt.Errorf("wrong data in FederatedInformerImpl cluster store: %v", item)
}
}
return result, nil
}
// GetCluster returns the cluster with the given name, if found.
func (f *federatedInformerImpl) GetReadyCluster(name string) (*federationapi.Cluster, bool, error) {
f.Lock()
defer f.Unlock()
return f.getReadyClusterUnlocked(name)
}
func (f *federatedInformerImpl) getReadyClusterUnlocked(name string) (*federationapi.Cluster, bool, error) {
if obj, exist, err := f.clusterInformer.store.GetByKey(name); exist && err == nil {
if cluster, ok := obj.(*federationapi.Cluster); ok {
if isClusterReady(cluster) {
return cluster, true, nil
}
return nil, false, nil
}
return nil, false, fmt.Errorf("wrong data in FederatedInformerImpl cluster store: %v", obj)
} else {
return nil, false, err
}
}
// Synced returns true if the view is synced (for the first time)
func (f *federatedInformerImpl) ClustersSynced() bool {
return f.clusterInformer.controller.HasSynced()
}
// Adds the given cluster to federated informer.
func (f *federatedInformerImpl) addCluster(cluster *federationapi.Cluster) {
f.Lock()
defer f.Unlock()
name := cluster.Name
if client, err := f.getClientsetForClusterUnlocked(name); err == nil {
store, controller := f.targetInformerFactory(cluster, client)
targetInformer := informer{
controller: controller,
store: store,
stopChan: make(chan struct{}),
}
f.targetInformers[name] = targetInformer
go targetInformer.controller.Run(targetInformer.stopChan)
} else {
// TODO: create also an event for cluster.
glog.Errorf("Failed to create a client for cluster: %v", err)
}
}
// Removes the cluster from federated informer.
func (f *federatedInformerImpl) deleteCluster(cluster *federationapi.Cluster) {
f.Lock()
defer f.Unlock()
name := cluster.Name
if targetInformer, found := f.targetInformers[name]; found {
close(targetInformer.stopChan)
}
delete(f.targetInformers, name)
}
// Returns a store created over all stores from target informers.
func (f *federatedInformerImpl) GetTargetStore() FederatedReadOnlyStore {
return &federatedStoreImpl{
federatedInformer: f,
}
}
// Returns all items in the store.
func (fs *federatedStoreImpl) List() ([]FederatedObject, error) {
fs.federatedInformer.Lock()
defer fs.federatedInformer.Unlock()
result := make([]FederatedObject, 0)
for clusterName, targetInformer := range fs.federatedInformer.targetInformers {
for _, value := range targetInformer.store.List() {
result = append(result, FederatedObject{ClusterName: clusterName, Object: value})
}
}
return result, nil
}
// Returns all items in the given cluster.
func (fs *federatedStoreImpl) ListFromCluster(clusterName string) ([]interface{}, error) {
fs.federatedInformer.Lock()
defer fs.federatedInformer.Unlock()
result := make([]interface{}, 0)
if targetInformer, found := fs.federatedInformer.targetInformers[clusterName]; found {
values := targetInformer.store.List()
result = append(result, values...)
}
return result, nil
}
// GetByKey returns the item stored under the given key in the specified cluster (if exist).
func (fs *federatedStoreImpl) GetByKey(clusterName string, key string) (interface{}, bool, error) {
fs.federatedInformer.Lock()
defer fs.federatedInformer.Unlock()
if targetInformer, found := fs.federatedInformer.targetInformers[clusterName]; found {
return targetInformer.store.GetByKey(key)
}
return nil, false, nil
}
// Returns the items stored under the given key in all clusters.
func (fs *federatedStoreImpl) GetFromAllClusters(key string) ([]FederatedObject, error) {
fs.federatedInformer.Lock()
defer fs.federatedInformer.Unlock()
result := make([]FederatedObject, 0)
for clusterName, targetInformer := range fs.federatedInformer.targetInformers {
value, exist, err := targetInformer.store.GetByKey(key)
if err != nil {
return nil, err
}
if exist {
result = append(result, FederatedObject{ClusterName: clusterName, Object: value})
}
}
return result, nil
}
// GetKeyFor returns the key under which the item would be put in the store.
func (fs *federatedStoreImpl) GetKeyFor(item interface{}) string {
// TODO: support other keying functions.
key, _ := cache.DeletionHandlingMetaNamespaceKeyFunc(item)
return key
}
// Checks whether stores for all clusters form the lists (and only these) are there and
// are synced.
func (fs *federatedStoreImpl) ClustersSynced(clusters []*federationapi.Cluster) bool {
// Get the list of informers to check under a lock and check it outside.
okSoFar, informersToCheck := func() (bool, []informer) {
fs.federatedInformer.Lock()
defer fs.federatedInformer.Unlock()
if len(fs.federatedInformer.targetInformers) != len(clusters) {
return false, []informer{}
}
informersToCheck := make([]informer, 0, len(clusters))
for _, cluster := range clusters {
if targetInformer, found := fs.federatedInformer.targetInformers[cluster.Name]; found {
informersToCheck = append(informersToCheck, targetInformer)
} else {
return false, []informer{}
}
}
return true, informersToCheck
}()
if !okSoFar {
return false
}
for _, informerToCheck := range informersToCheck {
if !informerToCheck.controller.HasSynced() {
return false
}
}
return true
}

View file

@ -0,0 +1,150 @@
/*
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"
apiv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/watch"
kubeclientset "k8s.io/client-go/kubernetes"
fakekubeclientset "k8s.io/client-go/kubernetes/fake"
core "k8s.io/client-go/testing"
"k8s.io/client-go/tools/cache"
federationapi "k8s.io/kubernetes/federation/apis/federation/v1beta1"
fakefederationclientset "k8s.io/kubernetes/federation/client/clientset_generated/federation_clientset/fake"
"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: metav1.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: metav1.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.Controller) {
return cache.NewInformer(
&cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
return clientset.Core().Services(metav1.NamespaceAll).List(options)
},
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
return clientset.Core().Services(metav1.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,157 @@
/*
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"
"strings"
"time"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
pkgruntime "k8s.io/apimachinery/pkg/runtime"
kubeclientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/record"
"k8s.io/kubernetes/pkg/api"
)
// 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
Key string
}
// A helper that executes the given set of updates on federation, in parallel.
type FederatedUpdater interface {
// Executes the given set of operations.
Update([]FederatedOperation) 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
kind string
timeout time.Duration
eventRecorder record.EventRecorder
addFunction FederatedOperationHandler
updateFunction FederatedOperationHandler
deleteFunction FederatedOperationHandler
}
func NewFederatedUpdater(federation FederationView, kind string, timeout time.Duration, recorder record.EventRecorder, add, update, del FederatedOperationHandler) FederatedUpdater {
return &federatedUpdaterImpl{
federation: federation,
kind: kind,
timeout: timeout,
eventRecorder: recorder,
addFunction: add,
updateFunction: update,
deleteFunction: del,
}
}
func (fu *federatedUpdaterImpl) recordEvent(obj runtime.Object, eventType, eventVerb string, args ...interface{}) {
messageFmt := eventVerb + " %s %q in cluster %s"
fu.eventRecorder.Eventf(obj, api.EventTypeNormal, eventType, messageFmt, args...)
}
// Update executes the given set of operations within the timeout specified for
// the instance. 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.
func (fu *federatedUpdaterImpl) Update(ops []FederatedOperation) 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
}
eventArgs := []interface{}{fu.kind, op.Key, clusterName}
baseEventType := fmt.Sprintf("%s", op.Type)
eventType := fmt.Sprintf("%sInCluster", strings.Title(baseEventType))
switch op.Type {
case OperationTypeAdd:
// TODO s+OperationTypeAdd+OperationTypeCreate+
baseEventType = "create"
eventType := "CreateInCluster"
fu.recordEvent(op.Obj, eventType, "Creating", eventArgs...)
err = fu.addFunction(clientset, op.Obj)
case OperationTypeUpdate:
fu.recordEvent(op.Obj, eventType, "Updating", eventArgs...)
err = fu.updateFunction(clientset, op.Obj)
case OperationTypeDelete:
fu.recordEvent(op.Obj, eventType, "Deleting", eventArgs...)
err = fu.deleteFunction(clientset, op.Obj)
// IsNotFound error is fine since that means the object is deleted already.
if errors.IsNotFound(err) {
err = nil
}
}
if err != nil {
eventType := eventType + "Failed"
messageFmt := "Failed to " + baseEventType + " %s %q in cluster %s: %v"
eventArgs = append(eventArgs, err)
fu.eventRecorder.Eventf(op.Obj, api.EventTypeWarning, eventType, messageFmt, eventArgs...)
}
done <- err
}(op)
}
start := time.Now()
for i := 0; i < len(ops); i++ {
now := time.Now()
if !now.Before(start.Add(fu.timeout)) {
return fmt.Errorf("failed to finish all operations in %v", fu.timeout)
}
select {
case err := <-done:
if err != nil {
return err
}
case <-time.After(start.Add(fu.timeout).Sub(now)):
return fmt.Errorf("failed to finish all operations in %v", fu.timeout)
}
}
// All operations finished in time.
return nil
}

View file

@ -0,0 +1,157 @@
/*
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"
apiv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
pkgruntime "k8s.io/apimachinery/pkg/runtime"
kubeclientset "k8s.io/client-go/kubernetes"
fakekubeclientset "k8s.io/client-go/kubernetes/fake"
federationapi "k8s.io/kubernetes/federation/apis/federation/v1beta1"
"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
}
type fakeEventRecorder struct{}
func (f *fakeEventRecorder) Event(object pkgruntime.Object, eventtype, reason, message string) {}
func (f *fakeEventRecorder) Eventf(object pkgruntime.Object, eventtype, reason, messageFmt string, args ...interface{}) {
}
func (f *fakeEventRecorder) PastEventf(object pkgruntime.Object, timestamp metav1.Time, eventtype, reason, messageFmt string, args ...interface{}) {
}
func TestFederatedUpdaterOK(t *testing.T) {
addChan := make(chan string, 5)
updateChan := make(chan string, 5)
updater := NewFederatedUpdater(&fakeFederationView{}, "foo", time.Minute, &fakeEventRecorder{},
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"),
},
})
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{}, "foo", time.Minute, &fakeEventRecorder{},
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"),
},
})
assert.Error(t, err)
}
func TestFederatedUpdaterTimeout(t *testing.T) {
start := time.Now()
updater := NewFederatedUpdater(&fakeFederationView{}, "foo", time.Second, &fakeEventRecorder{},
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"),
},
})
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: metav1.ObjectMeta{
Namespace: "ns1",
Name: name,
},
}
}
func noop(_ kubeclientset.Interface, _ pkgruntime.Object) error {
return nil
}

View file

@ -0,0 +1,43 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = ["finalizers.go"],
deps = [
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)
go_test(
name = "go_default_test",
srcs = ["finalizers_test.go"],
library = ":go_default_library",
deps = [
"//vendor/github.com/stretchr/testify/assert:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
],
)

View file

@ -0,0 +1,66 @@
/*
Copyright 2017 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.
*/
// Helper functions for manipulating finalizers.
package finalizers
import (
meta "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets"
)
// HasFinalizer returns true if the given object has the given finalizer in its ObjectMeta.
func HasFinalizer(obj runtime.Object, finalizer string) (bool, error) {
accessor, err := meta.Accessor(obj)
if err != nil {
return false, err
}
finalizers := sets.NewString(accessor.GetFinalizers()...)
return finalizers.Has(finalizer), nil
}
// AddFinalizers adds the given finalizers to the given objects ObjectMeta.
// Returns true if the object was updated.
func AddFinalizers(obj runtime.Object, newFinalizers sets.String) (bool, error) {
accessor, err := meta.Accessor(obj)
if err != nil {
return false, err
}
oldFinalizers := sets.NewString(accessor.GetFinalizers()...)
if oldFinalizers.IsSuperset(newFinalizers) {
return false, nil
}
allFinalizers := oldFinalizers.Union(newFinalizers)
accessor.SetFinalizers(allFinalizers.List())
return true, nil
}
// RemoveFinalizers removes the given finalizers from the given objects ObjectMeta.
// Returns true if the object was updated.
func RemoveFinalizers(obj runtime.Object, finalizers sets.String) (bool, error) {
accessor, err := meta.Accessor(obj)
if err != nil {
return false, err
}
oldFinalizers := sets.NewString(accessor.GetFinalizers()...)
if oldFinalizers.Intersection(finalizers).Len() == 0 {
return false, nil
}
newFinalizers := oldFinalizers.Difference(finalizers)
accessor.SetFinalizers(newFinalizers.List())
return true, nil
}

View file

@ -0,0 +1,171 @@
/*
Copyright 2017 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 finalizers
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/api/core/v1"
meta "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets"
)
func newObj(finalizers []string) runtime.Object {
pod := v1.Pod{}
pod.ObjectMeta.Finalizers = finalizers
return &pod
}
func TestHasFinalizer(t *testing.T) {
testCases := []struct {
obj runtime.Object
finalizer string
result bool
}{
{
newObj([]string{}),
"",
false,
},
{
newObj([]string{}),
"someFinalizer",
false,
},
{
newObj([]string{"someFinalizer"}),
"",
false,
},
{
newObj([]string{"someFinalizer"}),
"anotherFinalizer",
false,
},
{
newObj([]string{"someFinalizer"}),
"someFinalizer",
true,
},
{
newObj([]string{"anotherFinalizer", "someFinalizer"}),
"someFinalizer",
true,
},
}
for index, test := range testCases {
hasFinalizer, _ := HasFinalizer(test.obj, test.finalizer)
assert.Equal(t, hasFinalizer, test.result, fmt.Sprintf("Test case %d failed. Expected: %v, actual: %v", index, test.result, hasFinalizer))
}
}
func TestAddFinalizers(t *testing.T) {
testCases := []struct {
obj runtime.Object
finalizers sets.String
isUpdated bool
newFinalizers []string
}{
{
newObj([]string{}),
sets.NewString(),
false,
[]string{},
},
{
newObj([]string{}),
sets.NewString("someFinalizer"),
true,
[]string{"someFinalizer"},
},
{
newObj([]string{"someFinalizer"}),
sets.NewString(),
false,
[]string{"someFinalizer"},
},
{
newObj([]string{"someFinalizer"}),
sets.NewString("anotherFinalizer"),
true,
[]string{"anotherFinalizer", "someFinalizer"},
},
{
newObj([]string{"someFinalizer"}),
sets.NewString("someFinalizer"),
false,
[]string{"someFinalizer"},
},
}
for index, test := range testCases {
isUpdated, _ := AddFinalizers(test.obj, test.finalizers)
assert.Equal(t, isUpdated, test.isUpdated, fmt.Sprintf("Test case %d failed. Expected isUpdated: %v, actual: %v", index, test.isUpdated, isUpdated))
accessor, _ := meta.Accessor(test.obj)
newFinalizers := accessor.GetFinalizers()
assert.Equal(t, test.newFinalizers, newFinalizers, fmt.Sprintf("Test case %d failed. Expected finalizers: %v, actual: %v", index, test.newFinalizers, newFinalizers))
}
}
func TestRemoveFinalizers(t *testing.T) {
testCases := []struct {
obj runtime.Object
finalizers sets.String
isUpdated bool
newFinalizers []string
}{
{
newObj([]string{}),
sets.NewString(),
false,
[]string{},
},
{
newObj([]string{}),
sets.NewString("someFinalizer"),
false,
[]string{},
},
{
newObj([]string{"someFinalizer"}),
sets.NewString(),
false,
[]string{"someFinalizer"},
},
{
newObj([]string{"someFinalizer"}),
sets.NewString("anotherFinalizer"),
false,
[]string{"someFinalizer"},
},
{
newObj([]string{"someFinalizer", "anotherFinalizer"}),
sets.NewString("someFinalizer"),
true,
[]string{"anotherFinalizer"},
},
}
for index, test := range testCases {
isUpdated, _ := RemoveFinalizers(test.obj, test.finalizers)
assert.Equal(t, isUpdated, test.isUpdated, fmt.Sprintf("Test case %d failed. Expected isUpdated: %v, actual: %v", index, test.isUpdated, isUpdated))
accessor, _ := meta.Accessor(test.obj)
newFinalizers := accessor.GetFinalizers()
assert.Equal(t, test.newFinalizers, newFinalizers, fmt.Sprintf("Test case %d failed. Expected finalizers: %v, actual: %v", index, test.newFinalizers, newFinalizers))
}
}

View file

@ -0,0 +1,112 @@
/*
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"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
pkgruntime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/cache"
)
// 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").(metav1.ObjectMeta)
curMeta := getFieldOrPanic(cur, "ObjectMeta").(metav1.ObjectMeta)
if !ObjectMetaEquivalent(oldMeta, curMeta) ||
!reflect.DeepEqual(oldMeta.DeletionTimestamp, curMeta.DeletionTimestamp) ||
!reflect.DeepEqual(getFieldOrPanic(old, "Spec"), getFieldOrPanic(cur, "Spec")) {
triggerFunc(curObj)
}
},
}
}
// Returns cache.ResourceEventHandlerFuncs that trigger the given function
// on object add/delete or ObjectMeta or given field is updated.
func NewTriggerOnMetaAndFieldChanges(field string, 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").(metav1.ObjectMeta)
curMeta := getFieldOrPanic(cur, "ObjectMeta").(metav1.ObjectMeta)
if !ObjectMetaEquivalent(oldMeta, curMeta) ||
!reflect.DeepEqual(getFieldOrPanic(old, field), getFieldOrPanic(cur, field)) {
triggerFunc(curObj)
}
},
}
}

View file

@ -0,0 +1,100 @@
/*
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/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
pkgruntime "k8s.io/apimachinery/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: metav1.ObjectMeta{
Namespace: "ns1",
Name: "s1",
},
}
service2 := apiv1.Service{
ObjectMeta: metav1.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: metav1.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,37 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = ["hpa.go"],
visibility = ["//visibility:public"],
deps = [
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["hpa_test.go"],
library = ":go_default_library",
deps = [
"//vendor/github.com/stretchr/testify/require:go_default_library",
"//vendor/k8s.io/api/autoscaling/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View file

@ -0,0 +1,75 @@
/*
Copyright 2017 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 hpa
import (
"encoding/json"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
)
const (
// FederatedAnnotationOnHpaTargetObj as key, is used by hpa controller to
// set selected cluster name list as annotation on the target object.
FederatedAnnotationOnHpaTargetObj = "federation.kubernetes.io/hpa-target-cluster-list"
)
// ClusterNames stores the list of clusters represented by names as appearing on federation
// cluster objects. This is set by federation hpa and used by target objects federation
// controller to restrict that target object to only these clusters.
type ClusterNames struct {
Names []string
}
func (cn *ClusterNames) String() string {
annotationBytes, _ := json.Marshal(cn)
return string(annotationBytes[:])
}
// GetHpaTargetClusterList is used to get the list of clusters from the target object
// annotations.
func GetHpaTargetClusterList(obj runtime.Object) (*ClusterNames, error) {
accessor, _ := meta.Accessor(obj)
targetObjAnno := accessor.GetAnnotations()
if targetObjAnno == nil {
return nil, nil
}
targetObjAnnoString, exists := targetObjAnno[FederatedAnnotationOnHpaTargetObj]
if !exists {
return nil, nil
}
clusterNames := &ClusterNames{}
if err := json.Unmarshal([]byte(targetObjAnnoString), clusterNames); err != nil {
return nil, err
}
return clusterNames, nil
}
// SetHpaTargetClusterList is used to set the list of clusters on the target object
// annotations.
func SetHpaTargetClusterList(obj runtime.Object, clusterNames ClusterNames) runtime.Object {
accessor, _ := meta.Accessor(obj)
anno := accessor.GetAnnotations()
if anno == nil {
anno = make(map[string]string)
accessor.SetAnnotations(anno)
}
anno[FederatedAnnotationOnHpaTargetObj] = clusterNames.String()
return obj
}

View file

@ -0,0 +1,115 @@
/*
Copyright 2017 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 hpa
import (
"testing"
autoscalingv1 "k8s.io/api/autoscaling/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/stretchr/testify/require"
)
func TestGetHpaTargetClusterList(t *testing.T) {
// Any object is fine for this test.
obj := &autoscalingv1.HorizontalPodAutoscaler{
ObjectMeta: metav1.ObjectMeta{
Name: "myhpa",
Namespace: "myNamespace",
SelfLink: "/api/mylink",
},
}
testCases := map[string]struct {
clusterNames *ClusterNames
expectedErr bool
}{
"Wrong data set on annotations should return unmarshalling error when retrieving": {
expectedErr: true,
},
"Get clusternames on annotations with 2 clusters, should have same names, which were set": {
clusterNames: &ClusterNames{
Names: []string{
"c1",
"c2",
},
},
},
}
for testName, testCase := range testCases {
t.Run(testName, func(t *testing.T) {
accessor, _ := meta.Accessor(obj)
anno := accessor.GetAnnotations()
if anno == nil {
anno = make(map[string]string)
accessor.SetAnnotations(anno)
}
if testCase.expectedErr {
anno[FederatedAnnotationOnHpaTargetObj] = "{" //some random string
} else {
anno[FederatedAnnotationOnHpaTargetObj] = testCase.clusterNames.String()
}
readNames, err := GetHpaTargetClusterList(obj)
if testCase.expectedErr {
require.Error(t, err, "An error was expected")
} else {
require.Equal(t, testCase.clusterNames, readNames, "Names should have been equal")
}
})
}
}
func TestSetHpaTargetClusterList(t *testing.T) {
// Any object is fine for this test.
obj := &autoscalingv1.HorizontalPodAutoscaler{
ObjectMeta: metav1.ObjectMeta{
Name: "myhpa",
Namespace: "myNamespace",
SelfLink: "/api/mylink",
},
}
testCases := map[string]struct {
clusterNames ClusterNames
expectedErr bool
}{
"Get clusternames on annotations with 2 clusters, should have same names, which were set": {
clusterNames: ClusterNames{
Names: []string{
"c1",
"c2",
},
},
},
}
for testName, testCase := range testCases {
t.Run(testName, func(t *testing.T) {
SetHpaTargetClusterList(obj, testCase.clusterNames)
readNames, err := GetHpaTargetClusterList(obj)
require.NoError(t, err, "An error should not have happened")
require.Equal(t, &testCase.clusterNames, readNames, "Names should have been equal")
})
}
}

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"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/kubernetes/pkg/api"
)
// 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 metav1.ObjectMeta) metav1.ObjectMeta {
return metav1.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 metav1.ObjectMeta) metav1.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 metav1.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().(metav1.ObjectMeta)
objectMetaB := reflect.ValueOf(b).Elem().FieldByName("ObjectMeta").Interface().(metav1.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 := api.Scheme.DeepCopy(item)
if err != nil {
panic(err)
}
return result
}

View file

@ -0,0 +1,117 @@
/*
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/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/stretchr/testify/assert"
)
func TestObjectMeta(t *testing.T) {
o1 := metav1.ObjectMeta{
Namespace: "ns1",
Name: "s1",
UID: "1231231412",
ResourceVersion: "999",
}
o2 := copyObjectMeta(o1)
o3 := metav1.ObjectMeta{
Namespace: "ns1",
Name: "s1",
UID: "1231231412",
Annotations: map[string]string{"A": "B"},
}
o4 := metav1.ObjectMeta{
Namespace: "ns1",
Name: "s1",
UID: "1231255531412",
Annotations: map[string]string{"A": "B"},
}
o5 := metav1.ObjectMeta{
Namespace: "ns1",
Name: "s1",
ResourceVersion: "1231231412",
Annotations: map[string]string{"A": "B"},
}
o6 := metav1.ObjectMeta{
Namespace: "ns1",
Name: "s1",
ResourceVersion: "1231255531412",
Annotations: map[string]string{"A": "B"},
}
o7 := metav1.ObjectMeta{
Namespace: "ns1",
Name: "s1",
ResourceVersion: "1231255531412",
Annotations: map[string]string{},
Labels: map[string]string{},
}
o8 := metav1.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: metav1.ObjectMeta{
Namespace: "ns1",
Name: "s1",
},
Spec: api_v1.ServiceSpec{
ExternalName: "Service1",
},
}
s1b := s1
s2 := api_v1.Service{
ObjectMeta: metav1.ObjectMeta{
Namespace: "ns1",
Name: "s2",
},
Spec: api_v1.ServiceSpec{
ExternalName: "Service1",
},
}
s3 := api_v1.Service{
ObjectMeta: metav1.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,36 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = ["planner.go"],
deps = ["//federation/apis/federation:go_default_library"],
)
go_test(
name = "go_default_test",
srcs = ["planner_test.go"],
library = ":go_default_library",
deps = [
"//federation/apis/federation:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

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.ReplicaAllocationPreferences
}
type namedClusterPreferences struct {
clusterName string
hash uint32
fedapi.ClusterPreferences
}
type byWeight []*namedClusterPreferences
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.ReplicaAllocationPreferences) *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 lexicographical 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 scheduled 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([]*namedClusterPreferences, 0, len(availableClusters))
plan := make(map[string]int64, len(preferences))
overflow := make(map[string]int64, len(preferences))
named := func(name string, pref fedapi.ClusterPreferences) *namedClusterPreferences {
// Seems to work better than addler for our case.
hasher := fnv.New32()
hasher.Write([]byte(name))
hasher.Write([]byte(replicaSetKey))
return &namedClusterPreferences{
clusterName: name,
hash: hasher.Sum32(),
ClusterPreferences: 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 distribute 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([]*namedClusterPreferences, 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.ClusterPreferences, replicas int64, clusters []string, expected map[string]int64) {
planer := NewPlanner(&fedapi.ReplicaAllocationPreferences{
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.ClusterPreferences, replicas int64, clusters []string,
existing map[string]int64, expected map[string]int64) {
planer := NewPlanner(&fedapi.ReplicaAllocationPreferences{
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.ClusterPreferences, replicas int64, clusters []string,
existing map[string]int64,
capacity map[string]int64,
expected map[string]int64,
expectedOverflow map[string]int64) {
planer := NewPlanner(&fedapi.ReplicaAllocationPreferences{
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.ClusterPreferences{
"*": {Weight: 1}},
50, []string{"A", "B", "C"},
// hash dependent
map[string]int64{"A": 16, "B": 17, "C": 17})
doCheck(t, map[string]fedapi.ClusterPreferences{
"*": {Weight: 1}},
50, []string{"A", "B"},
map[string]int64{"A": 25, "B": 25})
doCheck(t, map[string]fedapi.ClusterPreferences{
"*": {Weight: 1}},
1, []string{"A", "B"},
// hash dependent
map[string]int64{"A": 0, "B": 1})
doCheck(t, map[string]fedapi.ClusterPreferences{
"*": {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.ClusterPreferences{
"*": {Weight: 1}},
1, []string{"A"},
map[string]int64{"A": 1})
doCheck(t, map[string]fedapi.ClusterPreferences{
"*": {Weight: 1}},
1, []string{},
map[string]int64{})
}
func TestEqualWithExisting(t *testing.T) {
doCheckWithExisting(t, map[string]fedapi.ClusterPreferences{
"*": {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.ClusterPreferences{
"*": {Weight: 1}},
50, []string{"A", "B"},
map[string]int64{"A": 30},
map[string]int64{"A": 30, "B": 20})
doCheckWithExisting(t, map[string]fedapi.ClusterPreferences{
"*": {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.ClusterPreferences{
"*": {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.ClusterPreferences{
"*": {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.ClusterPreferences{
"*": {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.ClusterPreferences{
"*": {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.ClusterPreferences{
"*": {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.ClusterPreferences{
"*": {Weight: 1}},
500000, []string{"A", "B"},
map[string]int64{"A": 300000},
map[string]int64{"A": 300000, "B": 200000})
doCheckWithExisting(t, map[string]fedapi.ClusterPreferences{
"*": {Weight: 1}},
50, []string{"A", "B"},
map[string]int64{"A": 10},
map[string]int64{"A": 25, "B": 25})
doCheckWithExisting(t, map[string]fedapi.ClusterPreferences{
"*": {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.ClusterPreferences{
"*": {Weight: 1}},
1, []string{"A", "B"},
map[string]int64{"A": 30},
map[string]int64{"A": 1, "B": 0})
doCheckWithExisting(t, map[string]fedapi.ClusterPreferences{
"*": {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.ClusterPreferences{
"*": {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.ClusterPreferences{
"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.ClusterPreferences{
"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.ClusterPreferences{
"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.ClusterPreferences{
"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.ClusterPreferences{
"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.ClusterPreferences{
"*": {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.ClusterPreferences{
"*": {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.ClusterPreferences{
"*": {MinReplicas: 2, Weight: 0}},
50, []string{"A", "B", "C"},
map[string]int64{"A": 2, "B": 2, "C": 2})
doCheck(t, map[string]fedapi.ClusterPreferences{
"*": {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.ClusterPreferences{
"*": {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.ClusterPreferences{
"*": {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.ClusterPreferences{
"*": {Weight: 1, MaxReplicas: pint(2)}},
50, []string{"A", "B", "C"},
map[string]int64{"A": 2, "B": 2, "C": 2})
doCheck(t, map[string]fedapi.ClusterPreferences{
"*": {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.ClusterPreferences{
"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.ClusterPreferences{
"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.ClusterPreferences{
"A": {Weight: 10000},
"B": {Weight: 1}},
50, []string{"B", "C"},
map[string]int64{"B": 50, "C": 0})
doCheck(t, map[string]fedapi.ClusterPreferences{
"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.ClusterPreferences{
"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.ClusterPreferences{
"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.ClusterPreferences{
"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,38 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = ["pod_helper.go"],
deps = ["//vendor/k8s.io/api/core/v1:go_default_library"],
)
go_test(
name = "go_default_test",
srcs = ["pod_helper_test.go"],
library = ":go_default_library",
deps = [
"//vendor/github.com/stretchr/testify/assert:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/api/extensions/v1beta1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

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 podanalyzer
import (
"time"
api_v1 "k8s.io/api/core/v1"
)
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
)
// AnalyzePods 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 AnalyzePods(pods *api_v1.PodList, currentTime time.Time) PodAnalysisResult {
result := PodAnalysisResult{}
for _, pod := range pods.Items {
result.Total++
for _, condition := range pod.Status.Conditions {
if pod.Status.Phase == api_v1.PodRunning {
if condition.Type == api_v1.PodReady {
result.RunningAndReady++
}
} else if condition.Type == api_v1.PodScheduled &&
condition.Status == api_v1.ConditionFalse &&
condition.Reason == api_v1.PodReasonUnschedulable &&
condition.LastTransitionTime.Add(UnschedulableThreshold).Before(currentTime) {
result.Unschedulable++
}
}
}
return result
}

View file

@ -0,0 +1,98 @@
/*
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"
api_v1 "k8s.io/api/core/v1"
"k8s.io/api/extensions/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/stretchr/testify/assert"
)
func TestAnalyze(t *testing.T) {
now := time.Now()
podRunning := newPod("p1",
api_v1.PodStatus{
Phase: api_v1.PodRunning,
Conditions: []api_v1.PodCondition{
{
Type: api_v1.PodReady,
Status: api_v1.ConditionTrue,
},
},
})
podUnschedulable := newPod("pU",
api_v1.PodStatus{
Phase: api_v1.PodPending,
Conditions: []api_v1.PodCondition{
{
Type: api_v1.PodScheduled,
Status: api_v1.ConditionFalse,
Reason: api_v1.PodReasonUnschedulable,
LastTransitionTime: metav1.Time{Time: now.Add(-10 * time.Minute)},
},
},
})
podOther := newPod("pO",
api_v1.PodStatus{
Phase: api_v1.PodPending,
Conditions: []api_v1.PodCondition{},
})
result := AnalyzePods(&api_v1.PodList{Items: []api_v1.Pod{*podRunning, *podRunning, *podRunning, *podUnschedulable, *podUnschedulable}}, now)
assert.Equal(t, PodAnalysisResult{
Total: 5,
RunningAndReady: 3,
Unschedulable: 2,
}, result)
result = AnalyzePods(&api_v1.PodList{Items: []api_v1.Pod{*podOther}}, now)
assert.Equal(t, PodAnalysisResult{
Total: 1,
RunningAndReady: 0,
Unschedulable: 0,
}, result)
}
func newReplicaSet(selectorMap map[string]string) *v1beta1.ReplicaSet {
replicas := int32(3)
rs := &v1beta1.ReplicaSet{
ObjectMeta: metav1.ObjectMeta{
Name: "foobar",
Namespace: metav1.NamespaceDefault,
},
Spec: v1beta1.ReplicaSetSpec{
Replicas: &replicas,
Selector: &metav1.LabelSelector{MatchLabels: selectorMap},
},
}
return rs
}
func newPod(name string, status api_v1.PodStatus) *api_v1.Pod {
return &api_v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: metav1.NamespaceDefault,
},
Status: status,
}
}

View file

@ -0,0 +1,43 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = ["preferences.go"],
deps = [
"//federation/apis/federation:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)
go_test(
name = "go_default_test",
srcs = ["preferences_test.go"],
library = ":go_default_library",
deps = [
"//vendor/github.com/stretchr/testify/assert:go_default_library",
"//vendor/k8s.io/api/extensions/v1beta1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
],
)

View file

@ -0,0 +1,55 @@
/*
Copyright 2017 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 replicapreferences
import (
"encoding/json"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
fed "k8s.io/kubernetes/federation/apis/federation"
)
// GetAllocationPreferences reads the preferences from the annotations on the given object.
// It takes in an object and determines the supported types.
// Callers need to pass the string key used to store the annotations.
// Returns nil if the annotations with the given key are not found.
func GetAllocationPreferences(obj runtime.Object, key string) (*fed.ReplicaAllocationPreferences, error) {
if obj == nil {
return nil, nil
}
accessor, err := meta.Accessor(obj)
if err != nil {
return nil, err
}
annotations := accessor.GetAnnotations()
if annotations == nil {
return nil, nil
}
prefString, found := annotations[key]
if !found {
return nil, nil
}
var pref fed.ReplicaAllocationPreferences
if err := json.Unmarshal([]byte(prefString), &pref); err != nil {
return nil, err
}
return &pref, nil
}

View file

@ -0,0 +1,92 @@
/*
Copyright 2017 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 replicapreferences
import (
"testing"
extensionsv1 "k8s.io/api/extensions/v1beta1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/runtime"
)
const (
TestPreferencesAnnotationKey = "federation.kubernetes.io/test-preferences"
)
func TestGetAllocationPreferences(t *testing.T) {
testCases := []struct {
testname string
prefs string
obj runtime.Object
errorExpected bool
}{
{
testname: "good preferences",
prefs: `{"rebalance": true,
"clusters": {
"k8s-1": {"minReplicas": 10, "maxReplicas": 20, "weight": 2},
"*": {"weight": 1}
}}`,
obj: &extensionsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-obj",
Namespace: metav1.NamespaceDefault,
SelfLink: "/api/v1/namespaces/default/obj/test-obj",
},
},
errorExpected: false,
},
{
testname: "failed preferences",
prefs: `{`, // bad json
obj: &extensionsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-obj",
Namespace: metav1.NamespaceDefault,
SelfLink: "/api/v1/namespaces/default/obj/test-obj",
},
},
errorExpected: true,
},
}
// prepare the objects
for _, tc := range testCases {
accessor, _ := meta.Accessor(tc.obj)
anno := accessor.GetAnnotations()
if anno == nil {
anno = make(map[string]string)
accessor.SetAnnotations(anno)
}
anno[TestPreferencesAnnotationKey] = tc.prefs
}
// test get preferences
for _, tc := range testCases {
pref, err := GetAllocationPreferences(tc.obj, TestPreferencesAnnotationKey)
if tc.errorExpected {
assert.NotNil(t, err)
} else {
assert.NotNil(t, pref)
assert.Nil(t, err)
}
}
}

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/api/core/v1"
)
// Checks if cluster-independent, user provided data in two given Secrets are equal. 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,39 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["test_helper.go"],
deps = [
"//federation/apis/federation/v1beta1:go_default_library",
"//federation/pkg/federation-controller/util:go_default_library",
"//federation/pkg/federation-controller/util/finalizers:go_default_library",
"//pkg/api:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
"//vendor/github.com/stretchr/testify/require:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/watch:go_default_library",
"//vendor/k8s.io/client-go/testing:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View file

@ -0,0 +1,454 @@
/*
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"
"testing"
"time"
apiv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/watch"
core "k8s.io/client-go/testing"
federationapi "k8s.io/kubernetes/federation/apis/federation/v1beta1"
"k8s.io/kubernetes/federation/pkg/federation-controller/util"
finalizersutil "k8s.io/kubernetes/federation/pkg/federation-controller/util/finalizers"
"k8s.io/kubernetes/pkg/api"
"github.com/golang/glog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
pushTimeout = 5 * time.Second
)
// A structure that distributes events 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)
glog.Infof("Stopping WatcherDispatcher")
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(), 100),
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
})
}
// RegisterFakeClusterGet registers a get response for the cluster resource inside the given fake client.
func RegisterFakeClusterGet(client *core.Fake, obj runtime.Object) {
clusterList, ok := obj.(*federationapi.ClusterList)
client.AddReactor("get", "clusters", func(action core.Action) (bool, runtime.Object, error) {
name := action.(core.GetAction).GetName()
if ok {
for _, cluster := range clusterList.Items {
if cluster.Name == name {
return true, &cluster, nil
}
}
}
return false, nil, fmt.Errorf("could not find the requested cluster: %s", name)
})
}
// RegisterFakeOnCreate registers a reactor in the given fake client that passes
// all created objects to the given watcher.
func RegisterFakeOnCreate(resource string, client *core.Fake, watcher *WatcherDispatcher) {
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: %v", obj)
watcher.Add(obj)
}
return true, originalObj, 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
}
// RegisterFakeOnUpdate registers a reactor in the given fake client that passes
// all updated objects to the given watcher.
func RegisterFakeOnUpdate(resource string, client *core.Fake, watcher *WatcherDispatcher) {
client.AddReactor("update", resource, func(action core.Action) (bool, runtime.Object, error) {
updateAction := action.(core.UpdateAction)
originalObj := updateAction.GetObject()
glog.V(7).Infof("Updating %s: %v", resource, updateAction.GetObject())
// Create a copy of the object here to prevent data races while reading the object in go routine.
obj := copy(originalObj)
operation := func() {
glog.V(4).Infof("Object updated %v", obj)
watcher.Modify(obj)
}
select {
case watcher.orderExecution <- operation:
break
case <-time.After(pushTimeout):
glog.Errorf("Fake client execution channel blocked")
glog.Errorf("Tried to push %v", updateAction)
}
return true, originalObj, nil
})
return
}
// 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()
glog.V(7).Infof("Updating %s: %v", resource, updateAction.GetObject())
// Create a copy of the object here to prevent data races while reading the object in go routine.
obj := copy(originalObj)
operation := func() {
glog.V(4).Infof("Object updated. Writing to channel: %v", obj)
watcher.Modify(obj)
objChan <- obj
}
select {
case watcher.orderExecution <- operation:
break
case <-time.After(pushTimeout):
glog.Errorf("Fake client execution channel blocked")
glog.Errorf("Tried to push %v", updateAction)
}
return true, originalObj, nil
})
return objChan
}
// RegisterFakeOnDelete registers a reactor in the given fake client that passes
// all deleted objects to the given watcher. Since we could get only name of the
// deleted object from DeleteAction, this register function relies on the getObject
// function passed to get the object by name and pass it watcher.
func RegisterFakeOnDelete(resource string, client *core.Fake, watcher *WatcherDispatcher, getObject func(name, namespace string) runtime.Object) {
client.AddReactor("delete", resource, func(action core.Action) (bool, runtime.Object, error) {
deleteAction := action.(core.DeleteAction)
obj := getObject(deleteAction.GetName(), deleteAction.GetNamespace())
glog.V(7).Infof("Deleting %s: %v", resource, obj)
operation := func() {
glog.V(4).Infof("Object deleted %v", obj)
watcher.Delete(obj)
}
select {
case watcher.orderExecution <- operation:
break
case <-time.After(pushTimeout):
glog.Errorf("Fake client execution channel blocked")
glog.Errorf("Tried to push %v", deleteAction)
}
return true, obj, nil
})
return
}
// Adds an update reactor to the given fake client.
// The reactor just returns the object passed to update action.
// This is used as a hack to workaround https://github.com/kubernetes/kubernetes/issues/40939.
// Without this, all update actions using fake client return empty objects.
func AddFakeUpdateReactor(resource string, client *core.Fake) {
client.AddReactor("update", resource, func(action core.Action) (bool, runtime.Object, error) {
updateAction := action.(core.UpdateAction)
originalObj := updateAction.GetObject()
return true, originalObj, nil
})
}
// 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 metav1.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.Name, b.Name)
}
if !reflect.DeepEqual(a.Labels, b.Labels) && (len(a.Labels) != 0 || len(b.Labels) != 0) {
return fmt.Errorf("Labels are different expected:%v observed:%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 observed:%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: metav1.ObjectMeta{
Name: name,
Annotations: map[string]string{},
Labels: map[string]string{"cluster": name},
},
Status: federationapi.ClusterStatus{
Conditions: []federationapi.ClusterCondition{
{Type: federationapi.ClusterReady, Status: readyStatus},
},
Zones: []string{"foozone"},
Region: "fooregion",
},
}
}
// 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)
}
}
func AssertHasFinalizer(t *testing.T, obj runtime.Object, finalizer string) {
hasFinalizer, err := finalizersutil.HasFinalizer(obj, finalizer)
require.Nil(t, err)
assert.True(t, hasFinalizer)
}
func NewInt32(val int32) *int32 {
p := new(int32)
*p = val
return p
}