Skip to content

Commit 2315f7c

Browse files
authored
Merge pull request #131 from qiangzii/master
support auto delete lb/listener without annotation;
2 parents eeec280 + 4bef34a commit 2315f7c

9 files changed

Lines changed: 269 additions & 23 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# GitHub viewer defaults to 8, change with ?ts=4 in URL
33

44
GIT_REPOSITORY= github.com/yunify/qingcloud-cloud-controller-manager
5-
IMG?= qingcloud/cloud-controller-manager:latest
5+
IMG?= qingcloud/cloud-controller-manager:v1.4.17
66
#Debug level: 0, 1, 2 (1 true, 2 use bash)
77
DEBUG?= 0
88
DOCKERFILE?= deploy/Dockerfile

cmd/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import (
4141
// e.g. _"k8s.io/legacy-cloud-providers/<provider>"
4242
"github.com/yunify/qingcloud-cloud-controller-manager/pkg/controllers/clusternode"
4343
"github.com/yunify/qingcloud-cloud-controller-manager/pkg/controllers/endpoint"
44+
"github.com/yunify/qingcloud-cloud-controller-manager/pkg/controllers/service"
4445
_ "github.com/yunify/qingcloud-cloud-controller-manager/pkg/qingcloud"
4546
)
4647

@@ -95,5 +96,6 @@ func controllerInitializers() map[string]app.InitFuncConstructor {
9596
controllerInitializers := app.DefaultInitFuncConstructors
9697
controllerInitializers["endpoint"] = endpoint.StartEndpointControllerWrapper
9798
controllerInitializers["clusternode"] = clusternode.StartClusterNodeControllerWrapper
99+
controllerInitializers["cloud-service"] = service.StartServiceControllerWarpper
98100
return controllerInitializers
99101
}

config/default/manager_image_patch.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ spec:
77
spec:
88
containers:
99
# Change the value of image field below to your controller image URL
10-
- image: qingcloud/cloud-controller-manager:v1.4.10
10+
- image: qingcloud/cloud-controller-manager:v1.4.17
1111
name: qingcloud-cloud-controller-manager
1212
imagePullPolicy: IfNotPresent

