Skip to content

Commit 89d264f

Browse files
authored
[octavia-ingress-controller] Add annotations to keep floating IP and/or specify an existing floating IP (kubernetes#2166)
* Add annotation to keep floationIP * Add annotation to specify floating ip to use on LB when creating ingress * Add doc for octavia.ingress.kubernetes.io/keep-floatingip & octavia.ingress.kubernetes.io/floatingip annotations * Remove debug logs * Change annotation syntax, don't create a new FIP, if user requested a particular one, add additional check if FIP already binded to correct port, add ability to update FIP of an existing ingress by updating annotation * Add missing else * Log format * Create fonctions to attach/detach fips to port * Fix bug when no fip provided in annotation the lb was created in private mode and improve openstack neutron fip logic
1 parent d9106b3 commit 89d264f

File tree

3 files changed

+169
-20
lines changed

3 files changed

+169
-20
lines changed

docs/octavia-ingress-controller/using-octavia-ingress-controller.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
- [Create an Ingress resource](#create-an-ingress-resource)
1616
- [Enable TLS encryption](#enable-tls-encryption)
1717
- [Allow CIDRs](#allow-cidrs)
18+
- [Creating Ingress by specifying a floating IP](#creating-ingress-by-specifying-a-floating-ip)
1819

1920
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
2021

@@ -504,3 +505,50 @@ spec:
504505
port:
505506
number: 8080
506507
```
508+
509+
## Creating Ingress by specifying a floating IP
510+
511+
Sometimes it's useful to use an existing available floating IP rather than creating a new one, especially in the automation scenario. In the example below, 122.112.219.229 is an available floating IP created in the OpenStack Networking service.
512+
513+
You can also specify to not delete the floating IP when the ingress will be deleted. By default, if not specified, the floating IP
514+
is deleted with the loadbalancer when the ingress if removed on kubernetes.
515+
516+
Create a new depolyment:
517+
```shell script
518+
kubectl create deployment test-web --replicas 3 --image nginx --port 80
519+
```
520+
521+
Create a service type NodePort:
522+
```shell script
523+
kubectl expose deployment test-web --type NodePort
524+
```
525+
526+
Create an ingress using a specific floating IP:
527+
```yaml
528+
apiVersion: networking.k8s.io/v1
529+
kind: Ingress
530+
metadata:
531+
name: test-web-ingress
532+
annotations:
533+
kubernetes.io/ingress.class: "openstack"
534+
octavia.ingress.kubernetes.io/internal: "false"
535+
octavia.ingress.kubernetes.io/keep-floatingip: "true" # floating ip will not be deleted when ingress is deleted
536+
octavia.ingress.kubernetes.io/floatingip: "122.112.219.229" # define the floating to use
537+
spec:
538+
rules:
539+
- host: test-web.foo.bar.com
540+
http:
541+
paths:
542+
- path: /
543+
pathType: Prefix
544+
backend:
545+
service:
546+
name: test-web
547+
port:
548+
number: 80
549+
```
550+
551+
If the floating IP is available you can test it with:
552+
```shell script
553+
curl -H "host: test-web.foo.bar.com" http://122.112.219.229
554+
```

pkg/ingress/controller/controller.go

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,16 @@ const (
101101
// Default to true.
102102
IngressAnnotationInternal = "octavia.ingress.kubernetes.io/internal"
103103

104+
// IngressAnnotationLoadBalancerKeepFloatingIP is the annotation used on the Ingress
105+
// to indicate that we want to keep the floatingIP after the ingress deletion. The Octavia LoadBalancer will be deleted
106+
// but not the floatingIP. That mean this floatingIP can be reused on another ingress without editing the dns area or update the whitelist.
107+
// Default to false.
108+
IngressAnnotationLoadBalancerKeepFloatingIP = "octavia.ingress.kubernetes.io/keep-floatingip"
109+
110+
// IngressAnnotationFloatingIp is the key of the annotation on an ingress to set floating IP that will be associated to LoadBalancers.
111+
// If the floatingIP is not available, an error will be returned.
112+
IngressAnnotationFloatingIP = "octavia.ingress.kubernetes.io/floatingip"
113+
104114
// IngressAnnotationSourceRangesKey is the key of the annotation on an ingress to set allowed IP ranges on their LoadBalancers.
105115
// It should be a comma-separated list of CIDRs.
106116
IngressAnnotationSourceRangesKey = "octavia.ingress.kubernetes.io/whitelist-source-range"
@@ -589,15 +599,24 @@ func (c *Controller) deleteIngress(ing *nwv1.Ingress) error {
589599
return nil
590600
}
591601

592-
// Delete the floating IP for the load balancer VIP. We don't check if the Ingress is internal or not, just delete
593-
// any floating IPs associated with the load balancer VIP port.
594-
logger.Debug("deleting floating IP")
595-
596-
if _, err = c.osClient.EnsureFloatingIP(true, loadbalancer.VipPortID, "", ""); err != nil {
597-
return fmt.Errorf("failed to delete floating IP: %v", err)
602+
// Manage the floatingIP
603+
keepFloatingSetting := getStringFromIngressAnnotation(ing, IngressAnnotationLoadBalancerKeepFloatingIP, "false")
604+
keepFloating, err := strconv.ParseBool(keepFloatingSetting)
605+
if err != nil {
606+
return fmt.Errorf("unknown annotation %s: %v", IngressAnnotationLoadBalancerKeepFloatingIP, err)
598607
}
599608

600-
logger.WithFields(log.Fields{"lbID": loadbalancer.ID}).Info("VIP or floating IP deleted")
609+
if !keepFloating {
610+
// Delete the floating IP for the load balancer VIP. We don't check if the Ingress is internal or not, just delete
611+
// any floating IPs associated with the load balancer VIP port.
612+
logger.WithFields(log.Fields{"lbID": loadbalancer.ID, "VIP": loadbalancer.VipAddress}).Info("deleting floating IPs associated with the load balancer VIP port")
613+
614+
if _, err = c.osClient.EnsureFloatingIP(true, loadbalancer.VipPortID, "", "", ""); err != nil {
615+
return fmt.Errorf("failed to delete floating IP: %v", err)
616+
}
617+
618+
logger.WithFields(log.Fields{"lbID": loadbalancer.ID}).Info("VIP or floating IP deleted")
619+
}
601620

602621
// Delete security group managed for the Ingress backend service
603622
if c.config.Octavia.ManageSecurityGroups {
@@ -934,15 +953,24 @@ func (c *Controller) ensureIngress(ing *nwv1.Ingress) error {
934953
address := lb.VipAddress
935954
// Allocate floating ip for loadbalancer vip if the external network is configured and the Ingress is not internal.
936955
if !isInternal && c.config.Octavia.FloatingIPNetwork != "" {
937-
logger.Info("creating floating IP")
938956

939-
description := fmt.Sprintf("Floating IP for Kubernetes ingress %s in namespace %s from cluster %s", ingName, ingNamespace, clusterName)
940-
address, err = c.osClient.EnsureFloatingIP(false, lb.VipPortID, c.config.Octavia.FloatingIPNetwork, description)
957+
floatingIPSetting := getStringFromIngressAnnotation(ing, IngressAnnotationFloatingIP, "")
941958
if err != nil {
942-
return fmt.Errorf("failed to create floating IP: %v", err)
959+
return fmt.Errorf("unknown annotation %s: %v", IngressAnnotationFloatingIP, err)
943960
}
944961

945-
logger.WithFields(log.Fields{"fip": address}).Info("floating IP created")
962+
description := fmt.Sprintf("Floating IP for Kubernetes ingress %s in namespace %s from cluster %s", ingName, ingNamespace, clusterName)
963+
964+
if floatingIPSetting != "" {
965+
logger.Info("try to use floating IP: ", floatingIPSetting)
966+
} else {
967+
logger.Info("creating new floating IP")
968+
}
969+
address, err = c.osClient.EnsureFloatingIP(false, lb.VipPortID, floatingIPSetting, c.config.Octavia.FloatingIPNetwork, description)
970+
if err != nil {
971+
return fmt.Errorf("failed to use provided floating IP %s : %v", floatingIPSetting, err)
972+
}
973+
logger.Info("floating IP ", address, " configured")
946974
}
947975

948976
// Update ingress status

pkg/ingress/controller/openstack/neutron.go

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,33 @@ func (os *OpenStack) getFloatingIPs(listOpts floatingips.ListOpts) ([]floatingip
4747
return allFIPs, nil
4848
}
4949

50+
func (os *OpenStack) createFloatingIP(portID string, floatingNetworkID string, description string) (*floatingips.FloatingIP, error) {
51+
floatIPOpts := floatingips.CreateOpts{
52+
PortID: portID,
53+
FloatingNetworkID: floatingNetworkID,
54+
Description: description,
55+
}
56+
return floatingips.Create(os.neutron, floatIPOpts).Extract()
57+
}
58+
59+
// associateFloatingIP associate an unused floating IP to a given Port
60+
func (os *OpenStack) associateFloatingIP(fip *floatingips.FloatingIP, portID string, description string) (*floatingips.FloatingIP, error) {
61+
updateOpts := floatingips.UpdateOpts{
62+
PortID: &portID,
63+
Description: &description,
64+
}
65+
return floatingips.Update(os.neutron, fip.ID, updateOpts).Extract()
66+
}
67+
68+
// disassociateFloatingIP disassociate a floating IP from a port
69+
func (os *OpenStack) disassociateFloatingIP(fip *floatingips.FloatingIP, description string) (*floatingips.FloatingIP, error) {
70+
updateDisassociateOpts := floatingips.UpdateOpts{
71+
PortID: new(string),
72+
Description: &description,
73+
}
74+
return floatingips.Update(os.neutron, fip.ID, updateDisassociateOpts).Extract()
75+
}
76+
5077
// GetSubnet get a subnet by the given ID.
5178
func (os *OpenStack) GetSubnet(subnetID string) (*subnets.Subnet, error) {
5279
subnet, err := subnets.Get(os.neutron, subnetID).Extract()
@@ -71,7 +98,7 @@ func (os *OpenStack) getPorts(listOpts ports.ListOpts) ([]ports.Port, error) {
7198
}
7299

73100
// EnsureFloatingIP makes sure a floating IP is allocated for the port
74-
func (os *OpenStack) EnsureFloatingIP(needDelete bool, portID string, floatingIPNetwork string, description string) (string, error) {
101+
func (os *OpenStack) EnsureFloatingIP(needDelete bool, portID string, existingfloatingIP string, floatingIPNetwork string, description string) (string, error) {
75102
listOpts := floatingips.ListOpts{PortID: portID}
76103
fips, err := os.getFloatingIPs(listOpts)
77104
if err != nil {
@@ -94,18 +121,64 @@ func (os *OpenStack) EnsureFloatingIP(needDelete bool, portID string, floatingIP
94121
}
95122

96123
var fip *floatingips.FloatingIP
97-
if len(fips) == 0 {
98-
floatIPOpts := floatingips.CreateOpts{
99-
PortID: portID,
124+
125+
if existingfloatingIP == "" {
126+
if len(fips) == 1 {
127+
fip = &fips[0]
128+
} else {
129+
fip, err = os.createFloatingIP(portID, floatingIPNetwork, description)
130+
if err != nil {
131+
return "", err
132+
}
133+
}
134+
} else {
135+
// if user provide FIP
136+
// check if provided fip is available
137+
opts := floatingips.ListOpts{
138+
FloatingIP: existingfloatingIP,
100139
FloatingNetworkID: floatingIPNetwork,
101-
Description: description,
102140
}
103-
fip, err = floatingips.Create(os.neutron, floatIPOpts).Extract()
141+
osFips, err := os.getFloatingIPs(opts)
104142
if err != nil {
105143
return "", err
106144
}
107-
} else {
108-
fip = &fips[0]
145+
if len(osFips) != 1 {
146+
return "", fmt.Errorf("error when searching floating IPs %s, %d floating IPs found", existingfloatingIP, len(osFips))
147+
}
148+
// check if fip is already attached to the correct port
149+
if osFips[0].PortID == portID {
150+
return osFips[0].FloatingIP, nil
151+
}
152+
// check if fip is already used by other port
153+
// We might consider if here we shouldn't detach that FIP instead of returning error
154+
if osFips[0].PortID != "" {
155+
return "", fmt.Errorf("floating IP %s already used by port %s", osFips[0].FloatingIP, osFips[0].PortID)
156+
}
157+
158+
// if port don't have fip
159+
if len(fips) == 0 {
160+
fip, err = os.associateFloatingIP(&osFips[0], portID, description)
161+
if err != nil {
162+
return "", err
163+
}
164+
} else if osFips[0].FloatingIP != fips[0].FloatingIP {
165+
// disassociate old fip : if update fip without disassociate
166+
// Openstack retrun http 409 error
167+
// "Cannot associate floating IP with port using fixed
168+
// IP, as that fixed IP already has a floating IP on
169+
// external network"
170+
_, err = os.disassociateFloatingIP(&fips[0], "")
171+
if err != nil {
172+
return "", err
173+
}
174+
// associate new fip
175+
fip, err = os.associateFloatingIP(&osFips[0], portID, description)
176+
if err != nil {
177+
return "", err
178+
}
179+
} else {
180+
fip = &fips[0]
181+
}
109182
}
110183

111184
return fip.FloatingIP, nil

0 commit comments

Comments
 (0)