• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

noironetworks / aci-containers / 11275

05 Nov 2025 01:53PM UTC coverage: 64.845% (+0.2%) from 64.6%
11275

Pull #1620

travis-pro

burhan20
Fix mutilpe same vlan NAD after controller restart:
During scale testing, if there are multiple EPGs attached to an aaep with same vlan,
it will result in only one NAD for the same vlan EPGs.
However, after controller restart, one extra NAD was created for the same vlan.

The fix makes sures to check if there is any already present NAD with same vlan attached to same aaep in thr cluster
A new NAD will be created only if no such NAD is found
Pull Request #1620: Fix mutilpe same vlan NAD after controller restart:

0 of 65 new or added lines in 1 file covered. (0.0%)

98 existing lines in 1 file now uncovered.

13358 of 20600 relevant lines covered (64.84%)

0.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/pkg/controller/aaepmonitor.go
1
// Copyright 2019 Cisco Systems, Inc.
2
//
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//     http://www.apache.org/licenses/LICENSE-2.0
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRATIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14

15
// Handlers for AaepMonitor CR updates.
16

17
package controller
18

19
import (
20
        "context"
21
        "crypto/sha256"
22
        "encoding/hex"
23
        "encoding/json"
24
        "fmt"
25
        "regexp"
26
        "strconv"
27
        "strings"
28

29
        amv1 "github.com/noironetworks/aci-containers/pkg/aaepmonitor/apis/aci.attachmentmonitor/v1"
30
        aaepmonitorclientset "github.com/noironetworks/aci-containers/pkg/aaepmonitor/clientset/versioned"
31
        "github.com/noironetworks/aci-containers/pkg/apicapi"
32

33
        nadapi "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1"
34
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
35
        "k8s.io/apimachinery/pkg/runtime"
36
        "k8s.io/apimachinery/pkg/watch"
37
        "k8s.io/client-go/tools/cache"
38
        "k8s.io/kubernetes/pkg/controller"
39
)
40

41
const (
42
        aaepMonitorCRDName = "aaepmonitors.aci.attachmentmonitor"
43
)
44

45
func (cont *AciController) queueAaepMonitorConfigByKey(key string) {
×
46
        cont.aaepMonitorConfigQueue.Add(key)
×
47
}
×
48

49
func aaepMonitorInit(cont *AciController, stopCh <-chan struct{}) {
×
50
        restconfig := cont.env.RESTConfig()
×
51
        aaepMonitorClient, err := aaepmonitorclientset.NewForConfig(restconfig)
×
52
        if err != nil {
×
53
                cont.log.Errorf("Failed to intialize aaepMonitorClient")
×
54
                return
×
55
        }
×
56

57
        cont.initAaepMonitorInformerFromClient(aaepMonitorClient)
×
58
        go cont.aaepMonitorInformer.Run(stopCh)
×
59
        go cont.processQueue(cont.aaepMonitorConfigQueue, cont.aaepMonitorInformer.GetIndexer(),
×
60
                func(obj interface{}) bool {
×
61
                        return cont.handleAaepMonitorConfigurationUpdate(obj)
×
62
                }, func(key string) bool {
×
63
                        return cont.handleAaepMonitorConfigurationDelete(key)
×
64
                }, nil, stopCh)
×
65
        cache.WaitForCacheSync(stopCh, cont.aaepMonitorInformer.HasSynced)
×
66
}
67

68
func (cont *AciController) initAaepMonitorInformerFromClient(
69
        aaepMonitorClient *aaepmonitorclientset.Clientset) {
×
70
        cont.initAaepMonitorInformerBase(
×
71
                &cache.ListWatch{
×
72
                        ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
×
73
                                return aaepMonitorClient.AciV1().AaepMonitors().List(context.TODO(), options)
×
74
                        },
×
75
                        WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
×
76
                                return aaepMonitorClient.AciV1().AaepMonitors().Watch(context.TODO(), options)
×
77
                        },
×
78
                })
79
}
80

81
func (cont *AciController) initAaepMonitorInformerBase(listWatch *cache.ListWatch) {
×
82
        cont.aaepMonitorInformer = cache.NewSharedIndexInformer(
×
83
                listWatch,
×
84
                &amv1.AaepMonitor{},
×
85
                controller.NoResyncPeriodFunc(),
×
86
                cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
×
87
        )
×
88
        cont.aaepMonitorInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
×
89
                AddFunc: func(obj interface{}) {
×
90
                        cont.aaepMonitorConfAdded(obj)
×
91
                },
×
92
                UpdateFunc: func(oldobj interface{}, newobj interface{}) {
×
93
                        cont.aaepMonitorConfUpdate(oldobj, newobj)
×
94
                },
×
95
                DeleteFunc: func(obj interface{}) {
×
96
                        cont.aaepMonitorConfDelete(obj)
×
97
                },
×
98
        })
99
}
100

101
func (cont *AciController) aaepMonitorConfAdded(obj interface{}) {
×
102
        aaepMonitorConfig, ok := obj.(*amv1.AaepMonitor)
×
103
        if !ok {
×
104
                cont.log.Error("aaepMonitorConfAdded: Bad object type")
×
105
                return
×
106
        }
×
107
        key, err := cache.MetaNamespaceKeyFunc(aaepMonitorConfig)
×
108
        if err != nil {
×
109
                return
×
110
        }
×
111
        cont.queueAaepMonitorConfigByKey(key)
×
112
}
113

114
func (cont *AciController) aaepMonitorConfUpdate(oldobj interface{}, newobj interface{}) {
×
115
        newAaepMonitorConfig := newobj.(*amv1.AaepMonitor)
×
116

×
117
        key, err := cache.MetaNamespaceKeyFunc(newAaepMonitorConfig)
×
118
        if err != nil {
×
119
                return
×
120
        }
×
121
        cont.queueAaepMonitorConfigByKey(key)
×
122
}
123

124
func (cont *AciController) aaepMonitorConfDelete(obj interface{}) {
×
125
        cont.indexMutex.Lock()
×
126
        defer cont.indexMutex.Unlock()
×
127
        aaepMonitorConfig, ok := obj.(*amv1.AaepMonitor)
×
128

×
129
        if !ok {
×
130
                deletedState, ok := obj.(cache.DeletedFinalStateUnknown)
×
131
                if !ok {
×
132
                        cont.log.Errorf("Received unexpected object: ")
×
133
                        return
×
134
                }
×
135
                aaepMonitorConfig, ok = deletedState.Obj.(*amv1.AaepMonitor)
×
136
                if !ok {
×
137
                        cont.log.Errorf("DeletedFinalStateUnknown contained non-aaepmonitorconfiguration object: %v", deletedState.Obj)
×
138
                        return
×
139
                }
×
140
        }
141

142
        key, err := cache.MetaNamespaceKeyFunc(aaepMonitorConfig)
×
143
        if err != nil {
×
144
                return
×
145
        }
×
146
        cont.queueAaepMonitorConfigByKey("DELETED_" + key)
×
147
}
148

149
func (cont *AciController) handleAaepMonitorConfigurationUpdate(obj interface{}) bool {
×
150
        aaepMonitorConfig, ok := obj.(*amv1.AaepMonitor)
×
151
        if !ok {
×
152
                cont.log.Error("handleAaepMonitorConfigurationUpdate: Bad object type")
×
153
                return false
×
154
        }
×
155

156
        cont.log.Infof("Started processing of aaepmonitor CR creation/modification")
×
157
        addedAaeps, removedAaeps := cont.getAaepDiff(aaepMonitorConfig.Spec.Aaeps)
×
158
        for _, aaepName := range addedAaeps {
×
159
                cont.reconcileNadData(aaepName)
×
160
        }
×
161

162
        for _, aaepName := range removedAaeps {
×
163
                cont.cleanAnnotationSubscriptions(aaepName)
×
164

×
165
                cont.indexMutex.Lock()
×
166
                aaepEpgAttachDataMap := cont.sharedAaepMonitor[aaepName]
×
167
                delete(cont.sharedAaepMonitor, aaepName)
×
168

×
169
                for epgDn, aaepEpgAttachData := range aaepEpgAttachDataMap {
×
170
                        cont.deleteNetworkAttachmentDefinition(aaepName, epgDn, aaepEpgAttachData, "AaepRemovedFromCR")
×
171
                }
×
172
                cont.indexMutex.Unlock()
×
173
        }
174
        cont.log.Infof("Completed processing of aaepmonitor CR creation/modification")
×
175
        return false
×
176
}
177

178
func (cont *AciController) handleAaepMonitorConfigurationDelete(key string) bool {
×
179
        cont.indexMutex.Lock()
×
180
        defer cont.indexMutex.Unlock()
×
181
        cont.log.Infof("Started processing of aaepmonitor CR deletion")
×
182
        for aaepName, aaepEpgAttachDataMap := range cont.sharedAaepMonitor {
×
183
                cont.cleanAnnotationSubscriptions(aaepName)
×
184

×
185
                for epgDn, aaepEpgAttachData := range aaepEpgAttachDataMap {
×
186
                        cont.deleteNetworkAttachmentDefinition(aaepName, epgDn, aaepEpgAttachData, "CRDeleted")
×
187
                }
×
188
        }
189

190
        cont.sharedAaepMonitor = make(map[string]map[string]*AaepEpgAttachData)
×
191
        cont.log.Infof("Completed processing of aaepmonitor CR deletion")
×
192
        return false
×
193
}
194

