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

noironetworks / aci-containers / 11294

06 Nov 2025 08:55AM UTC coverage: 64.429% (-0.2%) from 64.6%
11294

push

travis-pro

web-flow
Merge pull request #1620 from noironetworks/same-vlan-overlapping

Fix mutilpe same vlan NAD after controller restart:

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

4 existing lines in 1 file now uncovered.

13351 of 20722 relevant lines covered (64.43%)

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
        allNADs, err := cont.getAllNADs()
×
NEW
1334
        if err != nil {
×
NEW
1335
                cont.log.Errorf("Failed to get all NADs: %v", err)
×
NEW
1336
                return nil, ""
×
NEW
1337
        }
×
1338

NEW
1339
        for _, nad := range allNADs {
×
NEW
1340
                ns := nad.Namespace
×
NEW
1341
                annotations := nad.ObjectMeta.Annotations
×
NEW
1342
                if annotations == nil {
×
NEW
1343
                        continue
×
1344
                }
1345
                // Check for required annotations
NEW
1346
                if annotations["managed-by"] != "cisco-network-operator" ||
×
NEW
1347
                        annotations["aci-sync-status"] != "in-sync" ||
×
NEW
1348
                        annotations["aaep-name"] != aaepName {
×
NEW
1349
                        continue
×
1350
                }
1351

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

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

×
NEW
1368
                        return &AaepEpgAttachData{
×
NEW
1369
                                nadName:       annotations["cno-name"],
×
NEW
1370
                                namespaceName: ns,
×
NEW
1371
                                encapVlan:     existingVlan,
×
NEW
1372
                                nadCreated:    true,
×
NEW
1373
                        }, existingEpgDn
×
NEW
1374
                }
×
1375
        }
1376

NEW
1377
        return nil, ""
×
1378
}
1379

NEW
1380
func (cont *AciController) getAllNADs() ([]nadapi.NetworkAttachmentDefinition, error) {
×
NEW
1381
        nadClient := cont.env.(*K8sEnvironment).nadClient.K8sCniCncfIoV1().NetworkAttachmentDefinitions("")
×
NEW
1382

×
NEW
1383
        nadList, err := nadClient.List(context.TODO(), metav1.ListOptions{})
×
NEW
1384
        if err != nil {
×
NEW
1385
                return nil, fmt.Errorf("failed to list all NADs: %v", err)
×
NEW
1386
        }
×
1387

NEW
1388
        return nadList.Items, nil
×
1389
}
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