pkg/controllers/endpoint/endpoint_controller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ func (epc *EndpointController) handleEndpointsUpdate(key string) error {
175175
svc, err := epc.serviceLister.Services(namespace).Get(name)
176176
if err != nil {
177177
if k8sErr.IsNotFound(err) {
178-
klog.V(4).Infof("endpoints %s/%s has no service, ignore!", namespace, name)
178+
klog.V(4).Infof("endpoints %s/%s has no service, ignore handle endpoints update!", namespace, name)
179179
return nil
180180
}
181181
return fmt.Errorf("get service %s/%s error: %v", namespace, name, err)
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
package service
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"time"
8+
9+
corev1 "k8s.io/api/core/v1"
10+
"k8s.io/apimachinery/pkg/util/runtime"
11+
"k8s.io/apimachinery/pkg/util/wait"
12+
coreinformers "k8s.io/client-go/informers/core/v1"
13+
clientset "k8s.io/client-go/kubernetes"
14+
corelisters "k8s.io/client-go/listers/core/v1"
15+
"k8s.io/client-go/tools/cache"
16+
"k8s.io/client-go/util/workqueue"
17+
cloudprovider "k8s.io/cloud-provider"
18+
cloudproviderapp "k8s.io/cloud-provider/app"
19+
cloudcontrollerconfig "k8s.io/cloud-provider/app/config"
20+
genericcontrollermanager "k8s.io/controller-manager/app"
21+
"k8s.io/klog"
22+
23+
"github.com/yunify/qingcloud-cloud-controller-manager/pkg/qingcloud"
24+
)
25+
26+
const (
27+
serviceRunWorkerPeriod = 1 * time.Second
28+
serviceWorkers = 1
29+
)
30+
31+
type ServiceController struct {
32+
cloud cloudprovider.Interface
33+
34+
serviceLister corelisters.ServiceLister
35+
serviceListerSynced cache.InformerSynced
36+
serviceQueue workqueue.RateLimitingInterface
37+
}
38+
39+
func StartServiceControllerWarpper(completedConfig *cloudcontrollerconfig.CompletedConfig, cloud cloudprovider.Interface) cloudproviderapp.InitFunc {
40+
return func(ctx genericcontrollermanager.ControllerContext) (http.Handler, bool, error) {
41+
return startServiceController(completedConfig, cloud, ctx.Stop)
42+
}
43+
}
44+
45+
func startServiceController(ctx *cloudcontrollerconfig.CompletedConfig, cloud cloudprovider.Interface, stopCh <-chan struct{}) (http.Handler, bool, error) {
46+
// Start the endpoint controller
47+
serviceController, err := New(
48+
cloud,
49+
ctx.ClientBuilder.ClientOrDie("cloud-service-controller"),
50+
ctx.SharedInformers.Core().V1().Services(),
51+
)
52+
if err != nil {
53+
// This error shouldn't fail. It lives like this as a legacy.
54+
klog.Errorf("Failed to start cloud-service controller: %v", err)
55+
return nil, false, nil
56+
}
57+
58+
go serviceController.Run(stopCh, serviceWorkers)
59+
60+
return nil, true, nil
61+
}
62+
63+
func New(
64+
cloud cloudprovider.Interface,
65+
kubeClient clientset.Interface,
66+
serviceInformer coreinformers.ServiceInformer,
67+
) (*ServiceController, error) {
68+
69+
sc := &ServiceController{
70+
cloud: cloud,
71+
serviceQueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "cloud-service"),
72+
serviceLister: serviceInformer.Lister(),
73+
serviceListerSynced: serviceInformer.Informer().HasSynced,
74+
}
75+
76+
serviceInformer.Informer().AddEventHandler(
77+
cache.ResourceEventHandlerFuncs{
78+
UpdateFunc: func(old, cur interface{}) {
79+
oldSvc, ok1 := old.(*corev1.Service)
80+
newSvc, ok2 := cur.(*corev1.Service)
81+
if ok1 && ok2 && sc.needClean(oldSvc, newSvc) {
82+
sc.enqueueService(old)
83+
// sc.cleanLBAndListener(oldSvc, newSvc)
84+
85+
}
86+
},
87+
},
88+
)
89+
90+
return sc, nil
91+
}
92+
93+
// for service which reuse lb, need to clean listener in those situations
94+
// change lb: lb1 -> lb2, need to clean lb1's listener ==> TODO
95+
// change service type: loadbalancer -> nodeport/clusterip, if service changed type and delete annotation, need clean lb(auto created lb) or listener(reuse-lb)
96+
func (sc *ServiceController) needClean(old, new *corev1.Service) bool {
97+
if old.Spec.Type == corev1.ServiceTypeLoadBalancer {
98+
if len(old.Annotations) == 0 {
99+
klog.V(4).Infof("service %s/%s last config has no annotation, cloud-service-controller do nothing!", old.Namespace, old.Name)
100+
return false
101+
}
102+
if new.Annotations == nil {
103+
new.Annotations = make(map[string]string)
104+
}
105+
106+
if new.Spec.Type == corev1.ServiceTypeLoadBalancer {
107+
//TODO: change lb, clean listener for old listener if change lb
108+
klog.V(4).Infof("service %s/%s type not change, cloud-service-controller do nothing!", old.Namespace, old.Name)
109+
return false
110+
}
111+
112+
// change service type
113+
// reuse-lb, clean listener if new service delete annotation
114+
oldStrategy := old.Annotations[qingcloud.ServiceAnnotationLoadBalancerPolicy]
115+
oldLBID := old.Annotations[qingcloud.ServiceAnnotationLoadBalancerID]
116+
newStrategy := new.Annotations[qingcloud.ServiceAnnotationLoadBalancerPolicy]
117+
newLBID := new.Annotations[qingcloud.ServiceAnnotationLoadBalancerID]
118+
if oldStrategy == qingcloud.ReuseExistingLB && oldLBID != "" {
119+
if newStrategy == oldStrategy && newLBID == oldLBID {
120+
// new service keep reuse lb annotation, ignore!
121+
klog.V(4).Infof("service %s/%s keep reuse lb id annotation, cloud-service-controller do nothing!", old.Namespace, old.Name)
122+
return false
123+
}
124+
klog.V(4).Infof("service %s/%s deleted reuse lb id annotation, cloud-service-controller will try to delete lb later!", old.Namespace, old.Name)
125+
return true
126+
}
127+
128+
}
129+
return false
130+
}
131+
132+
func (sc *ServiceController) enqueueService(obj interface{}) {
133+
_, ok := obj.(*corev1.Service)
134+
if !ok {
135+
return
136+
}
137+
sc.serviceQueue.Add(obj)
138+
}
139+
140+
func (sc *ServiceController) Run(stopCh <-chan struct{}, workers int) {
141+
defer runtime.HandleCrash()
142+
defer sc.serviceQueue.ShutDown()
143+
144+
klog.Info("Starting cloud service controller")
145+
defer klog.Info("Shutting down cloud service controller")
146+
147+
if !cache.WaitForCacheSync(stopCh, sc.serviceListerSynced) {
148+
return
149+
}
150+
151+
for i := 0; i < workers; i++ {
152+
go wait.Until(sc.worker, serviceRunWorkerPeriod, stopCh)
153+
}
154+
155+
<-stopCh
156+
}
157+
158+
func (sc *ServiceController) worker() {
159+
for sc.processNextWorkItem() {
160+
}
161+
}
162+
163+
func (sc *ServiceController) processNextWorkItem() bool {
164+
obj, quit := sc.serviceQueue.Get()
165+
if quit {
166+
return false
167+
}
168+
defer sc.serviceQueue.Done(obj)
169+
170+
svc, ok := obj.(*corev1.Service)
171+
if !ok {
172+
runtime.HandleError(fmt.Errorf("error assert service %v (will retry)", svc))
173+
return true
174+
}
175+
err := sc.handleServiceUpdate(svc)
176+
if err == nil {
177+
sc.serviceQueue.Forget(obj)
178+
return true
179+
}
180+
181+
runtime.HandleError(fmt.Errorf("error processing service %s/%s (will retry): %v", svc.Namespace, svc.Name, err))
182+
sc.serviceQueue.AddRateLimited(obj)
183+
184+
return true
185+
}
186+
187+
func (sc *ServiceController) handleServiceUpdate(svc *corev1.Service) error {
188+
startTime := time.Now()
189+
defer func() {
190+
klog.V(4).Infof("Finished handleEndpointsUpdate %s/%s (%v)", svc.Namespace, svc.Name, time.Since(startTime))
191+
}()
192+
193+
cloudLbIntf, _ := sc.cloud.LoadBalancer()
194+
195+
listenerName := fmt.Sprintf("listener_%s_%s_", svc.Namespace, svc.Name)
196+
lbID := svc.Annotations[qingcloud.ServiceAnnotationLoadBalancerID]
197+
198+
klog.Infof("service %s/%s type changed, and loadbalancer annotation has been deleted, try to deleting listener %s of loadbalancer %s",
199+
svc.Namespace, svc.Name, listenerName, lbID)
200+
err := cloudLbIntf.EnsureLoadBalancerDeleted(context.TODO(), "", svc)
201+
if err != nil {
202+
return fmt.Errorf("delete listener %s of loadbalancer %s for service %s/%s error: %v", listenerName, lbID, svc.Namespace, svc.Name, err)
203+
}
204+
205+
klog.Infof("delete listener %s of loadbalancer %s for service %s/%s successful", listenerName, lbID, svc.Namespace, svc.Name)
206+
return nil
207+
}