195
func (cont *AciController) handleAaepEpgAttach(infraRsObj apicapi.ApicObject) {
×
196
        infraRsObjDn := infraRsObj.GetDn()
×
197
        aaepName := cont.matchesAEPFilter(infraRsObjDn)
×
198
        cont.log.Infof("Started processing of EPG: %s attached with AAEP: %s", infraRsObj.GetAttrStr("tDn"), aaepName)
×
199
        if aaepName == "" {
×
200
                cont.log.Debugf("Unable to find AAEP from %s in monitoring list", infraRsObjDn)
×
201
                return
×
202
        }
×
203

204
        state := infraRsObj.GetAttrStr("state")
×
205
        if state != "formed" {
×
206
                cont.log.Debugf("Skipping NAD creation: %s is with state: %s", infraRsObjDn, state)
×
207
                return
×
208
        }
×
209

210
        epgDn := infraRsObj.GetAttrStr("tDn")
×
211
        encap := infraRsObj.GetAttrStr("encap")
×
212
        vlanID := cont.getVlanId(encap)
×
213

×
214
        if cont.checkDuplicateAaepEpgAttachRequest(aaepName, epgDn, vlanID) {
×
215
                cont.log.Infof("AAEP %s EPG %s attachment data already exists", aaepName, epgDn)
×
216
                return
×
217
        }
×
218

219
        cont.indexMutex.Lock()
×
220
        oldAaepMonitorData := cont.getAaepEpgAttachDataLocked(aaepName, epgDn)
×
221
        cont.indexMutex.Unlock()
×
222

×
223
        if cont.checkVlanUsedInCluster(vlanID) {
×
224
                // This is needed when user updates vlan to the vlan already used in cluster
×
225
                cont.handleOldNadDeletion(aaepName, epgDn, oldAaepMonitorData, "AaepEpgAttachedWithVlanInUse")
×
226
                cont.log.Errorf("Skipping NAD creation: VLAN %d is already used in cluster", vlanID)
×
227
                return
×
228
        }
×
229

230
        defer cont.apicConn.AddImmediateSubscriptionDnLocked(epgDn, []string{"tagAnnotation"}, cont.handleAnnotationAdded,
×
231
                cont.handleAnnotationDeleted)
×
232

×
233
        epgVlanMap := &EpgVlanMap{
×
234
                epgDn:     epgDn,
×
235
                encapVlan: vlanID,
×
236
        }
×
237

×
238
        aaepEpgAttachData := cont.collectNadData(epgVlanMap)
×
239
        if aaepEpgAttachData == nil {
×
240
                return
×
241
        }
×
242

243
        if cont.checkIfEpgWithOverlappingVlan(aaepName, vlanID, epgDn) {
×
244
                // This is needed when user updates vlan from non-overlapping to overlapping
×
245
                cont.handleOldNadDeletion(aaepName, epgDn, oldAaepMonitorData, "AaepEpgAttachedWithOverlappingVlan")
×
246

×
247
                // Add new entry with nadCreated as false
×
248
                cont.indexMutex.Lock()
×
249
                if cont.sharedAaepMonitor[aaepName] == nil {
×
250
                        cont.sharedAaepMonitor[aaepName] = make(map[string]*AaepEpgAttachData)
×
251
                }
×
252
                aaepEpgAttachData.nadCreated = false
×
253
                cont.sharedAaepMonitor[aaepName][epgDn] = aaepEpgAttachData
×
254
                cont.indexMutex.Unlock()
×
255
                cont.log.Errorf("Skipping NAD creation: EPG %s with AAEP %s has overlapping VLAN %d", epgDn, aaepName, vlanID)
×
256
                return
×
257
        }
258
        cont.syncNADsWithAciState(aaepName, epgDn, oldAaepMonitorData, aaepEpgAttachData, "AaepEpgAttached")
×
259
        cont.log.Infof("Completed processing of EPG: %s attached with AAEP: %s", epgDn, aaepName)
×
260
}
261

262
func (cont *AciController) handleAaepEpgDetach(infraRsObjDn string) {
×
263
        aaepName := cont.matchesAEPFilter(infraRsObjDn)
×
264
        if aaepName == "" {
×
265
                cont.log.Debugf("Unable to find AAEP from %s in monitoring list", infraRsObjDn)
×
266
                return
×
267
        }
×
268

269
        epgDn := cont.getEpgDnFromInfraRsDn(infraRsObjDn)
×
270

×
271
        if epgDn == "" {
×
272
                cont.log.Errorf("Unable to find EPG from %s", infraRsObjDn)
×
273
                return
×
274
        }
×
275

276
        cont.log.Infof("Started processing of EPG: %s detached from AAEP: %s", infraRsObjDn, aaepName)
×
277
        // Need to check if EPG is not attached with any other AAEP
×
278
        if !cont.isEpgAttachedWithAaep(epgDn) {
×
279
                cont.apicConn.UnsubscribeImmediateDnLocked(epgDn, []string{"tagAnnotation"})
×
280
        }
×
281

282
        cont.indexMutex.Lock()
×
283
        aaepEpgAttachData := cont.getAaepEpgAttachDataLocked(aaepName, epgDn)
×
284
        cont.indexMutex.Unlock()
×
285

×
286
        if aaepEpgAttachData == nil {
×
287
                cont.log.Debugf("Monitoring data not available for EPG %s with AAEP %s", epgDn, aaepName)
×
288
                return
×
289
        }
×
290
        if !cont.namespaceChecks(aaepEpgAttachData.namespaceName, epgDn) {
×
291
                cont.log.Debugf("Namespace %s not found", aaepEpgAttachData.namespaceName)
×
292
                return
×
293
        }
×
294

295
        cont.handleOldNadDeletion(aaepName, epgDn, aaepEpgAttachData, "AaepEpgDetached")
×
296
        cont.log.Infof("Completed processing of EPG: %s detached from AAEP: %s", epgDn, aaepName)
×
297
}
298

299
func (cont *AciController) handleAnnotationAdded(obj apicapi.ApicObject) bool {
×
300
        annotationDn := obj.GetDn()
×
301
        epgDn := annotationDn[:strings.Index(annotationDn, "/annotationKey-")]
×
302
        aaepMonitorDataMap := cont.getAaepMonitoringDataForEpg(epgDn)
×
303

×
304
        cont.log.Infof("Started processing of EPG: %s annotation %s addition/modification", epgDn, annotationDn)
×
305
        for aaepName, aaepMonitorData := range aaepMonitorDataMap {
×
306
                cont.indexMutex.Lock()
×
307
                oldAaepMonitorData := cont.getAaepEpgAttachDataLocked(aaepName, epgDn)
×
308
                cont.indexMutex.Unlock()
×
309

×
310
                if len(aaepMonitorData) == 0 {
×
311
                        // No new monitoring data available for this EPG with this AAEP but old data exists
×
312
                        // delete old NAD if exists and remove from monitoring map
×
313
                        // create NAD for next EPG with old VLAN
×
314
                        cont.handleOldNadDeletion(aaepName, epgDn, oldAaepMonitorData, "NamespaceAnnotationAdded")
×
315
                        cont.log.Debugf("Insufficient data for NAD creation: Monitoring data not available for EPG %s with AAEP %s", epgDn, aaepName)
×
316
                        continue
×
317
                }
318

319
                aaepEpgAttachData, exists := aaepMonitorData[epgDn]
×
320
                if !exists || aaepEpgAttachData == nil {
×
321
                        cont.handleOldNadDeletion(aaepName, epgDn, oldAaepMonitorData, "NamespaceAnnotationAdded")
×
322
                        cont.log.Debugf("Insufficient data for NAD creation: Monitoring data not available for EPG %s with AAEP %s", epgDn, aaepName)
×
323
                        continue
×
324
                }
325

326
                if !cont.checkMonitorDataModified(oldAaepMonitorData, aaepEpgAttachData) {
×
327
                        cont.log.Debugf("No changes in annotation for EPG %s with AAEP %s", epgDn, aaepName)
×
328
                        continue
×
329
                }
330

331
                if !cont.namespaceChecks(aaepEpgAttachData.namespaceName, epgDn) {
×
332
                        // Namespace not exist, need to delete old NAD if exists and remove from monitoring map
×
333
                        // create NAD for next EPG with old VLAN
×
334
                        cont.handleOldNadDeletion(aaepName, epgDn, oldAaepMonitorData, "NamespaceAnnotationAdded")
×
335
                        cont.log.Debugf("Insufficient data in annotation for NAD creation: Namespace not exist, in case of EPG %s with AAEP %s", epgDn, aaepName)
×
336
                        continue
×
337
                }
338

339
                // This is needed when user updates annotation and VLAN is already overlapping
340
                if cont.checkIfEpgWithOverlappingVlan(aaepName, aaepEpgAttachData.encapVlan, epgDn) {
×
341
                        // Add new entry with nadCreated as false
×
342
                        aaepEpgAttachData.nadCreated = false
×
343
                        cont.indexMutex.Lock()
×
344
                        if cont.sharedAaepMonitor[aaepName] == nil {
×
345
                                cont.sharedAaepMonitor[aaepName] = make(map[string]*AaepEpgAttachData)
×
346
                        }
×
347
                        cont.sharedAaepMonitor[aaepName][epgDn] = aaepEpgAttachData
×
348
                        cont.indexMutex.Unlock()
×
349
                        cont.log.Debugf("Skipping EPG annotation add handling: EPG %s with AAEP %s has overlapping VLAN %d", epgDn,
×
350
                                aaepName, aaepEpgAttachData.encapVlan)
×
351
                        continue
×
352
                }
353

354
                cont.syncNADsWithAciState(aaepName, epgDn, oldAaepMonitorData, aaepEpgAttachData, "NamespaceAnnotationAdded")
×
355
        }
356
        cont.log.Infof("Completed processing annotation addition/modification for EPG: %s, AnnotationDn: %s", epgDn, annotationDn)
×
357
        return true
×
358
}
359

