Commit 428b8a41 authored by Bowei Du's avatar Bowei Du

(ALPHA GCP FEATURE) Add IPAM controller

IPAM controller unifies handling of node pod CIDR range allocation. It is intended to supersede the logic that is currently in range_allocator and cloud_cidr_allocator. Note: for this change, the other allocators still exist and are the default. It supports two modes: * CIDR range allocations done within the cluster that are then propagated out to the cloud provider. * Cloud provider managed IPAM that is then reflected into the cluster.
parent 44c51821
......@@ -174,7 +174,6 @@ pkg/controller/garbagecollector/metaonly
pkg/controller/job
pkg/controller/namespace
pkg/controller/namespace/deletion
pkg/controller/node
pkg/controller/podautoscaler
pkg/controller/podautoscaler/metrics
pkg/controller/podgc
......
......@@ -51,6 +51,7 @@ go_library(
"//pkg/cloudprovider:go_default_library",
"//pkg/controller:go_default_library",
"//pkg/controller/node/ipam:go_default_library",
"//pkg/controller/node/ipam/sync:go_default_library",
"//pkg/controller/node/scheduler:go_default_library",
"//pkg/controller/node/util:go_default_library",
"//pkg/util/metrics:go_default_library",
......@@ -65,7 +66,6 @@ go_library(
"//vendor/k8s.io/apimachinery/pkg/api/equality: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/fields:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/labels:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
......
......@@ -8,9 +8,15 @@ load(
go_test(
name = "go_default_test",
srcs = ["cidr_allocator_test.go"],
srcs = [
"controller_test.go",
"range_allocator_test.go",
"timeout_test.go",
],
library = ":go_default_library",
deps = [
"//pkg/controller/node/ipam/cidrset:go_default_library",
"//pkg/controller/node/ipam/test:go_default_library",
"//pkg/controller/testutil:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
......@@ -22,28 +28,38 @@ go_test(
go_library(
name = "go_default_library",
srcs = [
"adapter.go",
"cidr_allocator.go",
"cloud_cidr_allocator.go",
"controller.go",
"doc.go",
"range_allocator.go",
"timeout.go",
],
deps = [
"//pkg/api:go_default_library",
"//pkg/cloudprovider:go_default_library",
"//pkg/cloudprovider/providers/gce:go_default_library",
"//pkg/controller/node/ipam/cidrset:go_default_library",
"//pkg/controller/node/ipam/sync:go_default_library",
"//pkg/controller/node/util:go_default_library",
"//pkg/util/node:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/api/core/v1: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/fields:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/labels:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/k8s.io/client-go/informers/core/v1:go_default_library",
"//vendor/k8s.io/client-go/kubernetes:go_default_library",
"//vendor/k8s.io/client-go/kubernetes/scheme:go_default_library",
"//vendor/k8s.io/client-go/kubernetes/typed/core/v1:go_default_library",
"//vendor/k8s.io/client-go/tools/cache:go_default_library",
"//vendor/k8s.io/client-go/tools/record:go_default_library",
"//vendor/k8s.io/metrics/pkg/client/clientset_generated/clientset/scheme:go_default_library",
],
)
......@@ -59,6 +75,8 @@ filegroup(
srcs = [
":package-srcs",
"//pkg/controller/node/ipam/cidrset:all-srcs",
"//pkg/controller/node/ipam/sync:all-srcs",
"//pkg/controller/node/ipam/test:all-srcs",
],
tags = ["automanaged"],
)
/*
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 ipam
import (
"context"
"encoding/json"
"net"
"github.com/golang/glog"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
clientset "k8s.io/client-go/kubernetes"
v1core "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/tools/record"
"k8s.io/kubernetes/pkg/cloudprovider/providers/gce"
nodeutil "k8s.io/kubernetes/pkg/util/node"
"k8s.io/metrics/pkg/client/clientset_generated/clientset/scheme"
)
type adapter struct {
k8s clientset.Interface
cloud *gce.GCECloud
recorder record.EventRecorder
}
func newAdapter(k8s clientset.Interface, cloud *gce.GCECloud) *adapter {
ret := &adapter{
k8s: k8s,
cloud: cloud,
}
broadcaster := record.NewBroadcaster()
broadcaster.StartLogging(glog.Infof)
ret.recorder = broadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: "cloudCIDRAllocator"})
glog.V(0).Infof("Sending events to api server.")
broadcaster.StartRecordingToSink(&v1core.EventSinkImpl{
Interface: v1core.New(k8s.Core().RESTClient()).Events(""),
})
return ret
}
func (a *adapter) Alias(ctx context.Context, nodeName string) (*net.IPNet, error) {
cidrs, err := a.cloud.AliasRanges(types.NodeName(nodeName))
if err != nil {
return nil, err
}
switch len(cidrs) {
case 0:
return nil, nil
case 1:
break
default:
glog.Warningf("Node %q has more than one alias assigned (%v), defaulting to the first", nodeName, cidrs)
}
_, cidrRange, err := net.ParseCIDR(cidrs[0])
if err != nil {
return nil, err
}
return cidrRange, nil
}
func (a *adapter) AddAlias(ctx context.Context, nodeName string, cidrRange *net.IPNet) error {
return a.cloud.AddAliasToInstance(types.NodeName(nodeName), cidrRange)
}
func (a *adapter) Node(ctx context.Context, name string) (*v1.Node, error) {
return a.k8s.Core().Nodes().Get(name, metav1.GetOptions{})
}
func (a *adapter) UpdateNodePodCIDR(ctx context.Context, node *v1.Node, cidrRange *net.IPNet) error {
patch := map[string]interface{}{
"apiVersion": node.APIVersion,
"kind": node.Kind,
"metadata": map[string]interface{}{"name": node.Name},
"spec": map[string]interface{}{"podCIDR": cidrRange.String()},
}
bytes, err := json.Marshal(patch)
if err != nil {
return err
}
_, err = a.k8s.Core().Nodes().Patch(node.Name, types.StrategicMergePatchType, bytes)
return err
}
func (a *adapter) UpdateNodeNetworkUnavailable(nodeName string, unavailable bool) error {
condition := v1.ConditionFalse
if unavailable {
condition = v1.ConditionTrue
}
return nodeutil.SetNodeCondition(a.k8s, types.NodeName(nodeName), v1.NodeCondition{
Type: v1.NodeNetworkUnavailable,
Status: condition,
Reason: "RouteCreated",
Message: "NodeController created an implicit route",
LastTransitionTime: metav1.Now(),
})
}
func (a *adapter) EmitNodeWarningEvent(nodeName, reason, fmt string, args ...interface{}) {
ref := &v1.ObjectReference{Kind: "Node", Name: nodeName}
a.recorder.Eventf(ref, v1.EventTypeNormal, reason, fmt, args...)
}
......@@ -17,9 +17,20 @@ limitations under the License.
package ipam
import (
"fmt"
"net"
"time"
v1 "k8s.io/api/core/v1"
"github.com/golang/glog"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/wait"
informers "k8s.io/client-go/informers/core/v1"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/kubernetes/pkg/cloudprovider"
)
type nodeAndCIDR struct {
......@@ -37,10 +48,19 @@ const (
// CloudAllocatorType is the allocator that uses cloud platform
// support to do node CIDR range allocations.
CloudAllocatorType CIDRAllocatorType = "CloudAllocator"
// IPAMFromClusterAllocatorType uses the ipam controller sync'ing the node
// CIDR range allocations from the cluster to the cloud.
IPAMFromClusterAllocatorType = "IPAMFromCluster"
// IPAMFromCloudAllocatorType uses the ipam controller sync'ing the node
// CIDR range allocations from the cloud to the cluster.
IPAMFromCloudAllocatorType = "IPAMFromCloud"
// The amount of time the nodecontroller polls on the list nodes endpoint.
apiserverStartupGracePeriod = 10 * time.Minute
)
// CIDRAllocator is an interface implemented by things that know how to
// allocate/occupy/recycle CIDR for nodes.
// CIDRAllocator is an interface implemented by things that know how
// to allocate/occupy/recycle CIDR for nodes.
type CIDRAllocator interface {
// AllocateOrOccupyCIDR looks at the given node, assigns it a valid
// CIDR if it doesn't currently have one or mark the CIDR as used if
......@@ -48,4 +68,45 @@ type CIDRAllocator interface {
AllocateOrOccupyCIDR(node *v1.Node) error
// ReleaseCIDR releases the CIDR of the removed node
ReleaseCIDR(node *v1.Node) error
// Register allocator with the nodeInformer for updates.
Register(nodeInformer informers.NodeInformer)
}
// New creates a new CIDR range allocator.
func New(kubeClient clientset.Interface, cloud cloudprovider.Interface, allocatorType CIDRAllocatorType, clusterCIDR, serviceCIDR *net.IPNet, nodeCIDRMaskSize int) (CIDRAllocator, error) {
nodeList, err := listNodes(kubeClient)
if err != nil {
return nil, err
}
switch allocatorType {
case RangeAllocatorType:
return NewCIDRRangeAllocator(kubeClient, clusterCIDR, serviceCIDR, nodeCIDRMaskSize, nodeList)
case CloudAllocatorType:
return NewCloudCIDRAllocator(kubeClient, cloud)
default:
return nil, fmt.Errorf("Invalid CIDR allocator type: %v", allocatorType)
}
}
func listNodes(kubeClient clientset.Interface) (*v1.NodeList, error) {
var nodeList *v1.NodeList
// We must poll because apiserver might not be up. This error causes
// controller manager to restart.
if pollErr := wait.Poll(10*time.Second, apiserverStartupGracePeriod, func() (bool, error) {
var err error
nodeList, err = kubeClient.Core().Nodes().List(metav1.ListOptions{
FieldSelector: fields.Everything().String(),
LabelSelector: labels.Everything().String(),
})
if err != nil {
glog.Errorf("Failed to list all nodes: %v", err)
return false, nil
}
return true, nil
}); pollErr != nil {
return nil, fmt.Errorf("Failed to list all nodes in %v, cannot proceed without updating CIDR map",
apiserverStartupGracePeriod)
}
return nodeList, nil
}
......@@ -102,7 +102,8 @@ func (s *CidrSet) indexToCIDRBlock(index int) *net.IPNet {
}
}
// AllocateNext allocates the next free CIDR range.
// AllocateNext allocates the next free CIDR range. This will set the range
// as occupied and return the allocated range.
func (s *CidrSet) AllocateNext() (*net.IPNet, error) {
s.Lock()
defer s.Unlock()
......@@ -186,7 +187,8 @@ func (s *CidrSet) Release(cidr *net.IPNet) error {
return nil
}
// Occupy marks the given CIDR range as used.
// Occupy marks the given CIDR range as used. Occupy does not check if the CIDR
// range was previously used.
func (s *CidrSet) Occupy(cidr *net.IPNet) (err error) {
begin, end, err := s.getBeginingAndEndIndices(cidr)
if err != nil {
......@@ -203,21 +205,24 @@ func (s *CidrSet) Occupy(cidr *net.IPNet) (err error) {
}
func (s *CidrSet) getIndexForCIDR(cidr *net.IPNet) (int, error) {
var cidrIndex uint32
if cidr.IP.To4() != nil {
cidrIndex = (binary.BigEndian.Uint32(s.clusterIP) ^ binary.BigEndian.Uint32(cidr.IP.To4())) >> uint32(32-s.subNetMaskSize)
return s.getIndexForIP(cidr.IP)
}
func (s *CidrSet) getIndexForIP(ip net.IP) (int, error) {
if ip.To4() != nil {
cidrIndex := (binary.BigEndian.Uint32(s.clusterIP) ^ binary.BigEndian.Uint32(ip.To4())) >> uint32(32-s.subNetMaskSize)
if cidrIndex >= uint32(s.maxCIDRs) {
return 0, fmt.Errorf("CIDR: %v is out of the range of CIDR allocator", cidr)
return 0, fmt.Errorf("CIDR: %v/%v is out of the range of CIDR allocator", ip, s.subNetMaskSize)
}
} else if cidr.IP.To16() != nil {
cidrIndex64 := (binary.BigEndian.Uint64(s.clusterIP) ^ binary.BigEndian.Uint64(cidr.IP.To16())) >> uint64(64-s.subNetMaskSize)
if cidrIndex64 >= uint64(s.maxCIDRs) {
return 0, fmt.Errorf("CIDR: %v is out of the range of CIDR allocator", cidr)
return int(cidrIndex), nil
}
if ip.To16() != nil {
cidrIndex := (binary.BigEndian.Uint64(s.clusterIP) ^ binary.BigEndian.Uint64(ip.To16())) >> uint64(64-s.subNetMaskSize)
if cidrIndex >= uint64(s.maxCIDRs) {
return 0, fmt.Errorf("CIDR: %v/%v is out of the range of CIDR allocator", ip, s.subNetMaskSize)
}
cidrIndex = uint32(cidrIndex64)
} else {
return 0, fmt.Errorf("invalid CIDR block: %v", cidr)
return int(cidrIndex), nil
}
return int(cidrIndex), nil
return 0, fmt.Errorf("invalid IP: %v", ip)
}
......@@ -24,7 +24,8 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
informers "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/record"
"k8s.io/api/core/v1"
......@@ -142,3 +143,16 @@ func (ca *cloudCIDRAllocator) ReleaseCIDR(node *v1.Node) error {
node.Name, node.Spec.PodCIDR)
return nil
}
func (ca *cloudCIDRAllocator) Register(nodeInformer informers.NodeInformer) {
nodeInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: util.CreateAddNodeHandler(ca.AllocateOrOccupyCIDR),
UpdateFunc: util.CreateUpdateNodeHandler(func(_, newNode *v1.Node) error {
if newNode.Spec.PodCIDR == "" {
return ca.AllocateOrOccupyCIDR(newNode)
}
return nil
}),
DeleteFunc: util.CreateDeleteNodeHandler(ca.ReleaseCIDR),
})
}
/*
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 ipam
import (
"fmt"
"net"
"sync"
"time"
"github.com/golang/glog"
"k8s.io/api/core/v1"
informers "k8s.io/client-go/informers/core/v1"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"
"k8s.io/kubernetes/pkg/cloudprovider"
"k8s.io/kubernetes/pkg/cloudprovider/providers/gce"
"k8s.io/kubernetes/pkg/controller/node/ipam/cidrset"
nodesync "k8s.io/kubernetes/pkg/controller/node/ipam/sync"
"k8s.io/kubernetes/pkg/controller/node/util"
)
// Config for the IPAM controller.
type Config struct {
// Resync is the default timeout duration when there are no errors.
Resync time.Duration
// MaxBackoff is the maximum timeout when in a error backoff state.
MaxBackoff time.Duration
// InitialRetry is the initial retry interval when an error is reported.
InitialRetry time.Duration
// Mode to use to synchronize.
Mode nodesync.NodeSyncMode
}
// Controller is the controller for synchronizing cluster and cloud node
// pod CIDR range assignments.
type Controller struct {
config *Config
adapter *adapter
lock sync.Mutex
syncers map[string]*nodesync.NodeSync
set *cidrset.CidrSet
}
// NewController returns a new instance of the IPAM controller.
func NewController(
config *Config,
kubeClient clientset.Interface,
cloud cloudprovider.Interface,
clusterCIDR, serviceCIDR *net.IPNet,
nodeCIDRMaskSize int) (*Controller, error) {
if !nodesync.IsValidMode(config.Mode) {
return nil, fmt.Errorf("invalid IPAM controller mode %q", config.Mode)
}
gceCloud, ok := cloud.(*gce.GCECloud)
if !ok {
return nil, fmt.Errorf("cloud IPAM controller does not support %q provider", cloud.ProviderName())
}
c := &Controller{
config: config,
adapter: newAdapter(kubeClient, gceCloud),
syncers: make(map[string]*nodesync.NodeSync),
set: cidrset.NewCIDRSet(clusterCIDR, nodeCIDRMaskSize),
}
if err := occupyServiceCIDR(c.set, clusterCIDR, serviceCIDR); err != nil {
return nil, err
}
return c, nil
}
// Start initializes the Controller with the existing list of nodes and
// registers the informers for node chnages. This will start synchronization
// of the node and cloud CIDR range allocations.
func (c *Controller) Start(nodeInformer informers.NodeInformer) error {
glog.V(0).Infof("Starting IPAM controller (config=%+v)", c.config)
nodes, err := listNodes(c.adapter.k8s)
if err != nil {
return err
}
for _, node := range nodes.Items {
if node.Spec.PodCIDR != "" {
_, cidrRange, err := net.ParseCIDR(node.Spec.PodCIDR)
if err == nil {
c.set.Occupy(cidrRange)
glog.V(3).Infof("Occupying CIDR for node %q (%v)", node.Name, node.Spec.PodCIDR)
} else {
glog.Errorf("Node %q has an invalid CIDR (%q): %v", node.Name, node.Spec.PodCIDR, err)
}
}
func() {
c.lock.Lock()
defer c.lock.Unlock()
// XXX/bowei -- stagger the start of each sync cycle.
syncer := c.newSyncer(node.Name)
c.syncers[node.Name] = syncer
go syncer.Loop(nil)
}()
}
nodeInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: util.CreateAddNodeHandler(c.onAdd),
UpdateFunc: util.CreateUpdateNodeHandler(c.onUpdate),
DeleteFunc: util.CreateDeleteNodeHandler(c.onDelete),
})
return nil
}
// occupyServiceCIDR removes the service CIDR range from the cluster CIDR if it
// intersects.
func occupyServiceCIDR(set *cidrset.CidrSet, clusterCIDR, serviceCIDR *net.IPNet) error {
if clusterCIDR.Contains(serviceCIDR.IP) || serviceCIDR.Contains(clusterCIDR.IP) {
if err := set.Occupy(serviceCIDR); err != nil {
return err
}
}
return nil
}
type nodeState struct {
t Timeout
}
func (ns *nodeState) ReportResult(err error) {
ns.t.Update(err == nil)
}
func (ns *nodeState) ResyncTimeout() time.Duration {
return ns.t.Next()
}
func (c *Controller) newSyncer(name string) *nodesync.NodeSync {
ns := &nodeState{
Timeout{
Resync: c.config.Resync,
MaxBackoff: c.config.MaxBackoff,
InitialRetry: c.config.InitialRetry,
},
}
return nodesync.New(ns, c.adapter, c.adapter, c.config.Mode, name, c.set)
}
func (c *Controller) onAdd(node *v1.Node) error {
c.lock.Lock()
defer c.lock.Unlock()
if syncer, ok := c.syncers[node.Name]; !ok {
syncer = c.newSyncer(node.Name)
c.syncers[node.Name] = syncer
go syncer.Loop(nil)
} else {
glog.Warningf("Add for node %q that already exists", node.Name)
syncer.Update(node)
}
return nil
}
func (c *Controller) onUpdate(_, node *v1.Node) error {
c.lock.Lock()
defer c.lock.Unlock()
if sync, ok := c.syncers[node.Name]; ok {
sync.Update(node)
} else {
glog.Errorf("Received update for non-existant node %q", node.Name)
return fmt.Errorf("unknown node %q", node.Name)
}
return nil
}
func (c *Controller) onDelete(node *v1.Node) error {
c.lock.Lock()
defer c.lock.Unlock()
if syncer, ok := c.syncers[node.Name]; ok {
syncer.Delete(node)
delete(c.syncers, node.Name)
} else {
glog.Warning("Node %q was already deleted", node.Name)
}
return nil
}
/*
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 ipam
import (
"net"
"testing"
"k8s.io/kubernetes/pkg/controller/node/ipam/cidrset"
"k8s.io/kubernetes/pkg/controller/node/ipam/test"
)
func TestOccupyServiceCIDR(t *testing.T) {
const clusterCIDR = "10.1.0.0/16"
TestCase:
for _, tc := range []struct {
serviceCIDR string
}{
{"10.0.255.0/24"},
{"10.1.0.0/24"},
{"10.1.255.0/24"},
{"10.2.0.0/24"},
} {
serviceCIDR := test.MustParseCIDR(tc.serviceCIDR)
set := cidrset.NewCIDRSet(test.MustParseCIDR(clusterCIDR), 24)
if err := occupyServiceCIDR(set, test.MustParseCIDR(clusterCIDR), serviceCIDR); err != nil {
t.Errorf("test case %+v: occupyServiceCIDR() = %v, want nil", tc, err)
}
// Allocate until full.
var cidrs []*net.IPNet
for {
cidr, err := set.AllocateNext()
if err != nil {
if err == cidrset.ErrCIDRRangeNoCIDRsRemaining {
break
}
t.Errorf("set.AllocateNext() = %v, want %v", err, cidrset.ErrCIDRRangeNoCIDRsRemaining)
continue TestCase
}
cidrs = append(cidrs, cidr)
}
// No allocated CIDR range should intersect with serviceCIDR.
for _, c := range cidrs {
if c.Contains(serviceCIDR.IP) || serviceCIDR.Contains(c.IP) {
t.Errorf("test case %+v: allocated CIDR %v from service range", tc, c)
}
}
}
}
/*
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 ipam provides different allocators for assigning IP ranges to nodes.
// We currently support several kinds of IPAM allocators (these are denoted by
// the CIDRAllocatorType):
// - RangeAllocator is an allocator that assigns PodCIDRs to nodes and works
// in conjunction with the RouteController to configure the network to get
// connectivity.
// - CloudAllocator is an allocator that synchronizes PodCIDRs from IP
// ranges assignments from the underlying cloud platform.
// - (Alpha only) IPAMFromCluster is an allocator that has the similar
// functionality as the RangeAllocator but also synchronizes cluster-managed
// ranges into the cloud platform.
// - (Alpha only) IPAMFromCloud is the same as CloudAllocator (synchronizes
// from cloud into the cluster.)
package ipam
......@@ -28,9 +28,11 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait"
informers "k8s.io/client-go/informers/core/v1"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
v1core "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/record"
"k8s.io/kubernetes/pkg/controller/node/ipam/cidrset"
......@@ -264,3 +266,35 @@ func (r *rangeAllocator) updateCIDRAllocation(data nodeAndCIDR) error {
}
return err
}
func (r *rangeAllocator) Register(nodeInformer informers.NodeInformer) {
nodeInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: util.CreateAddNodeHandler(r.AllocateOrOccupyCIDR),
UpdateFunc: util.CreateUpdateNodeHandler(func(_, newNode *v1.Node) error {
// If the PodCIDR is not empty we either:
// - already processed a Node that already had a CIDR after NC restarted
// (cidr is marked as used),
// - already processed a Node successfully and allocated a CIDR for it
// (cidr is marked as used),
// - already processed a Node but we did saw a "timeout" response and
// request eventually got through in this case we haven't released
// the allocated CIDR (cidr is still marked as used).
// There's a possible error here:
// - NC sees a new Node and assigns a CIDR X to it,
// - Update Node call fails with a timeout,
// - Node is updated by some other component, NC sees an update and
// assigns CIDR Y to the Node,
// - Both CIDR X and CIDR Y are marked as used in the local cache,
// even though Node sees only CIDR Y
// The problem here is that in in-memory cache we see CIDR X as marked,
// which prevents it from being assigned to any new node. The cluster
// state is correct.
// Restart of NC fixes the issue.
if newNode.Spec.PodCIDR == "" {
return r.AllocateOrOccupyCIDR(newNode)
}
return nil
}),
DeleteFunc: util.CreateDeleteNodeHandler(r.ReleaseCIDR),
})
}
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = ["sync.go"],
visibility = ["//visibility:public"],
deps = [
"//pkg/controller/node/ipam/cidrset:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["sync_test.go"],
library = ":go_default_library",
deps = [
"//pkg/controller/node/ipam/cidrset:go_default_library",
"//pkg/controller/node/ipam/test:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/api/core/v1: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"],
)
/*
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 sync
import (
"context"
"fmt"
"net"
"reflect"
"testing"
"time"
"github.com/golang/glog"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/kubernetes/pkg/controller/node/ipam/cidrset"
"k8s.io/kubernetes/pkg/controller/node/ipam/test"
"k8s.io/api/core/v1"
)
var (
_, clusterCIDRRange, _ = net.ParseCIDR("10.1.0.0/16")
)
type fakeEvent struct {
nodeName string
reason string
}
type fakeAPIs struct {
aliasRange *net.IPNet
aliasErr error
addAliasErr error
nodeRet *v1.Node
nodeErr error
updateNodeErr error
resyncTimeout time.Duration
reportChan chan struct{}
updateNodeNetworkUnavailableErr error
calls []string
events []fakeEvent
results []error
}
func (f *fakeAPIs) Alias(ctx context.Context, nodeName string) (*net.IPNet, error) {
f.calls = append(f.calls, fmt.Sprintf("alias %v", nodeName))
return f.aliasRange, f.aliasErr
}
func (f *fakeAPIs) AddAlias(ctx context.Context, nodeName string, cidrRange *net.IPNet) error {
f.calls = append(f.calls, fmt.Sprintf("addAlias %v %v", nodeName, cidrRange))
return f.addAliasErr
}
func (f *fakeAPIs) Node(ctx context.Context, name string) (*v1.Node, error) {
f.calls = append(f.calls, fmt.Sprintf("node %v", name))
return f.nodeRet, f.nodeErr
}
func (f *fakeAPIs) UpdateNodePodCIDR(ctx context.Context, node *v1.Node, cidrRange *net.IPNet) error {
f.calls = append(f.calls, fmt.Sprintf("updateNode %v", node))
return f.updateNodeErr
}
func (f *fakeAPIs) UpdateNodeNetworkUnavailable(nodeName string, unavailable bool) error {
f.calls = append(f.calls, fmt.Sprintf("updateNodeNetworkUnavailable %v %v", nodeName, unavailable))
return f.updateNodeNetworkUnavailableErr
}
func (f *fakeAPIs) EmitNodeWarningEvent(nodeName, reason, fmtStr string, args ...interface{}) {
f.events = append(f.events, fakeEvent{nodeName, reason})
}
func (f *fakeAPIs) ReportResult(err error) {
glog.V(2).Infof("ReportResult %v", err)
f.results = append(f.results, err)
if f.reportChan != nil {
f.reportChan <- struct{}{}
}
}
func (f *fakeAPIs) ResyncTimeout() time.Duration {
if f.resyncTimeout == 0 {
return time.Second * 10000
}
return f.resyncTimeout
}
func (f *fakeAPIs) dumpTrace() {
for i, x := range f.calls {
glog.Infof("trace %v: %v", i, x)
}
}
var nodeWithoutCIDRRange = &v1.Node{
ObjectMeta: metav1.ObjectMeta{Name: "node1"},
}
var nodeWithCIDRRange = &v1.Node{
ObjectMeta: metav1.ObjectMeta{Name: "node1"},
Spec: v1.NodeSpec{PodCIDR: "10.1.1.0/24"},
}
func TestNodeSyncUpdate(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
desc string
mode NodeSyncMode
node *v1.Node
fake fakeAPIs
events []fakeEvent
wantError bool
}{
{
desc: "validate range ==",
mode: SyncFromCloud,
node: nodeWithCIDRRange,
fake: fakeAPIs{
aliasRange: test.MustParseCIDR(nodeWithCIDRRange.Spec.PodCIDR),
},
},
{
desc: "validate range !=",
mode: SyncFromCloud,
node: nodeWithCIDRRange,
fake: fakeAPIs{aliasRange: test.MustParseCIDR("192.168.0.0/24")},
events: []fakeEvent{{"node1", "CloudCIDRAllocatorMismatch"}},
},
{
desc: "update alias from node",
mode: SyncFromCloud,
node: nodeWithCIDRRange,
events: []fakeEvent{{"node1", "CloudCIDRAllocatorInvalidMode"}},
wantError: true,
},
{
desc: "update alias from node",
mode: SyncFromCluster,
node: nodeWithCIDRRange,
// XXX/bowei -- validation
},
{
desc: "update node from alias",
mode: SyncFromCloud,
node: nodeWithoutCIDRRange,
fake: fakeAPIs{aliasRange: test.MustParseCIDR("10.1.2.3/16")},
// XXX/bowei -- validation
},
{
desc: "update node from alias",
mode: SyncFromCluster,
node: nodeWithoutCIDRRange,
fake: fakeAPIs{aliasRange: test.MustParseCIDR("10.1.2.3/16")},
events: []fakeEvent{{"node1", "CloudCIDRAllocatorInvalidMode"}},
wantError: true,
},
{
desc: "allocate range",
mode: SyncFromCloud,
node: nodeWithoutCIDRRange,
events: []fakeEvent{{"node1", "CloudCIDRAllocatorInvalidMode"}},
wantError: true,
},
{
desc: "allocate range",
mode: SyncFromCluster,
node: nodeWithoutCIDRRange,
},
{
desc: "update with node==nil",
mode: SyncFromCluster,
node: nil,
fake: fakeAPIs{
nodeRet: nodeWithCIDRRange,
},
wantError: false,
},
} {
sync := New(&tc.fake, &tc.fake, &tc.fake, tc.mode, "node1", cidrset.NewCIDRSet(clusterCIDRRange, 24))
doneChan := make(chan struct{})
// Do a single step of the loop.
go sync.Loop(doneChan)
sync.Update(tc.node)
close(sync.opChan)
<-doneChan
tc.fake.dumpTrace()
if !reflect.DeepEqual(tc.fake.events, tc.events) {
t.Errorf("%v, %v; fake.events = %#v, want %#v", tc.desc, tc.mode, tc.fake.events, tc.events)
}
var hasError bool
for _, r := range tc.fake.results {
hasError = hasError || (r != nil)
}
if hasError != tc.wantError {
t.Errorf("%v, %v; hasError = %t, errors = %v, want %t",
tc.desc, tc.mode, hasError, tc.fake.events, tc.wantError)
}
}
}
func TestNodeSyncResync(t *testing.T) {
fake := &fakeAPIs{
nodeRet: nodeWithCIDRRange,
resyncTimeout: time.Millisecond,
reportChan: make(chan struct{}),
}
sync := New(fake, fake, fake, SyncFromCluster, "node1", cidrset.NewCIDRSet(clusterCIDRRange, 24))
doneChan := make(chan struct{})
go sync.Loop(doneChan)
<-fake.reportChan
close(sync.opChan)
// Unblock loop().
go func() {
<-fake.reportChan
}()
<-doneChan
fake.dumpTrace()
}
func TestNodeSyncDelete(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
desc string
mode NodeSyncMode
node *v1.Node
fake fakeAPIs
}{
{
desc: "delete",
mode: SyncFromCluster,
node: nodeWithCIDRRange,
},
{
desc: "delete without CIDR range",
mode: SyncFromCluster,
node: nodeWithoutCIDRRange,
},
{
desc: "delete with invalid CIDR range",
mode: SyncFromCluster,
node: &v1.Node{
ObjectMeta: metav1.ObjectMeta{Name: "node1"},
Spec: v1.NodeSpec{PodCIDR: "invalid"},
},
},
} {
sync := New(&tc.fake, &tc.fake, &tc.fake, tc.mode, "node1", cidrset.NewCIDRSet(clusterCIDRRange, 24))
doneChan := make(chan struct{})
// Do a single step of the loop.
go sync.Loop(doneChan)
sync.Delete(tc.node)
<-doneChan
tc.fake.dumpTrace()
/*
if !reflect.DeepEqual(tc.fake.events, tc.events) {
t.Errorf("%v, %v; fake.events = %#v, want %#v", tc.desc, tc.mode, tc.fake.events, tc.events)
}
var hasError bool
for _, r := range tc.fake.results {
hasError = hasError || (r != nil)
}
if hasError != tc.wantError {
t.Errorf("%v, %v; hasError = %t, errors = %v, want %t",
tc.desc, tc.mode, hasError, tc.fake.events, tc.wantError)
}
*/
}
}
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["utils.go"],
visibility = ["//visibility:public"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
/*
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 test
import (
"net"
)
// MustParseCIDR returns the CIDR range parsed from s or panics if the string
// cannot be parsed.
func MustParseCIDR(s string) *net.IPNet {
_, ret, err := net.ParseCIDR(s)
if err != nil {
panic(err)
}
return ret
}
/*
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 ipam
import (
"time"
)
// Timeout manages the resync loop timing for a given node sync operation. The
// timeout changes depending on whether or not there was an error reported for
// the operation. Consecutive errors will result in exponential backoff to a
// maxBackoff timeout.
type Timeout struct {
// Resync is the default timeout duration when there are no errors.
Resync time.Duration
// MaxBackoff is the maximum timeout when in a error backoff state.
MaxBackoff time.Duration
// InitialRetry is the initial retry interval when an error is reported.
InitialRetry time.Duration
// errs is the count of consecutive errors that have occurred.
errs int
// current is the current backoff timeout.
current time.Duration
}
// Update the timeout with the current error state.
func (b *Timeout) Update(ok bool) {
if ok {
b.errs = 0
b.current = b.Resync
return
}
b.errs++
if b.errs == 1 {
b.current = b.InitialRetry
return
}
b.current *= 2
if b.current >= b.MaxBackoff {
b.current = b.MaxBackoff
}
}
// Next returns the next operation timeout given the disposition of err.
func (b *Timeout) Next() time.Duration {
if b.errs == 0 {
return b.Resync
}
return b.current
}
/*
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 ipam
import (
"errors"
"testing"
"time"
)
func TestTimeout(t *testing.T) {
time10s := 10 * time.Second
time5s := 5 * time.Second
timeout := &Timeout{
Resync: time10s,
MaxBackoff: time5s,
InitialRetry: time.Second,
}
for _, testStep := range []struct {
err error
want time.Duration
}{
{nil, time10s},
{nil, time10s},
{errors.New("x"), time.Second},
{errors.New("x"), 2 * time.Second},
{errors.New("x"), 4 * time.Second},
{errors.New("x"), 5 * time.Second},
{errors.New("x"), 5 * time.Second},
{nil, time10s},
{nil, time10s},
{errors.New("x"), time.Second},
{errors.New("x"), 2 * time.Second},
{nil, time10s},
} {
timeout.Update(testStep.err == nil)
next := timeout.Next()
if next != testStep.want {
t.Errorf("timeout.next(%v) = %v, want %v", testStep.err, next, testStep.want)
}
}
}
......@@ -23,42 +23,42 @@ import (
)
const (
NodeControllerSubsystem = "node_collector"
ZoneHealthStatisticKey = "zone_health"
ZoneSizeKey = "zone_size"
ZoneNoUnhealthyNodesKey = "unhealthy_nodes_in_zone"
EvictionsNumberKey = "evictions_number"
nodeControllerSubsystem = "node_collector"
zoneHealthStatisticKey = "zone_health"
zoneSizeKey = "zone_size"
zoneNoUnhealthyNodesKey = "unhealthy_nodes_in_zone"
evictionsNumberKey = "evictions_number"
)
var (
ZoneHealth = prometheus.NewGaugeVec(
zoneHealth = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Subsystem: NodeControllerSubsystem,
Name: ZoneHealthStatisticKey,
Subsystem: nodeControllerSubsystem,
Name: zoneHealthStatisticKey,
Help: "Gauge measuring percentage of healthy nodes per zone.",
},
[]string{"zone"},
)
ZoneSize = prometheus.NewGaugeVec(
zoneSize = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Subsystem: NodeControllerSubsystem,
Name: ZoneSizeKey,
Subsystem: nodeControllerSubsystem,
Name: zoneSizeKey,
Help: "Gauge measuring number of registered Nodes per zones.",
},
[]string{"zone"},
)
UnhealthyNodes = prometheus.NewGaugeVec(
unhealthyNodes = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Subsystem: NodeControllerSubsystem,
Name: ZoneNoUnhealthyNodesKey,
Subsystem: nodeControllerSubsystem,
Name: zoneNoUnhealthyNodesKey,
Help: "Gauge measuring number of not Ready Nodes per zones.",
},
[]string{"zone"},
)
EvictionsNumber = prometheus.NewCounterVec(
evictionsNumber = prometheus.NewCounterVec(
prometheus.CounterOpts{
Subsystem: NodeControllerSubsystem,
Name: EvictionsNumberKey,
Subsystem: nodeControllerSubsystem,
Name: evictionsNumberKey,
Help: "Number of Node evictions that happened since current instance of NodeController started.",
},
[]string{"zone"},
......@@ -67,11 +67,12 @@ var (
var registerMetrics sync.Once
// Register the metrics that are to be monitored.
func Register() {
registerMetrics.Do(func() {
prometheus.MustRegister(ZoneHealth)
prometheus.MustRegister(ZoneSize)
prometheus.MustRegister(UnhealthyNodes)
prometheus.MustRegister(EvictionsNumber)
prometheus.MustRegister(zoneHealth)
prometheus.MustRegister(zoneSize)
prometheus.MustRegister(unhealthyNodes)
prometheus.MustRegister(evictionsNumber)
})
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment