Skip to content

Commit 28f67e9

Browse files
authored
kne cli - annotate azure LB - disable health check, use internal IP's, ignore other pod issues, wait on service delete on destroy (#13)
1 parent 33eeb0e commit 28f67e9

4 files changed

Lines changed: 295 additions & 1 deletion

File tree

deploy/deploy.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,18 @@ func (d *Deployment) Deploy(ctx context.Context, kubecfg string) (rerr error) {
224224
log.Warningf("Failed to start pod watcher: %v", err)
225225
} else {
226226
w.SetProgress(d.Progress)
227+
// Restrict watcher to known namespaces managed during deployment to avoid noise
228+
// from unrelated user workloads in other namespaces.
229+
w.AllowNamespaces(
230+
"kube-system",
231+
"metallb-system",
232+
"meshnet",
233+
"arista-ceoslab-operator-system",
234+
"lemming-operator",
235+
"srlinux-controller-system",
236+
"ixiatg-op-system",
237+
"cdnos-controller-system",
238+
)
227239
defer func() {
228240
cancel()
229241
rerr = w.Cleanup(rerr)

pods/status.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type Watcher struct {
3535
progress bool
3636
currentNamespace string
3737
currentPod types.UID
38+
allowedNS map[string]struct{}
3839
}
3940

4041
// NewWatcher returns a Watcher on the provided client or an error. The cancel
@@ -78,6 +79,24 @@ func (w *Watcher) SetProgress(value bool) {
7879
w.mu.Unlock()
7980
}
8081

82+
// AllowNamespaces restricts the watcher to only consider pods in the provided namespaces.
83+
// If no namespaces are provided, all namespaces are considered.
84+
func (w *Watcher) AllowNamespaces(namespaces ...string) {
85+
w.mu.Lock()
86+
defer w.mu.Unlock()
87+
if len(namespaces) == 0 {
88+
w.allowedNS = nil
89+
return
90+
}
91+
w.allowedNS = make(map[string]struct{}, len(namespaces))
92+
for _, ns := range namespaces {
93+
if ns == "" {
94+
continue
95+
}
96+
w.allowedNS[ns] = struct{}{}
97+
}
98+
}
99+
81100
func (w *Watcher) stop() {
82101
w.mu.Lock()
83102
stop := w.wstop
@@ -128,6 +147,16 @@ func (w *Watcher) display(format string, v ...any) {
128147
}
129148

130149
func (w *Watcher) updatePod(s *PodStatus) bool {
150+
// If allowed namespaces are configured, ignore pods outside them.
151+
w.mu.Lock()
152+
allowed := w.allowedNS
153+
w.mu.Unlock()
154+
if allowed != nil {
155+
if _, ok := allowed[s.Namespace]; !ok {
156+
return true
157+
}
158+
}
159+
131160
newNamespace := s.Namespace != w.currentNamespace
132161
var newState string
133162

topo/node/drivenets/drivenets.go

Lines changed: 252 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,19 @@ import (
2626
"os"
2727
"path/filepath"
2828
"strings"
29+
"time"
2930

3031
"github.com/drivenets/cdnos-controller/api/v1/clientset"
3132
"github.com/openconfig/kne/topo/node"
3233
"google.golang.org/grpc/codes"
3334
"google.golang.org/grpc/status"
3435
"google.golang.org/protobuf/proto"
36+
"k8s.io/client-go/kubernetes"
3537
"k8s.io/client-go/rest"
3638

3739
cdnosv1 "github.com/drivenets/cdnos-controller/api/v1"
3840
corev1 "k8s.io/api/core/v1"
41+
apierrors "k8s.io/apimachinery/pkg/api/errors"
3942
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
4043
log "k8s.io/klog/v2"
4144

@@ -255,9 +258,112 @@ func (n *Node) cdnosCreate(ctx context.Context) error {
255258
if _, err := cs.CdnosV1alpha1().Cdnoss(n.Namespace).Create(ctx, dut, metav1.CreateOptions{}); err != nil {
256259
return fmt.Errorf("failed to create cdnos: %v", err)
257260
}
261+
// Ensure the controller-created Service has required Azure LB annotations.
262+
// Azure annotations are required for proper LoadBalancer behavior.
263+
if err := n.annotateCdnosService(ctx); err != nil {
264+
return fmt.Errorf("failed to annotate service for %s: %v", n.Name(), err)
265+
}
258266
return nil
259267
}
260268

269+
// annotateCdnosService waits for the controller-created Service named "service-<node>"
270+
// and adds Azure LoadBalancer annotations. These annotations are required for proper operation.
271+
func (n *Node) annotateCdnosService(ctx context.Context) error {
272+
// Always apply Azure annotations - they're required and harmless on non-Azure clusters
273+
// If Azure detection fails, still apply annotations to ensure proper operation
274+
isAzure := isAzureAKS(n.KubeClient)
275+
if !isAzure {
276+
log.V(1).Infof("Azure AKS not detected, but applying Azure annotations anyway (required for operation)")
277+
} else {
278+
log.Infof("Azure AKS detected; annotating controller-managed Services for %q", n.Name())
279+
}
280+
log.Infof("Azure AKS detected; annotating controller-managed Services for %q", n.Name())
281+
deadline := time.Now().Add(10 * time.Minute)
282+
desired := map[string]string{
283+
"service.beta.kubernetes.io/azure-load-balancer-internal": "true",
284+
}
285+
// Build no-probe rules from this node's services (outside ports).
286+
for port := range n.Proto.Services {
287+
key := fmt.Sprintf("service.beta.kubernetes.io/port_%d_no_probe_rule", port)
288+
desired[key] = "true"
289+
}
290+
for {
291+
if time.Now().After(deadline) {
292+
return fmt.Errorf("timeout waiting to annotate services for %q", n.Name())
293+
}
294+
svcs, err := n.servicesForNode(ctx)
295+
if err != nil || len(svcs) == 0 {
296+
time.Sleep(1 * time.Second)
297+
continue
298+
}
299+
allAnnotated := true
300+
for i := range svcs {
301+
s := &svcs[i]
302+
changed := false
303+
if s.Annotations == nil {
304+
s.Annotations = map[string]string{}
305+
changed = true
306+
}
307+
for k, v := range desired {
308+
if s.Annotations[k] != v {
309+
s.Annotations[k] = v
310+
changed = true
311+
}
312+
}
313+
if changed {
314+
// Use a short-lived background context to avoid parent ctx cancellations.
315+
updateCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
316+
_, err := n.KubeClient.CoreV1().Services(n.Namespace).Update(updateCtx, s, metav1.UpdateOptions{})
317+
cancel()
318+
if err != nil {
319+
// Retry once on conflict with a fresh GET
320+
if apierrors.IsConflict(err) {
321+
getCtx, cancelGet := context.WithTimeout(context.Background(), 5*time.Second)
322+
fresh, gerr := n.KubeClient.CoreV1().Services(n.Namespace).Get(getCtx, s.Name, metav1.GetOptions{})
323+
cancelGet()
324+
if gerr == nil {
325+
if fresh.Annotations == nil {
326+
fresh.Annotations = map[string]string{}
327+
}
328+
for k, v := range desired {
329+
fresh.Annotations[k] = v
330+
}
331+
updateCtx2, cancelUpd2 := context.WithTimeout(context.Background(), 5*time.Second)
332+
_, uerr := n.KubeClient.CoreV1().Services(n.Namespace).Update(updateCtx2, fresh, metav1.UpdateOptions{})
333+
cancelUpd2()
334+
if uerr == nil {
335+
log.Infof("Annotated Service %q with Azure LB annotations (after conflict retry)", s.Name)
336+
continue
337+
}
338+
}
339+
}
340+
allAnnotated = false
341+
continue
342+
}
343+
log.Infof("Annotated Service %q with Azure LB annotations", s.Name)
344+
}
345+
// Verify
346+
getCtx, cancelGet := context.WithTimeout(context.Background(), 5*time.Second)
347+
got, err := n.KubeClient.CoreV1().Services(n.Namespace).Get(getCtx, s.Name, metav1.GetOptions{})
348+
cancelGet()
349+
if err != nil {
350+
allAnnotated = false
351+
continue
352+
}
353+
for k, v := range desired {
354+
if got.Annotations[k] != v {
355+
allAnnotated = false
356+
break
357+
}
358+
}
359+
}
360+
if allAnnotated {
361+
return nil
362+
}
363+
time.Sleep(500 * time.Millisecond)
364+
}
365+
}
366+
261367
func (n *Node) Status(ctx context.Context) (node.Status, error) {
262368
if !isModelCdnos(n.Impl.Proto.Model) {
263369
return node.StatusUnknown, fmt.Errorf("invalid model specified")
@@ -298,7 +404,112 @@ func (n *Node) cdnosDelete(ctx context.Context) error {
298404
if err != nil {
299405
return err
300406
}
301-
return cs.CdnosV1alpha1().Cdnoss(n.Namespace).Delete(ctx, n.Name(), metav1.DeleteOptions{})
407+
// 1) Start teardown by deleting all Cdnos CRs in the namespace
408+
// (controller will clean up owned objects for each).
409+
list, err := cs.CdnosV1alpha1().Cdnoss(n.Namespace).List(ctx, metav1.ListOptions{})
410+
if err != nil {
411+
return err
412+
}
413+
if len(list.Items) == 0 {
414+
log.V(1).Infof("No Cdnos CRs found in namespace %q", n.Namespace)
415+
} else {
416+
var crNames []string
417+
for _, item := range list.Items {
418+
crNames = append(crNames, item.Name)
419+
}
420+
log.Infof("Deleting Cdnos CRs in %q: %v", n.Namespace, crNames)
421+
for _, item := range list.Items {
422+
if err := cs.CdnosV1alpha1().Cdnoss(n.Namespace).Delete(ctx, item.Name, metav1.DeleteOptions{}); err != nil {
423+
return err
424+
}
425+
}
426+
}
427+
428+
// 2) Monitor Services associated with this node until the controller removes them.
429+
svcs, _ := n.servicesForNode(ctx)
430+
if len(svcs) == 0 {
431+
log.V(1).Infof("No Services found for node %q", n.Name())
432+
} else {
433+
var svcNames []string
434+
for _, s := range svcs {
435+
svcNames = append(svcNames, s.Name)
436+
}
437+
log.Infof("Monitoring Services for %q to be removed by controller: %v", n.Name(), svcNames)
438+
}
439+
// Wait for Services to be removed (longer on AKS due to LoadBalancer cleanup).
440+
waitDeadline := time.Now().Add(2 * time.Minute)
441+
if isAzureAKS(n.KubeClient) {
442+
waitDeadline = time.Now().Add(10 * time.Minute)
443+
log.Infof("AKS detected; waiting up to %v for all Services to be removed", time.Until(waitDeadline).Truncate(time.Second))
444+
} else {
445+
log.V(1).Infof("Azure AKS not detected; waiting up to %v for all Services to be removed", time.Until(waitDeadline).Truncate(time.Second))
446+
}
447+
start := time.Now()
448+
ticker := time.NewTicker(10 * time.Second)
449+
defer ticker.Stop()
450+
for {
451+
if time.Now().After(waitDeadline) {
452+
log.Warningf("Timeout waiting for Services removal; continuing teardown")
453+
break
454+
}
455+
svcs, _ = n.servicesForNode(ctx)
456+
remaining := len(svcs)
457+
if remaining == 0 {
458+
log.Infof("All Services for %q removed after %v", n.Name(), time.Since(start).Truncate(time.Second))
459+
break
460+
}
461+
select {
462+
case <-ticker.C:
463+
var names []string
464+
for _, s := range svcs {
465+
names = append(names, s.Name)
466+
}
467+
log.Infof("Waiting for Services removal for %q (%d remaining: %v, %v elapsed)", n.Name(), remaining, names, time.Since(start).Truncate(time.Second))
468+
default:
469+
}
470+
time.Sleep(2 * time.Second)
471+
}
472+
return nil
473+
}
474+
475+
// servicesForNode lists Services in the namespace that are associated with this node.
476+
// It matches by:
477+
// - name equals "service-<node>"
478+
// - label "name" equals node name (per controller)
479+
// - selector app == node name
480+
// - ownerReference is Cdnos/<node>
481+
func (n *Node) servicesForNode(ctx context.Context) ([]corev1.Service, error) {
482+
// Use a short-lived background context for API calls to avoid parent ctx deadline cancellations.
483+
_ = ctx // ctx is intentionally unused to avoid parent deadline cancellations
484+
listCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
485+
defer cancel()
486+
list, err := n.KubeClient.CoreV1().Services(n.Namespace).List(listCtx, metav1.ListOptions{})
487+
if err != nil {
488+
return nil, err
489+
}
490+
var out []corev1.Service
491+
wantName := fmt.Sprintf("service-%s", n.Name())
492+
for _, s := range list.Items {
493+
if s.Name == wantName {
494+
out = append(out, s)
495+
continue
496+
}
497+
if s.Labels["name"] == n.Name() {
498+
out = append(out, s)
499+
continue
500+
}
501+
if s.Spec.Selector != nil && s.Spec.Selector["app"] == n.Name() {
502+
out = append(out, s)
503+
continue
504+
}
505+
for _, or := range s.OwnerReferences {
506+
if or.Kind == "Cdnos" && or.Name == n.Name() {
507+
out = append(out, s)
508+
break
509+
}
510+
}
511+
}
512+
return out, nil
302513
}
303514

304515
func (n *Node) ResetCfg(ctx context.Context) error {
@@ -380,6 +591,46 @@ func init() {
380591
node.Vendor(tpb.Vendor_DRIVENETS, New)
381592
}
382593

594+
// isAzureAKS attempts to detect whether the current cluster is Azure AKS.
595+
// It returns true if any node has a providerID starting with "azure://"
596+
// or has any label prefixed with "kubernetes.azure.com/".
597+
func isAzureAKS(k kubernetes.Interface) bool {
598+
// Allow manual override for environments where listing nodes is restricted.
599+
if v := os.Getenv("KNE_FORCE_AKS"); v == "1" || strings.ToLower(v) == "true" {
600+
log.V(1).Infof("AKS detection overridden via KNE_FORCE_AKS")
601+
return true
602+
}
603+
if v := os.Getenv("KNE_FORCE_AZURE_ANNOTATIONS"); v == "1" || strings.ToLower(v) == "true" {
604+
log.V(1).Infof("AKS detection overridden via KNE_FORCE_AZURE_ANNOTATIONS")
605+
return true
606+
}
607+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
608+
defer cancel()
609+
nodes, err := k.CoreV1().Nodes().List(ctx, metav1.ListOptions{Limit: 1})
610+
if err != nil {
611+
log.V(1).Infof("AKS detection: failed to list nodes: %v", err)
612+
return false
613+
}
614+
if len(nodes.Items) == 0 {
615+
log.V(1).Infof("AKS detection: no nodes found in cluster")
616+
return false
617+
}
618+
for _, n := range nodes.Items {
619+
if strings.HasPrefix(n.Spec.ProviderID, "azure://") {
620+
log.V(1).Infof("AKS detection: node %q providerID %q indicates Azure", n.Name, n.Spec.ProviderID)
621+
return true
622+
}
623+
for key := range n.Labels {
624+
if strings.HasPrefix(key, "kubernetes.azure.com/") {
625+
log.V(1).Infof("AKS detection: node %q has Azure label %q", n.Name, key)
626+
return true
627+
}
628+
}
629+
}
630+
log.V(1).Infof("AKS detection: no Azure providerID or labels found on any node")
631+
return false
632+
}
633+
383634
func (n *Node) CreateConfig(ctx context.Context) (*corev1.Volume, error) {
384635
pb := n.Proto
385636
var data []byte

topo/topo.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,8 @@ func (m *Manager) Create(ctx context.Context, timeout time.Duration) (rerr error
270270
log.Warningf("Failed to start pod watcher: %v", err)
271271
} else {
272272
w.SetProgress(m.progress)
273+
// Only watch pods in this topology's namespace to avoid unrelated failures.
274+
w.AllowNamespaces(m.topo.Name)
273275
defer func() {
274276
cancel()
275277
rerr = w.Cleanup(rerr)

0 commit comments

Comments
 (0)