360
func (cont *AciController) handleAnnotationDeleted(annotationDn string) {
×
361
        epgDn := annotationDn[:strings.Index(annotationDn, "/annotationKey-")]
×
362
        cont.log.Infof("Started processing of EPG: %s annotation %s deletion", epgDn, annotationDn)
×
363
        aaepMonitorDataMap := cont.getAaepMonitoringDataForEpg(epgDn)
×
364

×
365
        for aaepName, aaepMonitorData := range aaepMonitorDataMap {
×
366
                cont.indexMutex.Lock()
×
367
                oldAaepMonitorData := cont.getAaepEpgAttachDataLocked(aaepName, epgDn)
×
368
                cont.indexMutex.Unlock()
×
369

×
370
                if oldAaepMonitorData == nil {
×
371
                        cont.log.Debugf("Monitoring data not available for EPG %s with AAEP %s", epgDn, aaepName)
×
372
                        continue
×
373
                }
374

375
                if oldAaepMonitorData.nadCreated == false {
×
376
                        cont.indexMutex.Lock()
×
377
                        delete(cont.sharedAaepMonitor[aaepName], epgDn)
×
378
                        cont.indexMutex.Unlock()
×
379
                        continue
×
380
                }
381

382
                if aaepMonitorData == nil {
×
383
                        cont.indexMutex.Lock()
×
384
                        delete(cont.sharedAaepMonitor[aaepName], epgDn)
×
385
                        cont.deleteNetworkAttachmentDefinition(aaepName, epgDn, oldAaepMonitorData, "NamespaceAnnotationRemoved")
×
386
                        cont.indexMutex.Unlock()
×
387
                        cont.createNadForNextEpg(aaepName, oldAaepMonitorData.encapVlan)
×
388
                        continue
×
389
                }
390

391
                aaepEpgAttachData, exists := aaepMonitorData[epgDn]
×
392
                if !exists || aaepEpgAttachData == nil {
×
393
                        cont.indexMutex.Lock()
×
394
                        delete(cont.sharedAaepMonitor[aaepName], epgDn)
×
395
                        cont.deleteNetworkAttachmentDefinition(aaepName, epgDn, oldAaepMonitorData, "NamespaceAnnotationRemoved")
×
396
                        cont.indexMutex.Unlock()
×
397
                        cont.createNadForNextEpg(aaepName, oldAaepMonitorData.encapVlan)
×
398
                        continue
×
399
                }
400

401
                cont.syncNADsWithAciState(aaepName, epgDn, oldAaepMonitorData, aaepEpgAttachData, "NamespaceAnnotationRemoved")
×
402
        }
403
        cont.log.Infof("Completed processing annotation deletion for EPG: %s, AnnotationDn: %s", epgDn, annotationDn)
×
404
}
405

406
func (cont *AciController) collectNadData(epgVlanMap *EpgVlanMap) *AaepEpgAttachData {
×
407
        epgDn := epgVlanMap.epgDn
×
408
        epgAnnotations := cont.getEpgAnnotations(epgDn)
×
409
        namespaceName, nadName := cont.getSpecificEPGAnnotation(epgAnnotations)
×
410

×
411
        if !cont.namespaceChecks(namespaceName, epgDn) {
×
412
                cont.log.Debugf("Namespace not exist, in case of EPG %s", epgDn)
×
413
                return nil
×
414
        }
×
415

416
        aaepMonitoringData := &AaepEpgAttachData{
×
417
                encapVlan:     epgVlanMap.encapVlan,
×
418
                nadName:       nadName,
×
419
                namespaceName: namespaceName,
×
420
                nadCreated:    true,
×
421
        }
×
422

×
423
        return aaepMonitoringData
×
424
}
425

426
func (cont *AciController) getAaepEpgAttachDataLocked(aaepName, epgDn string) *AaepEpgAttachData {
×
427
        aaepEpgAttachDataMap, exists := cont.sharedAaepMonitor[aaepName]
×
428
        if !exists || aaepEpgAttachDataMap == nil {
×
429
                cont.log.Debugf("AAEP %s EPG %s attachment data not found", aaepName, epgDn)
×
430
                return nil
×
431
        }
×
432

433
        aaepEpgAttachData, exists := aaepEpgAttachDataMap[epgDn]
×
434
        if !exists || aaepEpgAttachData == nil {
×
435
                cont.log.Debugf("AAEP %s EPG %s attachment data not found", aaepName, epgDn)
×
436
                return nil
×
437
        }
×
438

439
        cont.log.Infof("Found attachment data: %v for EPG : %s AAEP: %s", *aaepEpgAttachData, epgDn, aaepName)
×
440
        return aaepEpgAttachData
×
441
}
442

443
func (cont *AciController) matchesAEPFilter(infraRsObjDn string) string {
×
444
        cont.indexMutex.Lock()
×
445
        defer cont.indexMutex.Unlock()
×
446
        var aaepName string
×
447
        for aaepName = range cont.sharedAaepMonitor {
×
448
                expectedPrefix := fmt.Sprintf("uni/infra/attentp-%s/", aaepName)
×
449
                if strings.HasPrefix(infraRsObjDn, expectedPrefix) {
×
450
                        return aaepName
×
451
                }
×
452
        }
453
        return ""
×
454
}
455

456
func (cont *AciController) getEpgDnFromInfraRsDn(infraRsObjDn string) string {
×
457
        re := regexp.MustCompile(`\[(.*?)\]`)
×
458
        match := re.FindStringSubmatch(infraRsObjDn)
×
459

×
460
        var epgDn string
×
461
        if len(match) > 1 {
×
462
                epgDn = match[1]
×
463
                return epgDn
×
464
        }
×
465

466
        return epgDn
×
467
}
468

469
func (cont *AciController) getAaepMonitoringDataForEpg(epgDn string) map[string]map[string]*AaepEpgAttachData {
×
470
        aaepEpgAttachData := make(map[string]map[string]*AaepEpgAttachData)
×
471

×
472
        cont.indexMutex.Lock()
×
473
        defer cont.indexMutex.Unlock()
×
474
        for aaepName := range cont.sharedAaepMonitor {
×
475
                encap := cont.getEncapFromAaepEpgAttachObj(aaepName, epgDn)
×
476

×
477
                if encap != "" {
×
478
                        vlanID := cont.getVlanId(encap)
×
479
                        epgVlanMap := EpgVlanMap{
×
480
                                epgDn:     epgDn,
×
481
                                encapVlan: vlanID,
×
482
                        }
×
483
                        if aaepEpgAttachData[aaepName] == nil {
×
484
                                aaepEpgAttachData[aaepName] = make(map[string]*AaepEpgAttachData)
×
485
                        }
×
486
                        nadData := cont.collectNadData(&epgVlanMap)
×
487
                        if nadData != nil {
×
488
                                aaepEpgAttachData[aaepName][epgDn] = nadData
×
489
                        }
×
490
                }
491
        }
492

493
        return aaepEpgAttachData
×
494
}
495

496
func (cont *AciController) cleanAnnotationSubscriptions(aaepName string) {
×
497
        epgVlanMapList := cont.getAaepEpgAttObjDetails(aaepName)
×
498
        if epgVlanMapList == nil {
×
499
                return
×
500
        }
×
501

502
        for _, epgVlanMap := range epgVlanMapList {
×
503
                cont.apicConn.UnsubscribeImmediateDnLocked(epgVlanMap.epgDn, []string{"tagAnnotation"})
×
504
        }
×
505
}
506

507
func (cont *AciController) syncNADsWithAciState(aaepName string, epgDn string, oldAaepEpgAttachData,
508
        aaepEpgAttachData *AaepEpgAttachData, syncReason string) {
×
509
        if oldAaepEpgAttachData == nil {
×
510
                cont.handleNewNadCreation(aaepName, epgDn, aaepEpgAttachData, syncReason)
×
511
        } else {
×
512
                if cont.checkAnnotationModified(oldAaepEpgAttachData, aaepEpgAttachData) {
×
513
                        if oldAaepEpgAttachData.namespaceName != aaepEpgAttachData.namespaceName {
×
514
                                cont.indexMutex.Lock()
×
515
                                if oldAaepEpgAttachData.nadCreated == true {
×
516
                                        cont.deleteNetworkAttachmentDefinition(aaepName, epgDn, oldAaepEpgAttachData, syncReason)
×
517
                                }
×
518
                                delete(cont.sharedAaepMonitor[aaepName], epgDn)
×
519
                                cont.indexMutex.Unlock()
×
520
                        }
521
                        cont.handleNewNadCreation(aaepName, epgDn, aaepEpgAttachData, syncReason)
×
522
                        return
×
523
                }
524

525
                if oldAaepEpgAttachData.encapVlan != aaepEpgAttachData.encapVlan {
×
526
                        cont.handleOldNadDeletion(aaepName, epgDn, oldAaepEpgAttachData, syncReason)
×
527
                        cont.handleNewNadCreation(aaepName, epgDn, aaepEpgAttachData, syncReason)
×
528
                }
×
529
        }
530
}
531

