Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Try to release IP if it has been allocated to current LB #47

Merged
merged 2 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions pkg/controller/ippool/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/harvester/harvester-load-balancer/pkg/controller/ippool/kubevip"
ctllbv1 "github.com/harvester/harvester-load-balancer/pkg/generated/controllers/loadbalancer.harvesterhci.io/v1beta1"
"github.com/harvester/harvester-load-balancer/pkg/ipam"
"github.com/harvester/harvester-load-balancer/pkg/utils"
)

const controllerName = "harvester-ipam-controller"
Expand All @@ -23,6 +24,7 @@ type Handler struct {
ipPoolCache ctllbv1.IPPoolCache
ipPoolClient ctllbv1.IPPoolClient
cmClient ctlcorev1.ConfigMapClient
lbCache ctllbv1.LoadBalancerCache

allocatorMap *ipam.SafeAllocatorMap
kubevipIPPoolConverter *kubevip.IPPoolConverter
Expand All @@ -31,10 +33,12 @@ type Handler struct {
func Register(ctx context.Context, management *config.Management) error {
ipPools := management.LbFactory.Loadbalancer().V1beta1().IPPool()
configmaps := management.CoreFactory.Core().V1().ConfigMap()
lbCache := management.LbFactory.Loadbalancer().V1beta1().LoadBalancer().Cache()

handler := &Handler{
ipPoolCache: ipPools.Cache(),
ipPoolClient: ipPools,
lbCache: lbCache,
allocatorMap: management.AllocatorMap,
kubevipIPPoolConverter: kubevip.NewIPPoolConverter(configmaps),
}
Expand All @@ -44,6 +48,7 @@ func Register(ctx context.Context, management *config.Management) error {
}

ipPools.OnChange(ctx, controllerName, handler.OnChange)
ipPools.OnChange(ctx, controllerName, handler.OnChangeToReleaseAnIP)
ipPools.OnRemove(ctx, controllerName, handler.OnRemove)

return nil
Expand Down Expand Up @@ -152,3 +157,63 @@ func correctAllocatedHistory(pool *lbv1.IPPool) (map[string]string, error) {

return allocatedHistory, nil
}

// Due to previous version bug, the LB may be removed but the related IP still exists on pool allocation record
// This feature gives a way to remove the IP allocation record manually
func (h *Handler) OnChangeToReleaseAnIP(_ string, ipPool *lbv1.IPPool) (*lbv1.IPPool, error) {
if ipPool == nil || ipPool.DeletionTimestamp != nil || ipPool.Annotations == nil {
return ipPool, nil
}

ipStr := ipPool.Annotations[utils.AnnotationKeyManuallyReleaseIP]
if ipStr == "" {
return ipPool, nil
}

// check if it is a valid format, e.g.
// loadbalancer.harvesterhci.io/manuallyReleaseIP: "192.168.5.12: default/cluster1-lb-3"
ip, namespace, name, err := utils.SplitIPAllocatedString(ipStr)
if err != nil {
logrus.Infof("IP Pool %s has a manual IP release request %s, it is not valid, skip", ipPool.Name, ipStr)
return h.removeAnnotationKeyManuallyReleaseIP(ipPool)
}

// check it is a real allocation record
lbStr := ipPool.Status.Allocated[ip]
if lbStr == "" || lbStr != fmt.Sprintf("%s/%s", namespace, name) {
logrus.Infof("IP Pool %s has a manual IP release request %s, it has been released, skip", ipPool.Name, ipStr)
return h.removeAnnotationKeyManuallyReleaseIP(ipPool)
}

// check if the lb is existing
_, err = h.lbCache.Get(namespace, name)
if err == nil {
logrus.Infof("IP Pool %s has a manual IP release request %s, the lb %s/%s is still existing, skip", ipPool.Name, ipStr, namespace, name)
return h.removeAnnotationKeyManuallyReleaseIP(ipPool)
}

// return for retry
if !apierrors.IsNotFound(err) {
return ipPool, err
}

a := h.allocatorMap.Get(ipPool.Name)
if a == nil {
return ipPool, fmt.Errorf("IP Pool %s has a manual IP release request %s, fail to get allocator", ipPool.Name, ipStr)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is an IPPool, but there is no internal allocator, can we just create a new allocator for it and leave some log? If we return error directly, the controller may keep retrying.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There has already been a OnChange controller, which ensures the allocator.

a, err := ipam.NewAllocator(ipPool.Name, ipPool.Spec.Ranges, h.ipPoolCache, h.ipPoolClient)

If this place happens to get a nil allocator, then next reconciller will get it normally. That's the consideration, thanks.

}
err = a.Release(fmt.Sprintf("%s/%s", namespace, name), "")
if err != nil {
return ipPool, fmt.Errorf("IP Pool %s has a manual IP release request %s, fail to release, error:%w", ipPool.Name, ipStr, err)
}

logrus.Infof("IP Pool %s has a manual IP release request %s, it is successfully released", ipPool.Name, ipStr)

// note: above a.Release also updates pool internally, following call may fail at first time, as ipPool is stale
return h.removeAnnotationKeyManuallyReleaseIP(ipPool)
}

func (h *Handler) removeAnnotationKeyManuallyReleaseIP(ipPool *lbv1.IPPool) (*lbv1.IPPool, error) {
ipPoolCopy := ipPool.DeepCopy()
delete(ipPoolCopy.Annotations, utils.AnnotationKeyManuallyReleaseIP)
return h.ipPoolClient.Update(ipPoolCopy)
}
46 changes: 37 additions & 9 deletions pkg/controller/loadbalancer/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import (
"fmt"
"net"
"reflect"
"strings"
"time"

ctlcniv1 "github.com/harvester/harvester/pkg/generated/controllers/k8s.cni.cncf.io/v1"
ctlkubevirtv1 "github.com/harvester/harvester/pkg/generated/controllers/kubevirt.io/v1"
ctlcorev1 "github.com/rancher/wrangler/pkg/generated/controllers/core/v1"
"github.com/sirupsen/logrus"

lb "github.com/harvester/harvester-load-balancer/pkg/apis/loadbalancer.harvesterhci.io"
lbv1 "github.com/harvester/harvester-load-balancer/pkg/apis/loadbalancer.harvesterhci.io/v1beta1"
"github.com/harvester/harvester-load-balancer/pkg/config"
ctldiscoveryv1 "github.com/harvester/harvester-load-balancer/pkg/generated/controllers/discovery.k8s.io/v1"
Expand All @@ -26,10 +26,11 @@ import (
const (
controllerName = "harvester-lb-controller"

AnnotationKeyNetwork = lb.GroupName + "/network"
AnnotationKeyProject = lb.GroupName + "/project"
AnnotationKeyNamespace = lb.GroupName + "/namespace"
AnnotationKeyCluster = lb.GroupName + "/cluster"
// referred by cloud-provider-harvester
AnnotationKeyNetwork = utils.AnnotationKeyNetwork
AnnotationKeyProject = utils.AnnotationKeyProject
AnnotationKeyNamespace = utils.AnnotationKeyNamespace
AnnotationKeyCluster = utils.AnnotationKeyCluster
)

var (
Expand Down Expand Up @@ -277,6 +278,15 @@ func (h *Handler) ensureAllocatedAddressPool(lbCopy, lb *lbv1.LoadBalancer) (*lb
ip, err := h.allocateIPFromPool(lb)
if err != nil {
logrus.Debugf("lb %s/%s fail to allocate from pool %s", lb.Namespace, lb.Name, err.Error())
// if unlucky the DuplicateAllocationKeyWord is reported, try to release IP, do not overwrite original error
if strings.Contains(err.Error(), utils.DuplicateAllocationKeyWord) {
pool, releaseErr := h.tryReleaseDuplicatedIPToPool(lb)
if releaseErr != nil {
logrus.Infof("lb %s/%s error: %s, try to release ip to pool %s, error: %s", lb.Namespace, lb.Name, err.Error(), pool, releaseErr.Error())
} else {
logrus.Infof("lb %s/%s error: %s, try to release ip to pool %s, ok", lb.Namespace, lb.Name, err.Error(), pool)
}
}
return lb, err
}

Expand All @@ -302,6 +312,24 @@ func (h *Handler) allocateIPFromPool(lb *lbv1.LoadBalancer) (*lbv1.AllocatedAddr
return h.requestIP(lb, pool)
}

func (h *Handler) tryReleaseDuplicatedIPToPool(lb *lbv1.LoadBalancer) (string, error) {
pool := lb.Spec.IPPool
if pool == "" {
// match an IP pool automatically if not specified
pool, err := h.selectIPPool(lb)
if err != nil {
return pool, err
}
}

// if pool is not ready, just fail and wait
a := h.allocatorMap.Get(pool)
if a == nil {
return pool, fmt.Errorf("fail to get allocator %s", pool)
}
return pool, a.Release(fmt.Sprintf("%s/%s", lb.Namespace, lb.Name), "")
}

func (h *Handler) requestIP(lb *lbv1.LoadBalancer, pool string) (*lbv1.AllocatedAddress, error) {
allocator := h.allocatorMap.Get(pool)
if allocator == nil {
Expand All @@ -323,10 +351,10 @@ func (h *Handler) requestIP(lb *lbv1.LoadBalancer, pool string) (*lbv1.Allocated

func (h *Handler) selectIPPool(lb *lbv1.LoadBalancer) (string, error) {
r := &ipam.Requirement{
Network: lb.Annotations[AnnotationKeyNetwork],
Project: lb.Annotations[AnnotationKeyProject],
Namespace: lb.Annotations[AnnotationKeyNamespace],
Cluster: lb.Annotations[AnnotationKeyCluster],
Network: lb.Annotations[utils.AnnotationKeyNetwork],
Project: lb.Annotations[utils.AnnotationKeyProject],
Namespace: lb.Annotations[utils.AnnotationKeyNamespace],
Cluster: lb.Annotations[utils.AnnotationKeyCluster],
}
if r.Namespace == "" {
r.Namespace = lb.Namespace
Expand Down
5 changes: 5 additions & 0 deletions pkg/utils/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,9 @@ const (
AnnotationKeyProject = lb.GroupName + "/project"
AnnotationKeyNamespace = lb.GroupName + "/namespace"
AnnotationKeyCluster = lb.GroupName + "/cluster"

// value format: loadbalancer.harvesterhci.io/manuallyReleaseIP: "192.168.5.12: default/cluster1-lb-3"
AnnotationKeyManuallyReleaseIP = lb.GroupName + "/manuallyReleaseIP"

DuplicateAllocationKeyWord = "duplicate allocation is not allowed"
)
21 changes: 21 additions & 0 deletions pkg/utils/vid.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,24 @@ func GetVid(network string, nadCache ctlcniv1.NetworkAttachmentDefinitionCache)
}
return netConf.VLAN, nil
}

// input format: "192.168.5.12: default/cluster1-lb-3"
func SplitIPAllocatedString(ipStr string) (ip, namespace, name string, err error) {
ipStr = strings.Trim(ipStr, " ")
fields := strings.Split(ipStr, ":")
if len(fields) != 2 {
err = fmt.Errorf("%s is not a valid allocation record", ipStr)
return
}
nsnameStr := strings.Trim(fields[1], " ")
fields2 := strings.Split(nsnameStr, "/")
if len(fields2) != 2 {
err = fmt.Errorf("%s is not a valid allocation record", ipStr)
return
}
ip = fields[0]
namespace = fields2[0]
name = fields2[1]
err = nil
return
}