mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-01-13 11:25:19 +00:00
Make PortalIP alloc HA
* Add an allocator which saves state in etcd * Perform PortalIP allocation check on startup and periodically afterwards Also expose methods in master for downstream components to handle IP allocation / master registration themselves.
This commit is contained in:
120
pkg/registry/service/ipallocator/controller/repair.go
Normal file
120
pkg/registry/service/ipallocator/controller/repair.go
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
Copyright 2015 The Kubernetes Authors All rights reserved.
|
||||
|
||||
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 controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/ipallocator"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||
)
|
||||
|
||||
// Repair is a controller loop that periodically examines all service PortalIP allocations
|
||||
// and logs any errors, and then sets the compacted and accurate list of all allocated IPs.
|
||||
//
|
||||
// Handles:
|
||||
// * Duplicate PortalIP assignments caused by operator action or undetected race conditions
|
||||
// * PortalIPs that do not match the current portal network
|
||||
// * Allocations to services that were not actually created due to a crash or powerloss
|
||||
// * Migrates old versions of Kubernetes services into the atomic ipallocator model automatically
|
||||
//
|
||||
// Can be run at infrequent intervals, and is best performed on startup of the master.
|
||||
// Is level driven and idempotent - all valid PortalIPs will be updated into the ipallocator
|
||||
// map at the end of a single execution loop if no race is encountered.
|
||||
//
|
||||
// TODO: allocate new IPs if necessary
|
||||
// TODO: perform repair?
|
||||
type Repair struct {
|
||||
interval time.Duration
|
||||
registry service.Registry
|
||||
network *net.IPNet
|
||||
alloc service.IPRegistry
|
||||
}
|
||||
|
||||
// NewRepair creates a controller that periodically ensures that all portalIPs are uniquely allocated across the cluster
|
||||
// and generates informational warnings for a cluster that is not in sync.
|
||||
func NewRepair(interval time.Duration, registry service.Registry, network *net.IPNet, alloc service.IPRegistry) *Repair {
|
||||
return &Repair{
|
||||
interval: interval,
|
||||
registry: registry,
|
||||
network: network,
|
||||
alloc: alloc,
|
||||
}
|
||||
}
|
||||
|
||||
// RunUntil starts the controller until the provided ch is closed.
|
||||
func (c *Repair) RunUntil(ch chan struct{}) {
|
||||
util.Until(func() {
|
||||
if err := c.RunOnce(); err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
}, c.interval, ch)
|
||||
}
|
||||
|
||||
// RunOnce verifies the state of the portal IP allocations and returns an error if an unrecoverable problem occurs.
|
||||
func (c *Repair) RunOnce() error {
|
||||
latest, err := c.alloc.Get()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to refresh the service IP block: %v", err)
|
||||
}
|
||||
|
||||
ctx := api.WithNamespace(api.NewDefaultContext(), api.NamespaceAll)
|
||||
list, err := c.registry.ListServices(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to refresh the service IP block: %v", err)
|
||||
}
|
||||
|
||||
r := ipallocator.NewCIDRRange(c.network)
|
||||
for _, svc := range list.Items {
|
||||
if !api.IsServiceIPSet(&svc) {
|
||||
continue
|
||||
}
|
||||
ip := net.ParseIP(svc.Spec.PortalIP)
|
||||
if ip == nil {
|
||||
// portal IP is broken, reallocate
|
||||
util.HandleError(fmt.Errorf("the portal IP %s for service %s/%s is not a valid IP; please recreate", svc.Spec.PortalIP, svc.Name, svc.Namespace))
|
||||
continue
|
||||
}
|
||||
switch err := r.Allocate(ip); err {
|
||||
case nil:
|
||||
case ipallocator.ErrAllocated:
|
||||
// TODO: send event
|
||||
// portal IP is broken, reallocate
|
||||
util.HandleError(fmt.Errorf("the portal IP %s for service %s/%s was assigned to multiple services; please recreate", ip, svc.Name, svc.Namespace))
|
||||
case ipallocator.ErrNotInRange:
|
||||
// TODO: send event
|
||||
// portal IP is broken, reallocate
|
||||
util.HandleError(fmt.Errorf("the portal IP %s for service %s/%s is not within the service CIDR %s; please recreate", ip, svc.Name, svc.Namespace, c.network))
|
||||
case ipallocator.ErrFull:
|
||||
// TODO: send event
|
||||
return fmt.Errorf("the service CIDR %s is full; you must widen the CIDR in order to create new services")
|
||||
default:
|
||||
return fmt.Errorf("unable to allocate portal IP %s for service %s/%s due to an unknown error, exiting: %v", ip, svc.Name, svc.Namespace, err)
|
||||
}
|
||||
}
|
||||
|
||||
service.SnapshotRange(latest, r)
|
||||
|
||||
if err := c.alloc.CreateOrUpdate(latest); err != nil {
|
||||
return fmt.Errorf("unable to persist the updated service IP allocations: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
156
pkg/registry/service/ipallocator/controller/repair_test.go
Normal file
156
pkg/registry/service/ipallocator/controller/repair_test.go
Normal file
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
Copyright 2015 The Kubernetes Authors All rights reserved.
|
||||
|
||||
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 controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/registrytest"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/ipallocator"
|
||||
)
|
||||
|
||||
type mockIPRegistry struct {
|
||||
getCalled bool
|
||||
item *api.RangeAllocation
|
||||
err error
|
||||
|
||||
updateCalled bool
|
||||
updated *api.RangeAllocation
|
||||
updateErr error
|
||||
}
|
||||
|
||||
func (r *mockIPRegistry) Get() (*api.RangeAllocation, error) {
|
||||
r.getCalled = true
|
||||
return r.item, r.err
|
||||
}
|
||||
|
||||
func (r *mockIPRegistry) CreateOrUpdate(alloc *api.RangeAllocation) error {
|
||||
r.updateCalled = true
|
||||
r.updated = alloc
|
||||
return r.updateErr
|
||||
}
|
||||
|
||||
func TestRepair(t *testing.T) {
|
||||
registry := registrytest.NewServiceRegistry()
|
||||
_, cidr, _ := net.ParseCIDR("192.168.1.0/24")
|
||||
ipregistry := &mockIPRegistry{
|
||||
item: &api.RangeAllocation{},
|
||||
}
|
||||
r := NewRepair(0, registry, cidr, ipregistry)
|
||||
|
||||
if err := r.RunOnce(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !ipregistry.updateCalled || ipregistry.updated == nil || ipregistry.updated.Range != cidr.String() || ipregistry.updated != ipregistry.item {
|
||||
t.Errorf("unexpected ipregistry: %#v", ipregistry)
|
||||
}
|
||||
|
||||
ipregistry = &mockIPRegistry{
|
||||
item: &api.RangeAllocation{},
|
||||
updateErr: fmt.Errorf("test error"),
|
||||
}
|
||||
r = NewRepair(0, registry, cidr, ipregistry)
|
||||
if err := r.RunOnce(); !strings.Contains(err.Error(), ": test error") {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepairEmpty(t *testing.T) {
|
||||
_, cidr, _ := net.ParseCIDR("192.168.1.0/24")
|
||||
previous := ipallocator.NewCIDRRange(cidr)
|
||||
previous.Allocate(net.ParseIP("192.168.1.10"))
|
||||
network, data := previous.Snapshot()
|
||||
|
||||
registry := registrytest.NewServiceRegistry()
|
||||
ipregistry := &mockIPRegistry{
|
||||
item: &api.RangeAllocation{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
ResourceVersion: "1",
|
||||
},
|
||||
Range: network.String(),
|
||||
Data: data,
|
||||
},
|
||||
}
|
||||
r := NewRepair(0, registry, cidr, ipregistry)
|
||||
if err := r.RunOnce(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
after := ipallocator.NewCIDRRange(cidr)
|
||||
if err := after.Restore(cidr, ipregistry.updated.Data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if after.Has(net.ParseIP("192.168.1.10")) {
|
||||
t.Errorf("unexpected ipallocator state: %#v", after)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepairWithExisting(t *testing.T) {
|
||||
_, cidr, _ := net.ParseCIDR("192.168.1.0/24")
|
||||
previous := ipallocator.NewCIDRRange(cidr)
|
||||
network, data := previous.Snapshot()
|
||||
registry := registrytest.NewServiceRegistry()
|
||||
registry.List = api.ServiceList{
|
||||
Items: []api.Service{
|
||||
{
|
||||
Spec: api.ServiceSpec{PortalIP: "192.168.1.1"},
|
||||
},
|
||||
{
|
||||
Spec: api.ServiceSpec{PortalIP: "192.168.1.100"},
|
||||
},
|
||||
{ // outside CIDR, will be dropped
|
||||
Spec: api.ServiceSpec{PortalIP: "192.168.0.1"},
|
||||
},
|
||||
{ // empty, ignored
|
||||
Spec: api.ServiceSpec{PortalIP: ""},
|
||||
},
|
||||
{ // duplicate, dropped
|
||||
Spec: api.ServiceSpec{PortalIP: "192.168.1.1"},
|
||||
},
|
||||
{ // headless
|
||||
Spec: api.ServiceSpec{PortalIP: "None"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ipregistry := &mockIPRegistry{
|
||||
item: &api.RangeAllocation{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
ResourceVersion: "1",
|
||||
},
|
||||
Range: network.String(),
|
||||
Data: data,
|
||||
},
|
||||
}
|
||||
r := NewRepair(0, registry, cidr, ipregistry)
|
||||
if err := r.RunOnce(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
after := ipallocator.NewCIDRRange(cidr)
|
||||
if err := after.Restore(cidr, ipregistry.updated.Data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !after.Has(net.ParseIP("192.168.1.1")) || !after.Has(net.ParseIP("192.168.1.100")) {
|
||||
t.Errorf("unexpected ipallocator state: %#v", after)
|
||||
}
|
||||
if after.Free() != 252 {
|
||||
t.Errorf("unexpected ipallocator state: %#v", after)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user