mirror of https://github.com/k3s-io/k3s
Merge pull request #30126 from mwielgus/federated_updater
Automatic merge from submit-queue Federation - common libs - FedratedUpdater A helper for executing multiple add/update/del operations on federation clusters. Contains a workaround against missing #28921. cc @nikhiljindal @wojtek-t @madhusudancs @kubernetes/sig-cluster-federation Fixes: #29869 #30030 Ref: #29347 <!-- Reviewable:start --> --- This change is [<img src="https://reviewable.kubernetes.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.kubernetes.io/reviews/kubernetes/kubernetes/30126) <!-- Reviewable:end -->pull/6/head
commit
489b204b07
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
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"
|
||||||
|
|
||||||
|
"k8s.io/kubernetes/pkg/api/meta"
|
||||||
|
pkg_runtime "k8s.io/kubernetes/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
//TODO: This will be removed once cluster name field is added to ObjectMeta.
|
||||||
|
ClusterNameAnnotation = "federation.io/name"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: This will be refactored once cluster name field is added to ObjectMeta.
|
||||||
|
func GetClusterName(obj pkg_runtime.Object) (string, error) {
|
||||||
|
accessor, err := meta.Accessor(obj)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
annotations := accessor.GetAnnotations()
|
||||||
|
if annotations != nil {
|
||||||
|
if value, found := annotations[ClusterNameAnnotation]; found {
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("Cluster information not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This will be removed once cluster name field is added to ObjectMeta.
|
||||||
|
func SetClusterName(obj pkg_runtime.Object, clusterName string) error {
|
||||||
|
accessor, err := meta.Accessor(obj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
annotations := accessor.GetAnnotations()
|
||||||
|
if annotations == nil {
|
||||||
|
annotations = make(map[string]string)
|
||||||
|
accessor.SetAnnotations(annotations)
|
||||||
|
}
|
||||||
|
annotations[ClusterNameAnnotation] = clusterName
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
Copyright 2016 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
api_v1 "k8s.io/kubernetes/pkg/api/v1"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetClusterName(t *testing.T) {
|
||||||
|
// There is a single service ns1/s1 in cluster mycluster.
|
||||||
|
service := api_v1.Service{
|
||||||
|
ObjectMeta: api_v1.ObjectMeta{
|
||||||
|
Namespace: "ns1",
|
||||||
|
Name: "s1",
|
||||||
|
Annotations: map[string]string{
|
||||||
|
ClusterNameAnnotation: "mycluster",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
name, err := GetClusterName(&service)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "mycluster", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetClusterName(t *testing.T) {
|
||||||
|
// There is a single service ns1/s1 in cluster mycluster.
|
||||||
|
service := api_v1.Service{
|
||||||
|
ObjectMeta: api_v1.ObjectMeta{
|
||||||
|
Namespace: "ns1",
|
||||||
|
Name: "s1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := SetClusterName(&service, "mytestname")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
clusterName := service.Annotations[ClusterNameAnnotation]
|
||||||
|
assert.Equal(t, "mytestname", clusterName)
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
/*
|
||||||
|
Copyright 2016 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
federation_release_1_4 "k8s.io/kubernetes/federation/client/clientset_generated/federation_release_1_4"
|
||||||
|
pkg_runtime "k8s.io/kubernetes/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Type of the operation that can be executed in Federated.
|
||||||
|
type FederatedOperationType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
OperationTypeAdd = "add"
|
||||||
|
OperationTypeUpdate = "update"
|
||||||
|
OperationTypeDelete = "delete"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FederatedOperation definition contains type (add/update/delete) and the object itself.
|
||||||
|
type FederatedOperation struct {
|
||||||
|
Type FederatedOperationType
|
||||||
|
Obj pkg_runtime.Object
|
||||||
|
}
|
||||||
|
|
||||||
|
// A helper that executes the given set of updates on federation, in parallel.
|
||||||
|
type FederatedUpdater interface {
|
||||||
|
// Executes the given set of operations within the specified timeout.
|
||||||
|
// Timeout is best-effort. There is no guarantee that the underlying operations are
|
||||||
|
// stopped when it is reached. However the function will return after the timeout
|
||||||
|
// with a non-nil error.
|
||||||
|
Update([]FederatedOperation, time.Duration) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// A function that executes some operation using the passed client and object.
|
||||||
|
type FederatedOperationHandler func(federation_release_1_4.Interface, pkg_runtime.Object) error
|
||||||
|
|
||||||
|
type federatedUpdaterImpl struct {
|
||||||
|
federation FederationView
|
||||||
|
|
||||||
|
addFunction FederatedOperationHandler
|
||||||
|
updateFunction FederatedOperationHandler
|
||||||
|
deleteFunction FederatedOperationHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFederatedUpdater(federation FederationView, add, update, del FederatedOperationHandler) FederatedUpdater {
|
||||||
|
return &federatedUpdaterImpl{
|
||||||
|
federation: federation,
|
||||||
|
addFunction: add,
|
||||||
|
updateFunction: update,
|
||||||
|
deleteFunction: del,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fu *federatedUpdaterImpl) Update(ops []FederatedOperation, timeout time.Duration) error {
|
||||||
|
done := make(chan error, len(ops))
|
||||||
|
for _, op := range ops {
|
||||||
|
go func(op FederatedOperation) {
|
||||||
|
clusterName, err := GetClusterName(op.Obj)
|
||||||
|
if err != nil {
|
||||||
|
done <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Ensure that the clientset has reasonable timeout.
|
||||||
|
clientset, err := fu.federation.GetClientsetForCluster(clusterName)
|
||||||
|
if err != nil {
|
||||||
|
done <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch op.Type {
|
||||||
|
case OperationTypeAdd:
|
||||||
|
err = fu.addFunction(clientset, op.Obj)
|
||||||
|
case OperationTypeUpdate:
|
||||||
|
err = fu.updateFunction(clientset, op.Obj)
|
||||||
|
case OperationTypeDelete:
|
||||||
|
err = fu.deleteFunction(clientset, op.Obj)
|
||||||
|
}
|
||||||
|
done <- err
|
||||||
|
}(op)
|
||||||
|
}
|
||||||
|
start := time.Now()
|
||||||
|
for i := 0; i < len(ops); i++ {
|
||||||
|
now := time.Now()
|
||||||
|
if !now.Before(start.Add(timeout)) {
|
||||||
|
return fmt.Errorf("failed to finish all operations in %v", timeout)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case <-time.After(start.Add(timeout).Sub(now)):
|
||||||
|
return fmt.Errorf("failed to finish all operations in %v", timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// All operations finished in time.
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,144 @@
|
||||||
|
/*
|
||||||
|
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"
|
||||||
|
|
||||||
|
federation_api "k8s.io/kubernetes/federation/apis/federation/v1beta1"
|
||||||
|
federation_release_1_4 "k8s.io/kubernetes/federation/client/clientset_generated/federation_release_1_4"
|
||||||
|
fake_federation_release_1_4 "k8s.io/kubernetes/federation/client/clientset_generated/federation_release_1_4/fake"
|
||||||
|
api_v1 "k8s.io/kubernetes/pkg/api/v1"
|
||||||
|
pkg_runtime "k8s.io/kubernetes/pkg/runtime"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Fake federation view.
|
||||||
|
type fakeFederationView struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeFederationView) GetClientsetForCluster(clusterName string) (federation_release_1_4.Interface, error) {
|
||||||
|
return &fake_federation_release_1_4.Clientset{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeFederationView) GetReadyClusters() ([]*federation_api.Cluster, error) {
|
||||||
|
return []*federation_api.Cluster{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeFederationView) GetReadyCluster(name string) (*federation_api.Cluster, bool, error) {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeFederationView) ClustersSynced() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFederatedUpdaterOK(t *testing.T) {
|
||||||
|
addChan := make(chan string, 5)
|
||||||
|
updateChan := make(chan string, 5)
|
||||||
|
|
||||||
|
updater := NewFederatedUpdater(&fakeFederationView{},
|
||||||
|
func(_ federation_release_1_4.Interface, obj pkg_runtime.Object) error {
|
||||||
|
clusterName, _ := GetClusterName(obj)
|
||||||
|
addChan <- clusterName
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
func(_ federation_release_1_4.Interface, obj pkg_runtime.Object) error {
|
||||||
|
clusterName, _ := GetClusterName(obj)
|
||||||
|
updateChan <- clusterName
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
noop)
|
||||||
|
|
||||||
|
err := updater.Update([]FederatedOperation{
|
||||||
|
{
|
||||||
|
Type: OperationTypeAdd,
|
||||||
|
Obj: makeService("A", "s1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: OperationTypeUpdate,
|
||||||
|
Obj: makeService("B", "s1"),
|
||||||
|
},
|
||||||
|
}, time.Minute)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
add := <-addChan
|
||||||
|
update := <-updateChan
|
||||||
|
assert.Equal(t, "A", add)
|
||||||
|
assert.Equal(t, "B", update)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFederatedUpdaterError(t *testing.T) {
|
||||||
|
updater := NewFederatedUpdater(&fakeFederationView{},
|
||||||
|
func(_ federation_release_1_4.Interface, obj pkg_runtime.Object) error {
|
||||||
|
return fmt.Errorf("boom")
|
||||||
|
}, noop, noop)
|
||||||
|
|
||||||
|
err := updater.Update([]FederatedOperation{
|
||||||
|
{
|
||||||
|
Type: OperationTypeAdd,
|
||||||
|
Obj: makeService("A", "s1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: OperationTypeUpdate,
|
||||||
|
Obj: makeService("B", "s1"),
|
||||||
|
},
|
||||||
|
}, time.Minute)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFederatedUpdaterTimeout(t *testing.T) {
|
||||||
|
start := time.Now()
|
||||||
|
updater := NewFederatedUpdater(&fakeFederationView{},
|
||||||
|
func(_ federation_release_1_4.Interface, obj pkg_runtime.Object) error {
|
||||||
|
time.Sleep(time.Minute)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
noop, noop)
|
||||||
|
|
||||||
|
err := updater.Update([]FederatedOperation{
|
||||||
|
{
|
||||||
|
Type: OperationTypeAdd,
|
||||||
|
Obj: makeService("A", "s1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: OperationTypeUpdate,
|
||||||
|
Obj: makeService("B", "s1"),
|
||||||
|
},
|
||||||
|
}, time.Second)
|
||||||
|
end := time.Now()
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.True(t, start.Add(10*time.Second).After(end))
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeService(cluster, name string) *api_v1.Service {
|
||||||
|
return &api_v1.Service{
|
||||||
|
ObjectMeta: api_v1.ObjectMeta{
|
||||||
|
Namespace: "ns1",
|
||||||
|
Name: name,
|
||||||
|
Annotations: map[string]string{
|
||||||
|
ClusterNameAnnotation: cluster,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func noop(_ federation_release_1_4.Interface, _ pkg_runtime.Object) error {
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in New Issue