532
func (cont *AciController) addDeferredNADs(namespaceName string) {
×
533
        aaepEpgVlanMap := make(map[string][]EpgVlanMap)
×
534

×
535
        // Collect all AAEP EPG attachment details
×
536
        cont.indexMutex.Lock()
×
537
        for aaepName := range cont.sharedAaepMonitor {
×
538
                epgVlanMapList := cont.getAaepEpgAttObjDetails(aaepName)
×
539

×
540
                if epgVlanMapList == nil {
×
541
                        continue
×
542
                }
543

544
                aaepEpgVlanMap[aaepName] = epgVlanMapList
×
545
        }
546
        cont.indexMutex.Unlock()
×
547

×
548
        // Process each AAEP EPG attachment details
×
549
        for aaepName, epgVlanMapList := range aaepEpgVlanMap {
×
550
                for _, epgVlanMap := range epgVlanMapList {
×
551
                        aaepEpgAttachData := cont.collectNadData(&epgVlanMap)
×
552
                        if aaepEpgAttachData == nil || aaepEpgAttachData.namespaceName != namespaceName {
×
553
                                continue
×
554
                        }
555

556
                        if cont.checkIfEpgWithOverlappingVlan(aaepName, aaepEpgAttachData.encapVlan, epgVlanMap.epgDn) {
×
557
                                cont.indexMutex.Lock()
×
558
                                if cont.sharedAaepMonitor[aaepName] == nil {
×
559
                                        cont.sharedAaepMonitor[aaepName] = make(map[string]*AaepEpgAttachData)
×
560
                                }
×
561
                                aaepEpgAttachData.nadCreated = false
×
562
                                cont.sharedAaepMonitor[aaepName][epgVlanMap.epgDn] = aaepEpgAttachData
×
563
                                cont.indexMutex.Unlock()
×
564

×
565
                                cont.log.Errorf("Skipping NAD creation: EPG %s with AAEP %s has overlapping VLAN %d", epgVlanMap.epgDn,
×
566
                                        aaepName, aaepEpgAttachData.encapVlan)
×
567
                                continue
×
568
                        } else if cont.checkVlanUsedInCluster(aaepEpgAttachData.encapVlan) {
×
569
                                cont.log.Errorf("Skipping NAD creation: VLAN %d is already used in cluster", aaepEpgAttachData.encapVlan)
×
570
                                continue
×
571
                        } else {
×
572
                                epgDn := epgVlanMap.epgDn
×
573
                                cont.handleNewNadCreation(aaepName, epgDn, aaepEpgAttachData, "NamespaceCreated")
×
574
                        }
×
575
                }
576
        }
577
}
578

579
func (cont *AciController) cleanNADs(namespaceName string) {
×
580
        aaepMonitorDataToDelete := make(map[string]map[string]*AaepEpgAttachData)
×
581
        cont.indexMutex.Lock()
×
582
        for aaepName := range cont.sharedAaepMonitor {
×
583
                aaepEpgAttachDataMap, exists := cont.sharedAaepMonitor[aaepName]
×
584
                if !exists || aaepEpgAttachDataMap == nil {
×
585
                        continue
×
586
                }
587

588
                for epgDn, aaepEpgAttachData := range aaepEpgAttachDataMap {
×
589
                        if aaepEpgAttachData.namespaceName == namespaceName {
×
590
                                nadName := cont.generateDefaultNadName(aaepName, epgDn)
×
591
                                removedAnnotation := cont.removeManagedByAnnotationFromNAD(nadName, namespaceName)
×
592
                                if !removedAnnotation {
×
593
                                        continue
×
594
                                }
595
                                delete(cont.sharedAaepMonitor[aaepName], epgDn)
×
596
                                if aaepEpgAttachData.nadCreated {
×
597
                                        cont.deleteNetworkAttachmentDefinition(aaepName, epgDn, aaepEpgAttachData, "NamespaceDeleted")
×
598
                                }
×
599
                                aaepMonitorDataToDelete[aaepName] = aaepEpgAttachDataMap
×
600
                        }
601
                }
602
        }
603
        cont.indexMutex.Unlock()
×
604

×
605
        for aaepName, aaepEpgAttachDataMap := range aaepMonitorDataToDelete {
×
606
                for _, aaepEpgAttachData := range aaepEpgAttachDataMap {
×
607
                        cont.createNadForNextEpg(aaepName, aaepEpgAttachData.encapVlan)
×
608
                }
×
609
        }
610
}
611

612
func (cont *AciController) getAaepEpgAttObjDetails(aaepName string) []EpgVlanMap {
×
613
        uri := fmt.Sprintf("/api/node/mo/uni/infra/attentp-%s.json?query-target=subtree&target-subtree-class=infraRsFuncToEpg", aaepName)
×
614

×
615
        resp, err := cont.apicConn.GetApicResponse(uri)
×
616
        if err != nil {
×
617
                cont.log.Errorf("Failed to get response from APIC: %v", err)
×
618
                return nil
×
619
        }
×
620

621
        if len(resp.Imdata) == 0 {
×
622
                cont.log.Debugf("Can't find EPGs attached with AAEP %s", aaepName)
×
623
                return nil
×
624
        }
×
625

626
        epgVlanMapList := make([]EpgVlanMap, 0)
×
627
        for _, respImdata := range resp.Imdata {
×
628
                aaepEpgAttachObj, ok := respImdata["infraRsFuncToEpg"]
×
629
                if !ok {
×
630
                        cont.log.Debugf("Empty AAEP EPG attachment object")
×
631
                        continue
×
632
                }
633

634
                if state, hasState := aaepEpgAttachObj.Attributes["state"].(string); hasState {
×
635
                        if state != "formed" {
×
636
                                aaepEpgAttchDn := aaepEpgAttachObj.Attributes["dn"].(string)
×
637
                                cont.log.Debugf("%s is with state: %s", aaepEpgAttchDn, state)
×
638
                                continue
×
639
                        }
640
                }
641
                vlanID := 0
×
642
                if encap, hasEncap := aaepEpgAttachObj.Attributes["encap"].(string); hasEncap {
×
643
                        vlanID = cont.getVlanId(encap)
×
644
                }
×
645

646
                epgVlanMap := EpgVlanMap{
×
647
                        epgDn:     aaepEpgAttachObj.Attributes["tDn"].(string),
×
648
                        encapVlan: vlanID,
×
649
                }
×
650
                epgVlanMapList = append(epgVlanMapList, epgVlanMap)
×
651
        }
652

653
        return epgVlanMapList
×
654
}
655

656
func (cont *AciController) getEpgAnnotations(epgDn string) map[string]string {
×
657
        uri := fmt.Sprintf("/api/node/mo/%s.json?query-target=subtree&target-subtree-class=tagAnnotation", epgDn)
×
658
        resp, err := cont.apicConn.GetApicResponse(uri)
×
659
        if err != nil {
×
660
                cont.log.Errorf("Failed to get response from APIC: %v", err)
×
661
                return nil
×
662
        }
×
663

664
        annotationsMap := make(map[string]string)
×
665
        for _, respImdata := range resp.Imdata {
×
666
                annotationObj, ok := respImdata["tagAnnotation"]
×
667
                if !ok {
×
668
                        cont.log.Debugf("Empty tag annotation of EPG %s", epgDn)
×
669
                        continue
×
670
                }
671

672
                key := annotationObj.Attributes["key"].(string)
×
673
                annotationsMap[key] = annotationObj.Attributes["value"].(string)
×
674
        }
675

676
        return annotationsMap
×
677
}
678

679
func (cont *AciController) getSpecificEPGAnnotation(annotations map[string]string) (string, string) {
×
680
        namespaceNameAnnotationKey := cont.config.CnoIdentifier + "-namespace"
×
681
        namespaceName, exists := annotations[namespaceNameAnnotationKey]
×
682
        if !exists {
×
683
                cont.log.Debugf("Annotation with key '%s' not found", namespaceNameAnnotationKey)
×
684
        }
×
685

686
        nadNameAnnotationKey := cont.config.CnoIdentifier + "-nad"
×
687
        nadName, exists := annotations[nadNameAnnotationKey]
×
688
        if !exists {
×
689
                cont.log.Debugf("Annotation with key '%s' not found", nadNameAnnotationKey)
×
690
        }
×
691
        return namespaceName, nadName
×
692
}
693

694
func (cont *AciController) namespaceChecks(namespaceName string, epgDn string) bool {
×
695
        if namespaceName == "" {
×
696
                cont.log.Debugf("Defering NAD operation for EPG %s: Namespace name not provided in EPG annotation", epgDn)
×
697
                return false
×
698
        }
×
699

700
        kubeClient := cont.env.(*K8sEnvironment).kubeClient
×
701
        _, err := kubeClient.CoreV1().Namespaces().Get(context.TODO(), namespaceName, metav1.GetOptions{})
×
702
        namespaceExists := err == nil
×
703
        if !namespaceExists {
×
704
                cont.log.Debugf("Defering NAD operation for EPG %s: Namespace %s not exists", epgDn, namespaceName)
×
705
                return false
×
706
        }
×
707

708
        return true
×
709
}
710