pkg/executor/lb.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ func (q *QingCloudClient) DeleteLB(id *string) error {
271271

272272
err = qcclient.WaitJob(q.jobService, *output.JobID, operationWaitTimeout, waitInterval)
273273
if err != nil {
274-
return fmt.Errorf("lb %s delete job not completed", *id)
274+
return fmt.Errorf("lb %s delete job not completed, err: %v", *id, err)
275275
}
276276
}
277277

pkg/qingcloud/annotations.go

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -140,15 +140,16 @@ func (qc *QingCloud) ParseServiceLBConfig(cluster string, service *v1.Service) (
140140
lbEipIds, hasEip := annotation[ServiceAnnotationLoadBalancerEipIds]
141141
if hasEip && lbEipIds != "" {
142142
config.EipIDs = qcservice.StringSlice(strings.Split(lbEipIds, ","))
143-
}
144-
source := annotation[ServiceAnnotationLoadBalancerEipSource]
145-
config.EipSource = &source
146-
switch source {
147-
case AllocateOnly:
148-
case UseAvailableOnly:
149-
case UseAvailableOrAllocateOne:
150-
default:
151-
config.EipSource = nil
143+
} else {
144+
source := annotation[ServiceAnnotationLoadBalancerEipSource]
145+
config.EipSource = &source
146+
switch source {
147+
case AllocateOnly:
148+
case UseAvailableOnly:
149+
case UseAvailableOrAllocateOne:
150+
default:
151+
config.EipSource = nil
152+
}
152153
}
153154

154155
if vxnetID, ok := annotation[ServiceAnnotationLoadBalancerVxnetID]; ok {
@@ -193,29 +194,29 @@ func (qc *QingCloud) ParseServiceLBConfig(cluster string, service *v1.Service) (
193194
}
194195

195196
networkType := annotation[ServiceAnnotationLoadBalancerNetworkType]
196-
if config.VxNetID == nil && qc.Config.DefaultVxNetForLB != "" {
197-
config.VxNetID = qcservice.String(qc.Config.DefaultVxNetForLB)
198-
}
199197
switch networkType {
200198
case NetworkModePublic:
201199
config.NetworkType = networkType
202200
case NetworkModeInternal:
203201
config.NetworkType = networkType
202+
if config.VxNetID == nil && qc.Config.DefaultVxNetForLB != "" {
203+
config.VxNetID = qcservice.String(qc.Config.DefaultVxNetForLB)
204+
}
204205
default:
205206
config.NetworkType = NetworkModePublic
206207
}
207208

208209
if lbType, ok := annotation[ServiceAnnotationLoadBalancerType]; ok {
209210
t, err := strconv.Atoi(lbType)
210211
if err != nil {
211-
return nil, fmt.Errorf("Pls spec a valid value of loadBalancer type, acceptted values are '0-3',err: %s", err.Error())
212+
return nil, fmt.Errorf("pls spec a valid value of loadBalancer type, acceptted values are '0-3',err: %s", err.Error())
212213
}
213214
config.LoadBalancerType = &t
214215
}
215216
if nodes, ok := annotation[ServiceAnnotationLoadBalancerNodes]; ok {
216217
num, err := strconv.Atoi(nodes)
217218
if err != nil {
218-
return nil, fmt.Errorf("Pls spec a valid value of loadBalancer node number")
219+
return nil, fmt.Errorf("pls spec a valid value of loadBalancer node number")
219220
}
220221
config.NodeCount = &num
221222
}
@@ -235,7 +236,7 @@ func (qc *QingCloud) ParseServiceLBConfig(cluster string, service *v1.Service) (
235236
break
236237
} else {
237238
if config.InternalIP == nil || config.InternalReuseID == nil {
238-
return nil, fmt.Errorf("Must specify reuse-id or internalip if wants to use shared internal lb")
239+
return nil, fmt.Errorf("must specify reuse-id or internalip if wants to use shared internal lb")
239240
}
240241

241242
if config.InternalReuseID != nil {

pkg/qingcloud/qingcloud.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,9 @@ func (qc *QingCloud) GetLoadBalancerName(_ context.Context, _ string, service *v
152152
// GetLoadBalancer returns whether the specified load balancer exists, and
153153
// if so, what its status is.
154154
func (qc *QingCloud) GetLoadBalancer(ctx context.Context, _ string, service *v1.Service) (status *v1.LoadBalancerStatus, exists bool, err error) {
155-
_, lb, err := qc.getLoadBalancer(service)
155+
_, lb, err := qc.getLoadBalancerBeforeDelete(service)
156156

157-
if errors.IsResourceNotFound(err) {
157+
if errors.IsResourceNotFound(err) || lb == nil {
158158
return nil, false, nil
159159
}
160160

@@ -464,9 +464,9 @@ func (qc *QingCloud) createListenersAndBackends(conf *LoadBalancerConfig, status
464464
// Implementations must treat the *v1.Service parameter as read-only and not modify it.
465465
// Parameter 'clusterName' is the name of the cluster as presented to kube-controller-manager
466466
func (qc *QingCloud) EnsureLoadBalancerDeleted(ctx context.Context, _ string, service *v1.Service) error {
467-
lbConfig, lb, err := qc.getLoadBalancer(service)
467+
lbConfig, lb, err := qc.getLoadBalancerBeforeDelete(service)
468468
klog.V(4).Infof("==== EnsureLoadBalancerDeleted %s config %s ====", spew.Sdump(lb), spew.Sdump(lbConfig))
469-
if errors.IsResourceNotFound(err) {
469+
if errors.IsResourceNotFound(err) || lb == nil {
470470
return nil
471471
}
472472

pkg/qingcloud/qingcloud_utils.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,39 @@ func (qc *QingCloud) diffBackend(listener *apis.LoadBalancerListener, nodes []*v
182182

183183
return
184184
}
185+
186+
// this method only used to get lb before delete lb(service type changed: Loadbalancer -> ClusterIP/NodePort)
187+
// new service may deleted lb annotation, if so, not return error,
188+
// annother controller(cloud-service-controller) will deleted the lb according to the last version service annotation
189+
func (qc *QingCloud) getLoadBalancerBeforeDelete(svc *v1.Service) (conf *LoadBalancerConfig, lb *apis.LoadBalancer, err error) {
190+
191+
conf = qc.parseServiceLBAndEIP(svc)
192+
if conf == nil {
193+
return nil, nil, nil
194+
}
195+
196+
if conf.ReuseLBID != "" {
197+
lb, err = qc.Client.GetLoadBalancerByID(conf.ReuseLBID)
198+
} else if conf.LoadBalancerName != "" {
199+
lb, err = qc.Client.GetLoadBalancerByName(conf.LoadBalancerName)
200+
} else {
201+
return conf, nil, fmt.Errorf("cannot found loadbalance id or name")
202+
}
203+
return conf, lb, err
204+
}
205+
206+
// only used to get lb and eip config, return nil if has no lb and eip annotation
207+
// we think the service auto create lb as default; but if the service config reuse lb id, we use lb id first
208+
func (qc *QingCloud) parseServiceLBAndEIP(svc *v1.Service) (conf *LoadBalancerConfig) {
209+
conf = new(LoadBalancerConfig)
210+
211+
conf.listenerName = fmt.Sprintf("listener_%s_%s_", svc.Namespace, svc.Name)
212+
conf.sgName = conf.LoadBalancerName
213+
conf.LoadBalancerName = fmt.Sprintf("k8s_lb_%s_%s_%s_%s", qc.Config.ClusterID, svc.Namespace, svc.Name, util.GetFirstUID(string(svc.UID)))
214+
if len(svc.Annotations) > 0 {
215+
if svc.Annotations[ServiceAnnotationLoadBalancerPolicy] == ReuseExistingLB {
216+
conf.ReuseLBID = svc.Annotations[ServiceAnnotationLoadBalancerID]
217+
}
218+
}
219+
return
220+
}

0 commit comments

Comments
 (0)