@@ -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+
261367func (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
304515func (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+
383634func (n * Node ) CreateConfig (ctx context.Context ) (* corev1.Volume , error ) {
384635 pb := n .Proto
385636 var data []byte
0 commit comments