711
func (cont *AciController) reconcileNadData(aaepName string) {
×
712
        epgVlanMapList := cont.getAaepEpgAttObjDetails(aaepName)
×
713

×
714
        for _, epgVlanMap := range epgVlanMapList {
×
715
                if cont.checkVlanUsedInCluster(epgVlanMap.encapVlan) {
×
716
                        cont.log.Errorf("Skipping NAD creation: VLAN %d is already used in cluster", epgVlanMap.encapVlan)
×
717
                        continue
×
718
                }
719
                aaepEpgAttachData := cont.collectNadData(&epgVlanMap)
×
720
                if aaepEpgAttachData == nil {
×
721
                        cont.apicConn.AddImmediateSubscriptionDnLocked(epgVlanMap.epgDn,
×
722
                                []string{"tagAnnotation"}, cont.handleAnnotationAdded,
×
723
                                cont.handleAnnotationDeleted)
×
724
                        continue
×
725
                }
726

727
                epgDn := epgVlanMap.epgDn
×
728
                if cont.checkIfEpgWithOverlappingVlan(aaepName, aaepEpgAttachData.encapVlan, epgDn) {
×
729
                        aaepEpgAttachData.nadCreated = false
×
730
                        cont.indexMutex.Lock()
×
731
                        if cont.sharedAaepMonitor[aaepName] == nil {
×
732
                                cont.sharedAaepMonitor[aaepName] = make(map[string]*AaepEpgAttachData)
×
733
                        }
×
734
                        cont.sharedAaepMonitor[aaepName][epgDn] = aaepEpgAttachData
×
735
                        cont.indexMutex.Unlock()
×
736
                        cont.apicConn.AddImmediateSubscriptionDnLocked(epgDn,
×
737
                                []string{"tagAnnotation"}, cont.handleAnnotationAdded,
×
738
                                cont.handleAnnotationDeleted)
×
739
                        cont.log.Errorf("Skipping NAD creation: EPG %s with AAEP %s has overlapping VLAN %d",
×
740
                                epgDn, aaepName, aaepEpgAttachData.encapVlan)
×
741
                        continue
×
742
                }
743

NEW
744
                OldepgDn := cont.handleNewNadCreation(aaepName, epgDn, aaepEpgAttachData, "AaepAddedInCR")
×
NEW
745
                if OldepgDn != "" {
×
NEW
746
                        cont.apicConn.AddImmediateSubscriptionDnLocked(OldepgDn,
×
NEW
747
                                []string{"tagAnnotation"}, cont.handleAnnotationAdded,
×
NEW
748
                                cont.handleAnnotationDeleted)
×
NEW
749
                }
×
750

751
                cont.apicConn.AddImmediateSubscriptionDnLocked(epgDn,
×
752
                        []string{"tagAnnotation"}, cont.handleAnnotationAdded,
×
753
                        cont.handleAnnotationDeleted)
×
754
        }
755

756
        cont.indexMutex.Lock()
×
757
        if _, ok := cont.sharedAaepMonitor[aaepName]; !ok {
×
758
                cont.sharedAaepMonitor[aaepName] = make(map[string]*AaepEpgAttachData)
×
759
        }
×
760
        cont.indexMutex.Unlock()
×
761
}
762

763
// clean converts a string to lowercase, removes underscores and dots,
764
// and replaces any other invalid character with a hyphen.
765
func cleanApicResourceNames(apicResource string) string {
×
766
        apicResource = strings.ToLower(apicResource)
×
767
        var stringBuilder strings.Builder
×
768
        for _, character := range apicResource {
×
769
                switch {
×
770
                case character == '_' || character == '.':
×
771
                        continue
×
772
                case (character >= 'a' && character <= 'z') || (character >= '0' && character <= '9') || character == '-':
×
773
                        stringBuilder.WriteRune(character)
×
774
                default:
×
775
                        stringBuilder.WriteRune('-')
×
776
                }
777
        }
778
        return strings.Trim(stringBuilder.String(), "-")
×
779
}
780

781
func (cont *AciController) generateDefaultNadName(aaepName, epgDn string) string {
×
782
        parts := strings.Split(epgDn, "/")
×
783

×
784
        tenant := parts[1][3:]
×
785
        appProfile := parts[2][3:]
×
786
        epgName := parts[3][4:]
×
787

×
788
        apicResourceNames := tenant + appProfile + epgName + aaepName
×
789
        hashBytes := sha256.Sum256([]byte(apicResourceNames))
×
790
        hash := hex.EncodeToString(hashBytes[:])[:16]
×
791

×
792
        return fmt.Sprintf("%s-%s-%s-%s",
×
793
                cleanApicResourceNames(tenant), cleanApicResourceNames(appProfile), cleanApicResourceNames(epgName), hash)
×
794
}
×
795

796
func (cont *AciController) isNADUpdateRequired(aaepName string, epgDn string, nadData *AaepEpgAttachData,
797
        existingNAD *nadapi.NetworkAttachmentDefinition) bool {
×
798
        vlanID := nadData.encapVlan
×
799
        namespaceName := nadData.namespaceName
×
800
        customNadName := nadData.nadName
×
801
        defaultNadName := cont.generateDefaultNadName(aaepName, epgDn)
×
802
        existingAnnotaions := existingNAD.ObjectMeta.Annotations
×
803
        if existingAnnotaions != nil {
×
804
                if existingNAD.ObjectMeta.Annotations["aci-sync-status"] == "out-of-sync" || existingNAD.ObjectMeta.Annotations["cno-name"] != customNadName {
×
805
                        return true
×
806
                }
×
807
        } else {
×
808
                // NAD exists, check if VLAN needs to be updated
×
809
                existingConfig := existingNAD.Spec.Config
×
810
                if existingConfig != "" {
×
811
                        var existingCNVConfig map[string]interface{}
×
812
                        if json.Unmarshal([]byte(existingConfig), &existingCNVConfig) == nil {
×
813
                                if existingVLAN, ok := existingCNVConfig["vlan"].(float64); ok {
×
814
                                        if int(existingVLAN) == vlanID {
×
815
                                                // VLAN hasn't changed, no update needed
×
816
                                                cont.log.Infof("NetworkAttachmentDefinition %s already exists with correct VLAN %d in namespace %s",
×
817
                                                        defaultNadName, vlanID, namespaceName)
×
818
                                                return false
×
819
                                        }
×
820
                                } else if vlanID == 0 {
×
821
                                        // Both existing and new have no VLAN, no update needed
×
822
                                        cont.log.Infof("NetworkAttachmentDefinition %s already exists with no VLAN in namespace %s", defaultNadName, namespaceName)
×
823
                                        return false
×
824
                                }
×
825
                        }
826
                }
827
        }
828

829
        return true
×
830
}
831

