Skip to content

Commit fc40adf

Browse files
committed
[f5] fix entity deletion handling
1 parent d64ba62 commit fc40adf

File tree

2 files changed

+297
-30
lines changed

2 files changed

+297
-30
lines changed

internal/driver/f5/declaration.go

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import (
1818
"github.com/sapcc/andromeda/models"
1919
)
2020

21+
var errEntityPendingDeletion = errors.New("this entity has been marked as either PENDING_DELETE or DELETED and therefore must be excluded from the AS3 declaration")
22+
2123
type as3CommonTenantBuilderFunc func(s AndromedaF5Store, datacenters []*rpcmodels.Datacenter, domains []*rpcmodels.Domain) (
2224
as3.Tenant, []*server.ProvisioningStatusRequest_ProvisioningStatus, error)
2325

@@ -51,22 +53,50 @@ func buildAS3Declaration(f5Config config.F5Config, s AndromedaF5Store, ctbFunc a
5153
// build all /domain_{domainID} keys
5254
for _, domain := range domains {
5355
domainTenant, domainTenantRPCUpdates, err := dtbFunc(f5Config, datacentersByID, domain)
54-
if err != nil {
56+
// not a soft error: the declaration cannot be built
57+
if err != nil && err != errEntityPendingDeletion {
5558
return adc, rpcRequest, err
5659
}
60+
// only the active domains may be included in the declaration.
61+
// domains pending deletion are excluded (and thus deleted by AS3).
62+
if err != errEntityPendingDeletion {
63+
adc.AddTenant(as3DeclarationGSLBDomainTenantKey(domain.Id), domainTenant)
64+
}
5765
rpcUpdates = append(rpcUpdates, domainTenantRPCUpdates...)
58-
adc.AddTenant(as3DeclarationGSLBDomainTenantKey(domain.Id), domainTenant)
5966
}
6067
rpcRequest.ProvisioningStatus = rpcUpdates
6168
return adc, rpcRequest, nil
6269
}
6370

71+
// buildAS3DomainTenant builds one domain tenant ("domain_{domainID}" keys of AS3 declaration).
72+
//
73+
// If the given domain has been either scheduled for deletion or marked as
74+
// deleted, the returned tenant (first return value) should be considered a
75+
// zero value to be discarded. This condition can be checked for by comparing
76+
// the return error (third return value) to errEntityPendingDeletion.
77+
//
78+
// In case the domain provisioning status is set as PENDING_DELETE, the
79+
// returned slice of provisioning status requests will include an entry for
80+
// this domain, which shall be sent over RPC to the Andromeda Server
81+
// immediately after the next successful posting of the AS3 declaration.
6482
func buildAS3DomainTenant(
6583
f5Config config.F5Config,
6684
datacentersByID map[string]*rpcmodels.Datacenter,
6785
domain *rpcmodels.Domain) (as3.Tenant, []*server.ProvisioningStatusRequest_ProvisioningStatus, error) {
6886
tenant := as3.Tenant{}
6987
rpcUpdates := []*server.ProvisioningStatusRequest_ProvisioningStatus{}
88+
switch domain.ProvisioningStatus {
89+
case server.ProvisioningStatusRequest_ProvisioningStatus_DELETED.String(),
90+
server.ProvisioningStatusRequest_ProvisioningStatus_PENDING_DELETE.String():
91+
if domain.ProvisioningStatus == server.ProvisioningStatusRequest_ProvisioningStatus_PENDING_DELETE.String() {
92+
rpcUpdates = append(rpcUpdates, &server.ProvisioningStatusRequest_ProvisioningStatus{
93+
Id: domain.Id,
94+
Model: server.ProvisioningStatusRequest_ProvisioningStatus_DOMAIN,
95+
Status: server.ProvisioningStatusRequest_ProvisioningStatus_DELETED,
96+
})
97+
}
98+
return tenant, rpcUpdates, errEntityPendingDeletion
99+
}
70100
application := as3.Application{}
71101
as3PoolReferences := []as3.PointerGSLBPool{}
72102
for _, p := range domain.Pools {
@@ -132,6 +162,19 @@ func buildAS3CommonTenant(
132162
for _, pool := range domain.Pools {
133163
monitorsByPoolID[pool.Id] = pool.Monitors
134164
for _, monitor := range pool.Monitors {
165+
switch monitor.ProvisioningStatus {
166+
case server.ProvisioningStatusRequest_ProvisioningStatus_PENDING_DELETE.String(),
167+
server.ProvisioningStatusRequest_ProvisioningStatus_DELETED.String():
168+
if monitor.ProvisioningStatus == server.ProvisioningStatusRequest_ProvisioningStatus_PENDING_DELETE.String() {
169+
rpcUpdates = append(rpcUpdates, &server.ProvisioningStatusRequest_ProvisioningStatus{
170+
Id: monitor.Id,
171+
Model: server.ProvisioningStatusRequest_ProvisioningStatus_MONITOR,
172+
Status: server.ProvisioningStatusRequest_ProvisioningStatus_DELETED,
173+
})
174+
}
175+
// by excluding the entity from the AS3 declaration the API will delete it from the F5 device
176+
continue
177+
}
135178
monitorKey := as3DeclarationGSLBMonitorKey(monitor.Id)
136179
application.SetEntity(monitorKey, as3.GSLBMonitor{
137180
Class: "GSLB_Monitor",
@@ -156,9 +199,28 @@ func buildAS3CommonTenant(
156199
return tenant, rpcUpdates, err
157200
}
158201
for _, member := range members {
202+
switch member.ProvisioningStatus {
203+
case server.ProvisioningStatusRequest_ProvisioningStatus_PENDING_DELETE.String(),
204+
server.ProvisioningStatusRequest_ProvisioningStatus_DELETED.String():
205+
if member.ProvisioningStatus == server.ProvisioningStatusRequest_ProvisioningStatus_PENDING_DELETE.String() {
206+
rpcUpdates = append(rpcUpdates, &server.ProvisioningStatusRequest_ProvisioningStatus{
207+
Id: member.Id,
208+
Model: server.ProvisioningStatusRequest_ProvisioningStatus_MEMBER,
209+
Status: server.ProvisioningStatusRequest_ProvisioningStatus_DELETED,
210+
})
211+
}
212+
// by excluding the entity from the AS3 declaration the API will delete it from the F5 device
213+
continue
214+
}
159215
monitorPointers := []as3.PointerGSLBMonitor{}
160216
if monitors, ok := monitorsByPoolID[member.PoolId]; ok {
161217
for _, monitor := range monitors {
218+
switch monitor.ProvisioningStatus {
219+
case server.ProvisioningStatusRequest_ProvisioningStatus_PENDING_DELETE.String(),
220+
server.ProvisioningStatusRequest_ProvisioningStatus_DELETED.String():
221+
// these monitors are not part of the declaration (see GSLB_Monitor setup above)
222+
continue
223+
}
162224
monitorPointers = append(monitorPointers, as3.PointerGSLBMonitor{
163225
Use: as3DeclarationGSLBMonitorKey(monitor.Id),
164226
})

internal/driver/f5/declaration_test.go

Lines changed: 233 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -64,48 +64,230 @@ func TestBuildAS3Declaration(t *testing.T) {
6464

6565
t.Run("Succeeds by creating the full AS3 declaration", func(t *testing.T) {
6666
store := new(mockedStore)
67-
store.On("GetDatacenters").Return([]*rpcmodels.Datacenter{{Id: "dc1-uuid", Name: "dc1"}}, nil)
68-
store.On("GetDomains").Return([]*rpcmodels.Domain{{Id: "dom1-uuid"}}, nil)
69-
expectedCommonTenant := as3.Tenant{}
70-
expectedDomainTenant := as3.Tenant{}
71-
as3CommonTenantBuilder := func(s AndromedaF5Store, datacenters []*rpcmodels.Datacenter, domains []*rpcmodels.Domain) (as3.Tenant, []*server.ProvisioningStatusRequest_ProvisioningStatus, error) {
72-
return expectedCommonTenant, []*server.ProvisioningStatusRequest_ProvisioningStatus{
73-
{Id: "member1", Model: server.ProvisioningStatusRequest_ProvisioningStatus_MEMBER, Status: server.ProvisioningStatusRequest_ProvisioningStatus_ACTIVE},
74-
}, nil
75-
}
76-
as3DomainTenantBuilder := func(f5Config config.F5Config, datacentersByID map[string]*rpcmodels.Datacenter, domain *rpcmodels.Domain) (as3.Tenant, []*server.ProvisioningStatusRequest_ProvisioningStatus, error) {
77-
return expectedDomainTenant, []*server.ProvisioningStatusRequest_ProvisioningStatus{
78-
{Id: "pool1-uuid", Model: server.ProvisioningStatusRequest_ProvisioningStatus_POOL, Status: server.ProvisioningStatusRequest_ProvisioningStatus_ACTIVE},
79-
{Id: "pool2-uuid", Model: server.ProvisioningStatusRequest_ProvisioningStatus_POOL, Status: server.ProvisioningStatusRequest_ProvisioningStatus_ACTIVE},
80-
{Id: "dom1-uuid", Model: server.ProvisioningStatusRequest_ProvisioningStatus_DOMAIN, Status: server.ProvisioningStatusRequest_ProvisioningStatus_ACTIVE},
81-
}, nil
82-
}
8367

84-
declaration, req, err := buildAS3Declaration(config.F5Config{}, store, as3CommonTenantBuilder, as3DomainTenantBuilder)
68+
store.On("GetDatacenters").Return(
69+
[]*rpcmodels.Datacenter{
70+
{Id: "dc1-uuid", Name: "dc1"},
71+
},
72+
nil)
73+
74+
store.On("GetDomains").Return(
75+
[]*rpcmodels.Domain{
76+
{
77+
Id: "dom1-uuid",
78+
ProvisioningStatus: server.ProvisioningStatusRequest_ProvisioningStatus_ACTIVE.String(),
79+
Pools: []*rpcmodels.Pool{
80+
{
81+
Id: "pool1-uuid",
82+
Members: []*rpcmodels.Member{
83+
{
84+
Id: "member1-uuid",
85+
DatacenterId: "dc1-uuid",
86+
Address: "200.100.50.1",
87+
Port: 81,
88+
},
89+
{
90+
Id: "member2-uuid",
91+
DatacenterId: "dc1-uuid",
92+
Address: "200.100.50.2",
93+
Port: 82,
94+
},
95+
},
96+
Monitors: []*rpcmodels.Monitor{
97+
{
98+
Id: "mon1-uuid",
99+
Type: rpcmodels.Monitor_HTTP,
100+
Interval: 60,
101+
Timeout: 10,
102+
ProvisioningStatus: server.ProvisioningStatusRequest_ProvisioningStatus_ACTIVE.String(),
103+
},
104+
{
105+
Id: "mon2-uuid",
106+
Type: rpcmodels.Monitor_HTTP,
107+
Interval: 60,
108+
Timeout: 10,
109+
ProvisioningStatus: server.ProvisioningStatusRequest_ProvisioningStatus_PENDING_DELETE.String(),
110+
},
111+
},
112+
},
113+
},
114+
},
115+
{
116+
Id: "dom2-uuid",
117+
ProvisioningStatus: server.ProvisioningStatusRequest_ProvisioningStatus_PENDING_DELETE.String(),
118+
},
119+
{
120+
Id: "dom3-uuid",
121+
ProvisioningStatus: server.ProvisioningStatusRequest_ProvisioningStatus_DELETED.String(),
122+
},
123+
},
124+
nil)
125+
126+
store.On("GetMembers", "dc1-uuid").Return(
127+
[]*rpcmodels.Member{
128+
{
129+
Id: "member1-uuid",
130+
Address: "200.100.50.1",
131+
Port: 81,
132+
PoolId: "pool1-uuid",
133+
ProvisioningStatus: server.ProvisioningStatusRequest_ProvisioningStatus_ACTIVE.String(),
134+
},
135+
{
136+
Id: "member2-uuid",
137+
Address: "200.100.50.2",
138+
Port: 82,
139+
PoolId: "pool1-uuid",
140+
ProvisioningStatus: server.ProvisioningStatusRequest_ProvisioningStatus_ACTIVE.String(),
141+
},
142+
{
143+
Id: "member3-uuid",
144+
Address: "200.100.50.3",
145+
Port: 83,
146+
PoolId: "pool1-uuid",
147+
ProvisioningStatus: server.ProvisioningStatusRequest_ProvisioningStatus_PENDING_DELETE.String(),
148+
},
149+
{
150+
Id: "member4-uuid",
151+
Address: "200.100.50.4",
152+
Port: 84,
153+
PoolId: "pool1-uuid",
154+
ProvisioningStatus: server.ProvisioningStatusRequest_ProvisioningStatus_DELETED.String(),
155+
},
156+
},
157+
nil)
158+
159+
declaration, req, err := buildAS3Declaration(config.F5Config{}, store, buildAS3CommonTenant, buildAS3DomainTenant)
85160
assert.Nil(err)
86161

87-
t.Run("Adds the common tenant to the AS3 declaration", func(t *testing.T) {
162+
t.Run("Builds the Common tenant correctly", func(t *testing.T) {
163+
expectedCommonTenant := as3.Tenant{}
164+
commonApp := as3.Application{Template: "shared"}
165+
commonApp.SetEntity("cc_andromeda_srv_200.100.50.1_dc1", as3.GSLBServer{
166+
Class: "GSLB_Server",
167+
ServerType: "generic-host",
168+
DataCenter: as3.PointerGSLBDataCenter{BigIP: "/Common/dc1"},
169+
Devices: []as3.GSLBServerDevice{{Address: "200.100.50.1"}},
170+
Monitors: []as3.PointerGSLBMonitor{
171+
{Use: "cc_andromeda_monitor_mon1-uuid"},
172+
},
173+
VirtualServers: []as3.GSLBVirtualServer{
174+
{Address: "200.100.50.1", Port: 81, Name: "200.100.50.1:81"},
175+
},
176+
})
177+
commonApp.SetEntity("cc_andromeda_srv_200.100.50.2_dc1", as3.GSLBServer{
178+
Class: "GSLB_Server",
179+
ServerType: "generic-host",
180+
DataCenter: as3.PointerGSLBDataCenter{BigIP: "/Common/dc1"},
181+
Devices: []as3.GSLBServerDevice{{Address: "200.100.50.2"}},
182+
Monitors: []as3.PointerGSLBMonitor{
183+
{Use: "cc_andromeda_monitor_mon1-uuid"},
184+
},
185+
VirtualServers: []as3.GSLBVirtualServer{
186+
{Address: "200.100.50.2", Port: 82, Name: "200.100.50.2:82"},
187+
},
188+
})
189+
commonApp.SetEntity("cc_andromeda_monitor_mon1-uuid", as3.GSLBMonitor{
190+
Class: "GSLB_Monitor",
191+
MonitorType: "http",
192+
Interval: 60,
193+
ProbeTimeout: 10,
194+
})
195+
expectedCommonTenant.AddApplication("Shared", commonApp)
88196
tenant, err := declaration.GetTenant("Common")
89197
assert.Nil(err)
90198
assert.Equal(expectedCommonTenant, tenant)
91199
})
92200

93-
t.Run("Adds the domain tenant to the AS3 declaration", func(t *testing.T) {
94-
tenant, err := declaration.GetTenant("domain_dom1-uuid")
201+
t.Run("Builds the active domain tenants correctly", func(t *testing.T) {
202+
domainApp := as3.Application{}
203+
domainApp.SetEntity("wideip", as3.GSLBDomain{
204+
Class: "GSLB_Domain",
205+
Pools: []as3.PointerGSLBPool{
206+
{Use: "pool_pool1-uuid"},
207+
},
208+
PoolLbMode: "global-availability",
209+
})
210+
domainApp.SetEntity("pool_pool1-uuid", as3.GSLBPool{
211+
Class: "GSLB_Pool",
212+
LBModePreferred: "round-robin",
213+
LBModeAlternate: "none",
214+
LBModeFallback: "none",
215+
Members: []as3.GSLBPoolMember{
216+
{
217+
Server: as3.PointerGSLBServer{Use: "/Common/Shared/cc_andromeda_srv_200.100.50.1_dc1"},
218+
VirtualServer: "200.100.50.1:81",
219+
},
220+
{
221+
Server: as3.PointerGSLBServer{Use: "/Common/Shared/cc_andromeda_srv_200.100.50.2_dc1"},
222+
VirtualServer: "200.100.50.2:82",
223+
},
224+
},
225+
ResourceRecordType: "A",
226+
})
227+
expectedDomainTenant := as3.Tenant{}
228+
expectedDomainTenant.AddApplication("application", domainApp)
229+
domainTenant, err := declaration.GetTenant(as3DeclarationGSLBDomainTenantKey("dom1-uuid"))
95230
assert.Nil(err)
96-
assert.Equal(expectedDomainTenant, tenant)
231+
assert.Equal(expectedDomainTenant, domainTenant)
232+
})
233+
234+
t.Run("Excludes domains marked PENDING_DELETE", func(t *testing.T) {
235+
_, err := declaration.GetTenant(as3DeclarationGSLBDomainTenantKey("dom2-uuid"))
236+
assert.Error(err)
237+
})
238+
239+
t.Run("Excludes domains marked DELETED", func(t *testing.T) {
240+
_, err := declaration.GetTenant(as3DeclarationGSLBDomainTenantKey("dom3-uuid"))
241+
assert.Error(err)
97242
})
98243

99-
t.Run("Combines the RPC update requests returned by both the common and domain tenant builders", func(t *testing.T) {
100-
expectedReq := &server.ProvisioningStatusRequest{
244+
t.Run("Creates the expected provisioning status updates", func(t *testing.T) {
245+
// TODO: test pool deletion
246+
expectedUpdates := &server.ProvisioningStatusRequest{
101247
ProvisioningStatus: []*server.ProvisioningStatusRequest_ProvisioningStatus{
102-
{Id: "member1", Model: server.ProvisioningStatusRequest_ProvisioningStatus_MEMBER, Status: server.ProvisioningStatusRequest_ProvisioningStatus_ACTIVE},
103-
{Id: "pool1-uuid", Model: server.ProvisioningStatusRequest_ProvisioningStatus_POOL, Status: server.ProvisioningStatusRequest_ProvisioningStatus_ACTIVE},
104-
{Id: "pool2-uuid", Model: server.ProvisioningStatusRequest_ProvisioningStatus_POOL, Status: server.ProvisioningStatusRequest_ProvisioningStatus_ACTIVE},
105-
{Id: "dom1-uuid", Model: server.ProvisioningStatusRequest_ProvisioningStatus_DOMAIN, Status: server.ProvisioningStatusRequest_ProvisioningStatus_ACTIVE},
248+
{
249+
Id: "mon1-uuid",
250+
Model: server.ProvisioningStatusRequest_ProvisioningStatus_MONITOR,
251+
Status: server.ProvisioningStatusRequest_ProvisioningStatus_ACTIVE,
252+
},
253+
{
254+
Id: "mon2-uuid",
255+
Model: server.ProvisioningStatusRequest_ProvisioningStatus_MONITOR,
256+
Status: server.ProvisioningStatusRequest_ProvisioningStatus_DELETED, // transitioned from PENDING_DELETE
257+
},
258+
{
259+
Id: "member1-uuid",
260+
Model: server.ProvisioningStatusRequest_ProvisioningStatus_MEMBER,
261+
Status: server.ProvisioningStatusRequest_ProvisioningStatus_ACTIVE,
262+
},
263+
{
264+
Id: "member2-uuid",
265+
Model: server.ProvisioningStatusRequest_ProvisioningStatus_MEMBER,
266+
Status: server.ProvisioningStatusRequest_ProvisioningStatus_ACTIVE,
267+
},
268+
{
269+
Id: "member3-uuid",
270+
Model: server.ProvisioningStatusRequest_ProvisioningStatus_MEMBER,
271+
Status: server.ProvisioningStatusRequest_ProvisioningStatus_DELETED, // transitioned from PENDING_DELETE
272+
},
273+
{
274+
Id: "pool1-uuid",
275+
Model: server.ProvisioningStatusRequest_ProvisioningStatus_POOL,
276+
Status: server.ProvisioningStatusRequest_ProvisioningStatus_ACTIVE,
277+
},
278+
{
279+
Id: "dom1-uuid",
280+
Model: server.ProvisioningStatusRequest_ProvisioningStatus_DOMAIN,
281+
Status: server.ProvisioningStatusRequest_ProvisioningStatus_ACTIVE,
282+
},
283+
{
284+
Id: "dom2-uuid",
285+
Model: server.ProvisioningStatusRequest_ProvisioningStatus_DOMAIN,
286+
Status: server.ProvisioningStatusRequest_ProvisioningStatus_DELETED, // transitioned from PENDING_DELETE
287+
},
106288
},
107289
}
108-
assert.Equal(expectedReq, req)
290+
assert.Equal(expectedUpdates, req)
109291
})
110292
})
111293
}
@@ -143,6 +325,29 @@ func TestBuildAS3DomainTenant(t *testing.T) {
143325
assert.ErrorContains(err, "nil datacenter for member")
144326
})
145327

328+
t.Run("Soft-fails if domain has been marked PENDING_DELETE", func(t *testing.T) {
329+
domain := &rpcmodels.Domain{
330+
Id: "dom1-uuid",
331+
ProvisioningStatus: server.ProvisioningStatusRequest_ProvisioningStatus_PENDING_DELETE.String(),
332+
}
333+
_, updates, err := buildAS3DomainTenant(config.F5Config{}, map[string]*rpcmodels.Datacenter{}, domain)
334+
assert.Equal(err, errEntityPendingDeletion)
335+
assert.Equal(updates, []*server.ProvisioningStatusRequest_ProvisioningStatus{
336+
{
337+
Id: domain.Id,
338+
Model: server.ProvisioningStatusRequest_ProvisioningStatus_DOMAIN,
339+
Status: server.ProvisioningStatusRequest_ProvisioningStatus_DELETED,
340+
},
341+
})
342+
})
343+
344+
t.Run("Soft-fails if domain has been marked DELETED", func(t *testing.T) {
345+
domain := &rpcmodels.Domain{ProvisioningStatus: server.ProvisioningStatusRequest_ProvisioningStatus_DELETED.String()}
346+
_, updates, err := buildAS3DomainTenant(config.F5Config{}, map[string]*rpcmodels.Datacenter{}, domain)
347+
assert.Equal(err, errEntityPendingDeletion)
348+
assert.Empty(updates)
349+
})
350+
146351
t.Run("Succeeds by creating a direct mapping between each Andromeda domain's pools and members and their F5 counterpart entity", func(t *testing.T) {
147352
datacentersByID := map[string]*rpcmodels.Datacenter{
148353
"dc1-uuid": {Id: "dc1-uuid", Name: "dc1"},

0 commit comments

Comments
 (0)