832
func (cont *AciController) createNetworkAttachmentDefinition(aaepName string, epgDn string, nadData *AaepEpgAttachData, createReason string) bool {
×
833
        bridge := cont.config.BridgeName
×
834
        if bridge == "" {
×
835
                cont.log.Errorf("Linux bridge name must be specified when creating NetworkAttachmentDefinitions")
×
836
                return false
×
837
        }
×
838

839
        vlanID := nadData.encapVlan
×
840
        namespaceName := nadData.namespaceName
×
841
        customNadName := nadData.nadName
×
842
        defaultNadName := cont.generateDefaultNadName(aaepName, epgDn)
×
843
        nadClient := cont.env.(*K8sEnvironment).nadClient
×
844
        mtu := 1500
×
845

×
846
        // Check if NAD already exists
×
847
        existingNAD, err := nadClient.K8sCniCncfIoV1().NetworkAttachmentDefinitions(namespaceName).Get(context.TODO(), defaultNadName, metav1.GetOptions{})
×
848
        nadExists := err == nil
×
849

×
850
        if nadExists && !cont.isNADUpdateRequired(aaepName, epgDn, nadData, existingNAD) {
×
851
                return true
×
852
        }
×
853

854
        cnvBridgeConfig := map[string]any{
×
855
                "cniVersion":                "0.3.1",
×
856
                "name":                      defaultNadName,
×
857
                "type":                      "bridge",
×
858
                "bridge":                    bridge,
×
859
                "mtu":                       mtu,
×
860
                "disableContainerInterface": true,
×
861
        }
×
862

×
863
        // Add optional parameters from controller config if they are set
×
864
        if cont.config.IsGateway != nil {
×
865
                cnvBridgeConfig["isGateway"] = *cont.config.IsGateway
×
866
        }
×
867
        if cont.config.IsDefaultGateway != nil {
×
868
                cnvBridgeConfig["isDefaultGateway"] = *cont.config.IsDefaultGateway
×
869
        }
×
870
        if cont.config.ForceAddress != nil {
×
871
                cnvBridgeConfig["forceAddress"] = *cont.config.ForceAddress
×
872
        }
×
873
        if cont.config.IpMasq != nil {
×
874
                cnvBridgeConfig["ipMasq"] = *cont.config.IpMasq
×
875
        }
×
876
        if cont.config.IpMasqBackend != "" {
×
877
                cnvBridgeConfig["ipMasqBackend"] = cont.config.IpMasqBackend
×
878
        }
×
879
        if cont.config.Mtu != nil {
×
880
                cnvBridgeConfig["mtu"] = *cont.config.Mtu
×
881
        }
×
882
        if cont.config.HairpinMode != nil {
×
883
                cnvBridgeConfig["hairpinMode"] = *cont.config.HairpinMode
×
884
        }
×
885
        if cont.config.PromiscMode != nil {
×
886
                cnvBridgeConfig["promiscMode"] = *cont.config.PromiscMode
×
887
        }
×
888
        if cont.config.Enabledad != nil {
×
889
                cnvBridgeConfig["enabledad"] = *cont.config.Enabledad
×
890
        }
×
891
        if cont.config.Macspoofchk != nil {
×
892
                cnvBridgeConfig["macspoofchk"] = *cont.config.Macspoofchk
×
893
        }
×
894
        if cont.config.DisableContainerInterface != nil {
×
895
                cnvBridgeConfig["disableContainerInterface"] = *cont.config.DisableContainerInterface
×
896
        }
×
897
        if cont.config.PortIsolation != nil {
×
898
                cnvBridgeConfig["portIsolation"] = *cont.config.PortIsolation
×
899
        }
×
900
        if len(cont.config.Ipam) > 0 {
×
901
                cnvBridgeConfig["ipam"] = cont.config.Ipam
×
902
        }
×
903
        if vlanID > 0 {
×
904
                cnvBridgeConfig["vlan"] = vlanID
×
905
        }
×
906

907
        configJSON, err := json.Marshal(cnvBridgeConfig)
×
908
        if err != nil {
×
909
                cont.log.Errorf("Failed to marshal CNV bridge config: %v", err)
×
910
                return false
×
911
        }
×
912

913
        nad := &nadapi.NetworkAttachmentDefinition{
×
914
                TypeMeta: metav1.TypeMeta{
×
915
                        APIVersion: "k8s.cni.cncf.io/v1",
×
916
                        Kind:       "NetworkAttachmentDefinition",
×
917
                },
×
918
                ObjectMeta: metav1.ObjectMeta{
×
919
                        Name:      defaultNadName,
×
920
                        Namespace: namespaceName,
×
921
                        Labels: map[string]string{
×
922
                                "managed-by": "cisco-network-operator",
×
923
                                "vlan":       strconv.Itoa(vlanID),
×
924
                        },
×
925
                        Annotations: map[string]string{
×
926
                                "managed-by":      "cisco-network-operator",
×
927
                                "vlan":            strconv.Itoa(vlanID),
×
928
                                "cno-name":        customNadName,
×
929
                                "aci-sync-status": "in-sync",
×
930
                                "aaep-name":       aaepName,
×
931
                                "epg-dn":          epgDn,
×
932
                        },
×
933
                },
×
934
                Spec: nadapi.NetworkAttachmentDefinitionSpec{
×
935
                        Config: string(configJSON),
×
936
                },
×
937
        }
×
938

×
939
        if nadExists {
×
940
                nad.ObjectMeta.ResourceVersion = existingNAD.ObjectMeta.ResourceVersion
×
941

×
942
                updatedNad, err := nadClient.K8sCniCncfIoV1().NetworkAttachmentDefinitions(namespaceName).Update(context.TODO(), nad, metav1.UpdateOptions{})
×
943

×
944
                if err != nil {
×
945
                        cont.log.Errorf("Failed to update NetworkAttachmentDefinition %s from namespace %s : %v", customNadName, namespaceName, err)
×
946
                        return false
×
947
                }
×
948

949
                cont.log.Debugf("Existing NAD Annotations: %v, %s", existingNAD.ObjectMeta.Annotations, createReason)
×
950
                if existingNAD.ObjectMeta.Annotations["aci-sync-status"] == "out-of-sync" {
×
951
                        cont.submitEvent(updatedNad, createReason, cont.getNADRevampMessage(createReason))
×
952
                }
×
953
                cont.log.Infof("Updated NetworkAttachmentDefinition %s from namespace %s", defaultNadName, namespaceName)
×
954
        } else {
×
955
                _, err = nadClient.K8sCniCncfIoV1().NetworkAttachmentDefinitions(namespaceName).Create(context.TODO(), nad, metav1.CreateOptions{})
×
956
                if err != nil {
×
957
                        cont.log.Errorf("Failed to create NetworkAttachmentDefinition %s in namespace %s : %v", customNadName, namespaceName, err)
×
958
                        return false
×
959
                }
×
960
                cont.log.Infof("Created NetworkAttachmentDefinition %s in namespace %s", defaultNadName, namespaceName)
×
961
        }
962

963
        return true
×
964
}
965

966
func (cont *AciController) deleteNetworkAttachmentDefinition(aaepName string, epgDn string, nadData *AaepEpgAttachData, deleteReason string) {
×
967
        namespaceName := nadData.namespaceName
×
968

×
969
        kubeClient := cont.env.(*K8sEnvironment).kubeClient
×
970
        _, err := kubeClient.CoreV1().Namespaces().Get(context.TODO(), namespaceName, metav1.GetOptions{})
×
971
        namespaceExists := err == nil
×
972
        if !namespaceExists {
×
973
                cont.log.Debugf("Defering NAD deletion for EPG %s: Namespace %s not exists", epgDn, namespaceName)
×
974
                return
×
975
        }
×
976

977
        nadName := cont.generateDefaultNadName(aaepName, epgDn)
×
978
        nadClient := cont.env.(*K8sEnvironment).nadClient
×
979

×
980
        nadDetails, err := nadClient.K8sCniCncfIoV1().NetworkAttachmentDefinitions(namespaceName).Get(context.TODO(), nadName, metav1.GetOptions{})
×
981
        nadExists := err == nil
×
982

×
983
        if nadExists {
×
984
                if !cont.isVmmLiteNAD(nadDetails) {
×
985
                        return
×
986
                }
×
987

988
                if cont.isNADinUse(namespaceName, nadName) {
×
989
                        nadDetails.ObjectMeta.Annotations["aci-sync-status"] = "out-of-sync"
×
990
                        _, err = nadClient.K8sCniCncfIoV1().NetworkAttachmentDefinitions(namespaceName).Update(context.TODO(), nadDetails, metav1.UpdateOptions{})
×
991
                        if err != nil {
×
992
                                cont.log.Errorf("Failed to add out-of-sync annotation to the NAD %s from namespace %s : %v", nadName, namespaceName, err)
×
993
                                return
×
994
                        }
×
995
                        cont.submitEvent(nadDetails, deleteReason, cont.getNADDeleteMessage(deleteReason))
×
996
                        cont.log.Infof("Added annotation out-of-sync for NAD %s from namespace %s", nadName, namespaceName)
×
997
                        return
×
998
                }
999

1000
                delete(nadDetails.ObjectMeta.Annotations, "managed-by")
×
1001
                _, err = nadClient.K8sCniCncfIoV1().NetworkAttachmentDefinitions(namespaceName).Update(context.TODO(), nadDetails, metav1.UpdateOptions{})
×
1002
                if err != nil {
×
1003
                        cont.log.Errorf("Failed to remove VMM lite annotation from NetworkAttachmentDefinition %s from namespace %s: %v", nadName, namespaceName, err)
×
1004
                        return
×
1005
                }
×
1006

1007
                nadClient.K8sCniCncfIoV1().NetworkAttachmentDefinitions(namespaceName).Delete(context.TODO(), nadName, metav1.DeleteOptions{})
×
1008
                cont.log.Infof("Deleted NAD %s from %s namespace", nadName, namespaceName)
×
1009
        } else {
×
1010
                cont.log.Debugf("NAD %s not there to delete in namespace %s", nadName, namespaceName)
×
1011
        }
×
1012
}
1013

1014
func (cont *AciController) removeManagedByAnnotationFromNAD(nadName, namespaceName string) bool {
×
1015
        nadClient := cont.env.(*K8sEnvironment).nadClient
×
1016
        // Check if NAD already exists
×
1017
        existingNAD, err := nadClient.K8sCniCncfIoV1().NetworkAttachmentDefinitions(namespaceName).Get(context.TODO(), nadName, metav1.GetOptions{})
×
1018
        nadExists := err == nil
×
1019

×
1020
        if nadExists {
×
1021
                if !cont.isVmmLiteNAD(existingNAD) {
×
1022
                        return true
×
1023
                }
×
1024

1025
                delete(existingNAD.ObjectMeta.Annotations, "managed-by")
×
1026
                _, err = nadClient.K8sCniCncfIoV1().NetworkAttachmentDefinitions(namespaceName).Update(context.TODO(), existingNAD, metav1.UpdateOptions{})
×
1027
                if err != nil {
×
1028
                        cont.log.Errorf("Failed to remove managed-by VMM lite annotation from NetworkAttachmentDefinition %s from namespace %s: %v", nadName, namespaceName, err)
×
1029
                        return false
×
1030
                }
×
1031

1032
                cont.log.Infof("Removed managed-by annotation from NAD %s in namespace %s", nadName, namespaceName)
×
1033
        } else {
×
1034
                cont.log.Debugf("NAD %s not there to remove managed-by annotation in namespace %s", nadName, namespaceName)
×
1035
        }
×
1036
        return true
×
1037
}
1038

1039
func (cont *AciController) getVlanId(encap string) int {
×
1040
        if after, ok := strings.CutPrefix(encap, "vlan-"); ok {
×
1041
                vlanStr := after
×
1042
                if vlanID, err := strconv.Atoi(vlanStr); err == nil && vlanID > 0 {
×
1043
                        return vlanID
×
1044
                }
×
1045
        } else if after, ok := strings.CutPrefix(encap, "vlan"); ok {
×
1046
                vlanStr := after
×
1047
                if vlanID, err := strconv.Atoi(vlanStr); err == nil && vlanID > 0 {
×
1048
                        return vlanID
×
1049
                }
×
1050
        }
1051

1052
        return 0
×
1053
}
1054

1055
func (cont *AciController) getAaepDiff(crAaeps []string) (addedAaeps, removedAaeps []string) {
×
1056
        crAaepMap := make(map[string]bool)
×
1057
        for _, crAaep := range crAaeps {
×
1058
                crAaepMap[crAaep] = true
×
1059
        }
×
1060

1061
        cont.indexMutex.Lock()
×
1062
        for _, crAaep := range crAaeps {
×
1063
                if _, ok := cont.sharedAaepMonitor[crAaep]; !ok {
×
1064
                        addedAaeps = append(addedAaeps, crAaep)
×
1065
                }
×
1066
        }
1067
        cont.indexMutex.Unlock()
×
1068

×
1069
        cont.indexMutex.Lock()
×
1070
        for cachedAaep := range cont.sharedAaepMonitor {
×
1071
                if !crAaepMap[cachedAaep] {
×
1072
                        removedAaeps = append(removedAaeps, cachedAaep)
×
1073
                }
×
1074
        }
1075
        cont.indexMutex.Unlock()
×
1076

×
1077
        return
×
1078
}
1079

1080
func (cont *AciController) getEncapFromAaepEpgAttachObj(aaepName, epgDn string) string {
×
1081
        uri := fmt.Sprintf("/api/node/mo/uni/infra/attentp-%s/gen-default/rsfuncToEpg-[%s].json?query-target=self", aaepName, epgDn)
×
1082
        resp, err := cont.apicConn.GetApicResponse(uri)
×
1083
        if err != nil {
×
1084
                cont.log.Errorf("Failed to get response from APIC: AAEP %s and EPG %s ERROR: %v", aaepName, epgDn, err)
×
1085
                return ""
×
1086
        }
×
1087

1088
        for _, obj := range resp.Imdata {
×
1089
                lresp, ok := obj["infraRsFuncToEpg"]
×
1090
                if !ok {
×
1091
                        cont.log.Errorf("InfraRsFuncToEpg object not found in response for %s", uri)
×
1092
                        break
×
1093
                }
1094
                if val, ok := lresp.Attributes["encap"]; ok {
×
1095
                        encap := val.(string)
×
1096
                        return encap
×
1097
                } else {
×
1098
                        cont.log.Errorf("Encap missing for infraRsFuncToEpg object for %s: %v", uri, err)
×
1099
                        break
×
1100
                }
1101
        }
1102

1103
        return ""
×
1104
}
1105

1106
func (cont *AciController) isVmmLiteNAD(nadDetails *nadapi.NetworkAttachmentDefinition) bool {
×
1107
        return nadDetails.ObjectMeta.Annotations["managed-by"] == "cisco-network-operator"
×
1108
}
×
1109

1110
func (cont *AciController) isNADinUse(namespaceName string, nadName string) bool {
×
1111
        kubeClient := cont.env.(*K8sEnvironment).kubeClient
×
1112
        pods, err := kubeClient.CoreV1().Pods(namespaceName).List(context.TODO(), metav1.ListOptions{})
×
1113
        if err == nil {
×
1114
                var networks []map[string]string
×
1115
                for _, pod := range pods.Items {
×
1116
                        networksAnn, ok := pod.Annotations["k8s.v1.cni.cncf.io/networks"]
×
1117
                        if ok && (networksAnn == nadName) {
×
1118
                                cont.log.Infof("NAD %s is still used by Pod %s/%s", nadName, namespaceName, pod.Name)
×
1119
                                return true
×
1120
                        }
×
1121
                        if err := json.Unmarshal([]byte(networksAnn), &networks); err != nil {
×
1122
                                cont.log.Errorf("Error while getting pod annotations: %v", err)
×
1123
                                return false
×
1124
                        }
×
1125
                        for _, network := range networks {
×
1126
                                if ok && (network["name"] == nadName) {
×
1127
                                        cont.log.Infof("NAD %s is still used by VM %s/%s", nadName, namespaceName, pod.Name)
×
1128
                                        return true
×
1129
                                }
×
1130
                        }
1131
                }
1132
        }
1133
        return false
×
1134
}
1135

1136
func (cont *AciController) getNADDeleteMessage(deleteReason string) string {
×
1137
        messagePrefix := "NAD is in use by pods: "
×
1138
        switch {
×
1139
        case deleteReason == "NamespaceAnnotationRemoved":
×
1140
                return messagePrefix + "Either EPG deleted or namespace name EPG annotaion removed"
×
1141
        case deleteReason == "AaepEpgDetached":
×
1142
                return messagePrefix + "EPG detached from AAEP"
×
1143
        case deleteReason == "CRDeleted":
×
1144
                return messagePrefix + "aaepmonitor CR deleted"
×
1145
        case deleteReason == "AaepRemovedFromCR":
×
1146
                return messagePrefix + "AAEP removed from aaepmonitor CR"
×
1147
        case deleteReason == "AaepEpgAttachedWithVlanUsedInCluster":
×
1148
                return messagePrefix + "EPG with AAEP has overlapping VLAN with existing cluster VLAN"
×
1149
        case deleteReason == "AaepEpgAttachedWithOverlappingVlan":
×
1150
                return messagePrefix + "EPG with AAEP has overlapping VLAN with another EPG"
×
1151
        case deleteReason == "NamespaceDeleted":
×
1152
                return messagePrefix + "Namespace deleted"
×
1153
        case deleteReason == "AaepEpgAttachedWithVlanInUse":
×
1154
                return messagePrefix + "EPG with AAEP has VLAN already used in cluster"
×
1155
        }
1156
        return messagePrefix + "One or many pods are using NAD"
×
1157
}
1158

1159
func (cont *AciController) getNADRevampMessage(createReason string) string {
×
1160
        messagePrefix := "NAD is in sync: "
×
1161
        switch {
×
1162
        case createReason == "NamespaceAnnotationAdded":
×
1163
                return messagePrefix + "Namespace name EPG annotaion added"
×
1164
        case createReason == "AaepEpgAttached":
×
1165
                return messagePrefix + "EPG attached with AAEP"
×
1166
        case createReason == "AaepAddedInCR":
×
1167
                return messagePrefix + "AAEP added back in aaepmonitor CR"
×
1168
        case createReason == "NamespaceCreated":
×
1169
                return messagePrefix + "Namespace created back"
×
1170
        case createReason == "NextEpgNadCreation":
×
1171
                return messagePrefix + "NAD creation for next EPG with same VLAN"
×
1172
        }
1173
        return messagePrefix + "NAD synced with ACI"
×
1174
}
1175

1176
func (cont *AciController) isEpgAttachedWithAaep(epgDn string) bool {
×
1177
        cont.indexMutex.Lock()
×
1178
        defer cont.indexMutex.Unlock()
×
1179
        for aaepName := range cont.sharedAaepMonitor {
×
1180
                encap := cont.getEncapFromAaepEpgAttachObj(aaepName, epgDn)
×
1181
                if encap != "" {
×
1182
                        return true
×
1183
                }
×
1184
        }
1185
        return false
×
1186
}
1187

1188
func (cont *AciController) checkDuplicateAaepEpgAttachRequest(aaepName, epgDn string, vlanID int) bool {
×
1189
        cont.indexMutex.Lock()
×
1190
        defer cont.indexMutex.Unlock()
×
1191
        aaepEpgAttachDataMap, exists := cont.sharedAaepMonitor[aaepName]
×
1192
        if !exists || aaepEpgAttachDataMap == nil {
×
1193
                return false
×
1194
        }
×
1195

1196
        aaepEpgAttachData, exists := aaepEpgAttachDataMap[epgDn]
×
1197
        if !exists || aaepEpgAttachData == nil {
×
1198
                return false
×
1199
        }
×
1200

1201
        if vlanID == aaepEpgAttachData.encapVlan {
×
1202
                return true
×
1203
        }
×
1204
        return false
×
1205
}
1206

1207
func (cont *AciController) checkIfEpgWithOverlappingVlan(aaepName string, vlanId int, epgDn string) bool {
×
1208
        cont.indexMutex.Lock()
×
1209
        defer cont.indexMutex.Unlock()
×
1210
        oldAaepEpgAttachData := cont.getAaepEpgAttachDataLocked(aaepName, epgDn)
×
1211

×
1212
        if oldAaepEpgAttachData != nil {
×
1213
                // If old data exists with NAD created and VLAN is same in old and new data, skip checking for overlapping VLAN for the same EPG
×
1214
                if oldAaepEpgAttachData.encapVlan == vlanId && oldAaepEpgAttachData.nadCreated {
×
1215
                        return false
×
1216
                }
×
1217
        }
1218
        aaepEpgAttachDataMap, exists := cont.sharedAaepMonitor[aaepName]
×
1219
        if !exists || aaepEpgAttachDataMap == nil {
×
1220
                return false
×
1221
        }
×
1222

1223
        for _, aaepEpgData := range aaepEpgAttachDataMap {
×
1224
                if vlanId == aaepEpgData.encapVlan {
×
1225
                        return true
×
1226
                }
×
1227
        }
1228
        return false
×
1229
}
1230

1231
func (cont *AciController) createNadForNextEpg(aaepName string, vlanId int) {
×
1232
        cont.indexMutex.Lock()
×
1233
        defer cont.indexMutex.Unlock()
×
1234

×
1235
        aaepEpgAttachDataMap := cont.sharedAaepMonitor[aaepName]
×
1236
        for epgDn, aaepEpgAttachData := range aaepEpgAttachDataMap {
×
1237
                if aaepEpgAttachData.encapVlan != vlanId || aaepEpgAttachData.nadCreated {
×
1238
                        continue
×
1239
                } else if !cont.namespaceChecks(aaepEpgAttachData.namespaceName, epgDn) {
×
1240
                        cont.log.Debugf("Skipping NAD creation for next EPG %s with AAEP %s and VLAN %d: Namespace checks failed", epgDn, aaepName, vlanId)
×
1241
                        continue
×
1242
                } else {
×
1243
                        cont.log.Infof("Creating NAD for next EPG %s with AAEP %s and VLAN %d, for which previously NAD not created due to overlapping Vlan",
×
1244
                                epgDn, aaepName, vlanId)
×
1245
                        needCacheChange := cont.createNetworkAttachmentDefinition(aaepName, epgDn, aaepEpgAttachData, "NextEpgNadCreation")
×
1246
                        if needCacheChange {
×
1247
                                aaepEpgAttachData.nadCreated = true
×
1248
                                if cont.sharedAaepMonitor[aaepName] == nil {
×
1249
                                        cont.sharedAaepMonitor[aaepName] = make(map[string]*AaepEpgAttachData)
×
1250
                                }
×
1251
                                cont.sharedAaepMonitor[aaepName][epgDn] = aaepEpgAttachData
×
1252
                        }
1253
                        cont.log.Infof("Created NAD for next EPG %s with AAEP %s and VLAN %d", epgDn, aaepName, vlanId)
×
1254
                        break
×
1255
                }
1256
        }
1257
}
1258

1259
func (cont *AciController) checkVlanUsedInCluster(vlanId int) bool {
×
1260
        if vlanId == cont.config.KubeapiVlan {
×
1261
                return true
×
1262
        }
×
1263
        return false
×
1264
}
1265

1266
func (cont *AciController) checkMonitorDataModified(oldData, newData *AaepEpgAttachData) bool {
×
1267
        if oldData == nil {
×
1268
                return true
×
1269
        }
×
1270
        if oldData.namespaceName != newData.namespaceName ||
×
1271
                oldData.nadName != newData.nadName ||
×
1272
                oldData.encapVlan != newData.encapVlan {
×
1273
                return true
×
1274
        }
×
1275
        return false
×
1276
}
1277

1278
func (cont *AciController) handleOldNadDeletion(aaepName, epgDn string,
1279
        oldAaepMonitorData *AaepEpgAttachData, deleteReason string) {
×
1280
        if oldAaepMonitorData == nil {
×
1281
                return
×
1282
        }
×
1283
        cont.indexMutex.Lock()
×
1284
        delete(cont.sharedAaepMonitor[aaepName], epgDn)
×
1285
        if oldAaepMonitorData.nadCreated {
×
1286
                cont.log.Debugf("Deleting NAD for EPG %s with AAEP %s due to %s", epgDn, aaepName, deleteReason)
×
1287
                cont.deleteNetworkAttachmentDefinition(aaepName, epgDn, oldAaepMonitorData, deleteReason)
×
1288
        }
×
1289
        cont.indexMutex.Unlock()
×
1290
        if oldAaepMonitorData.nadCreated {
×
1291
                cont.createNadForNextEpg(aaepName, oldAaepMonitorData.encapVlan)
×
1292
        }
×
1293
}
1294

1295
func (cont *AciController) handleNewNadCreation(aaepName, epgDn string,
NEW
1296
        newAaepMonitorData *AaepEpgAttachData, createReason string) string {
×
1297
        cont.indexMutex.Lock()
×
1298
        defer cont.indexMutex.Unlock()
×
NEW
1299
        // If there are multiple EPGs associated with same vlan for an aaep,
×
NEW
1300
        // then only one NAD will be there.
×
NEW
1301
        // After controller restart, if any other EPG notification comes before the already present NAD
×
NEW
1302
        // it will result in 2 NADs.
×
NEW
1303
        // Below check is to avoid the condition
×
NEW
1304
        NADAlreadyInCluster, oldEpgDn := cont.isNADWithSameVlanAlreadyPresent(aaepName, epgDn, newAaepMonitorData)
×
NEW
1305
        if NADAlreadyInCluster != nil {
×
NEW
1306
                cont.log.Errorf("Cannot create NAD for EPG %s as there is a NAD already present %v with EPG %s attached to AAEP %s", epgDn, NADAlreadyInCluster, oldEpgDn, aaepName)
×
NEW
1307
                if cont.sharedAaepMonitor[aaepName] == nil {
×
NEW
1308
                        cont.sharedAaepMonitor[aaepName] = make(map[string]*AaepEpgAttachData)
×
NEW
1309
                }
×
NEW
1310
                cont.sharedAaepMonitor[aaepName][oldEpgDn] = NADAlreadyInCluster
×
NEW
1311
                newAaepMonitorData.nadCreated = false
×
NEW
1312
                cont.sharedAaepMonitor[aaepName][epgDn] = newAaepMonitorData
×
NEW
1313
                return oldEpgDn
×
1314
        }
1315
        needCacheChange := cont.createNetworkAttachmentDefinition(aaepName, epgDn, newAaepMonitorData, createReason)
×
1316
        if needCacheChange {
×
1317
                if cont.sharedAaepMonitor[aaepName] == nil {
×
1318
                        cont.sharedAaepMonitor[aaepName] = make(map[string]*AaepEpgAttachData)
×
1319
                }
×
1320
                cont.sharedAaepMonitor[aaepName][epgDn] = newAaepMonitorData
×
1321
        }
NEW
1322
        return ""
×
1323
}
1324

1325
func (cont *AciController) checkAnnotationModified(oldAaepEpgAttachData, newAaepEpgAttachData *AaepEpgAttachData) bool {
×
1326
        if oldAaepEpgAttachData.nadName != newAaepEpgAttachData.nadName || oldAaepEpgAttachData.namespaceName != newAaepEpgAttachData.namespaceName {
×
1327
                return true
×
1328
        }
×
1329
        return false
×
1330
}
1331

NEW
1332
func (cont *AciController) isNADWithSameVlanAlreadyPresent(aaepName string, epgDn string, newAaepMonitorData *AaepEpgAttachData) (*AaepEpgAttachData, string) {
×
NEW
1333
        kubeClient := cont.env.(*K8sEnvironment).kubeClient
×
NEW
1334
        nadClient := cont.env.(*K8sEnvironment).nadClient
×
NEW
1335
        nsList, err := kubeClient.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{})
×
NEW
1336
        if err != nil {
×
NEW
1337
                cont.log.Errorf("Failed to list namespaces: %v", err)
×
NEW
1338
                return nil, ""
×
NEW
1339
        }
×
NEW
1340
        for _, ns := range nsList.Items {
×
NEW
1341
                nads, err := nadClient.K8sCniCncfIoV1().NetworkAttachmentDefinitions(ns.Name).List(context.TODO(), metav1.ListOptions{})
×
NEW
1342
                if err != nil {
×
NEW
1343
                        cont.log.Errorf("Failed to list NADs in namespace %s: %v", ns.Name, err)
×
NEW
1344
                        continue
×
1345
                }
1346

NEW
1347
                for _, nad := range nads.Items {
×
NEW
1348
                        annotations := nad.ObjectMeta.Annotations
×
NEW
1349
                        if annotations == nil {
×
NEW
1350
                                continue
×
1351
                        }
1352
                        // Check for required annotations
NEW
1353
                        if annotations["managed-by"] != "cisco-network-operator" ||
×
NEW
1354
                                annotations["aci-sync-status"] != "in-sync" ||
×
NEW
1355
                                annotations["aaep-name"] != aaepName {
×
NEW
1356
                                continue
×
1357
                        }
1358

NEW
1359
                        existingEpgDn := annotations["epg-dn"]
×
NEW
1360
                        // Skip if same EPG DN
×
NEW
1361
                        if existingEpgDn == epgDn {
×
NEW
1362
                                cont.log.Infof("Found existing NAD %s/%s for epg %s ", ns.Name, nad.Name, epgDn)
×
NEW
1363
                                return nil, ""
×
NEW
1364
                        }
×
1365
                        // Compare VLAN
NEW
1366
                        existingVlan, err := strconv.Atoi(annotations["vlan"])
×
NEW
1367
                        if err != nil {
×
NEW
1368
                                cont.log.Warnf("Invalid VLAN in NAD %s/%s: %v", ns.Name, nad.Name, err)
×
NEW
1369
                                continue
×
1370
                        }
1371

NEW
1372
                        if existingVlan == newAaepMonitorData.encapVlan {
×
NEW
1373
                                cont.log.Infof("Found existing NAD %s/%s with same VLAN %d for aaep %s", ns.Name, nad.Name, existingVlan, aaepName)
×
NEW
1374

×
NEW
1375
                                return &AaepEpgAttachData{
×
NEW
1376
                                        nadName:       annotations["cno-name"],
×
NEW
1377
                                        namespaceName: ns.Name,
×
NEW
1378
                                        encapVlan:     existingVlan,
×
NEW
1379
                                        nadCreated:    true,
×
NEW
1380
                                }, existingEpgDn
×
NEW
1381
                        }
×
1382
                }
1383
        }
1384

NEW
1385
        return nil, ""
×
1386
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc