Skip to content

Commit 9bb5853

Browse files
committed
[#1716] Filter by multiple zone types
1 parent 4c5a936 commit 9bb5853

File tree

7 files changed

+437
-69
lines changed

7 files changed

+437
-69
lines changed

api/swagger.in.yaml

+8-5
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ definitions:
116116
$ref: '#/definitions/VersionDetails'
117117
currentStable:
118118
type: array
119-
items:
119+
items:
120120
$ref: '#/definitions/VersionDetails'
121121
latestSecure:
122122
type: array
@@ -180,15 +180,14 @@ definitions:
180180
DNSZoneType:
181181
type: string
182182
enum: &DNSZONETYPE
183+
- builtin
183184
- delegation-only
184185
- forward
185186
- hint
186-
- master
187187
- mirror
188188
- primary
189189
- redirect
190190
- secondary
191-
- slave
192191
- static-stub
193192
- stub
194193

@@ -244,5 +243,9 @@ parameters:
244243
in: query
245244
description: >-
246245
Limit returned list of entities (e.g. zones) to the ones of a given type.
247-
type: string
248-
enum: *DNSZONETYPE
246+
Multiple zone types can be specified. If unspecified, all zone types are returned.
247+
type: array
248+
items:
249+
type: string
250+
enum: *DNSZONETYPE
251+
collectionFormat: multi
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package dbmigs
2+
3+
import "github.com/go-pg/migrations/v8"
4+
5+
func init() {
6+
migrations.MustRegisterTx(func(db migrations.DB) error {
7+
_, err := db.Exec(`
8+
-- Create an index on the type column of the local_zone table.
9+
CREATE INDEX local_zone_type_idx ON local_zone(type);
10+
`)
11+
return err
12+
}, func(db migrations.DB) error {
13+
_, err := db.Exec(`
14+
DROP INDEX IF EXISTS local_zone_type_idx;
15+
`)
16+
return err
17+
})
18+
}

backend/server/database/migrations_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import (
2121

2222
// Current schema version. This value must be bumped up every
2323
// time the schema is updated.
24-
const expectedSchemaVersion int64 = 62
24+
const expectedSchemaVersion int64 = 63
2525

2626
// Common function which tests a selected migration action.
2727
func testMigrateAction(t *testing.T, db *dbops.PgDB, expectedOldVersion, expectedNewVersion int64, action ...string) {

backend/server/database/model/zone.go

+124-52
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package dbmodel
22

33
import (
44
"context"
5+
"iter"
56
"slices"
67
"strings"
78
"time"
@@ -20,6 +21,59 @@ const (
2021
ZoneRelationLocalZonesApp = "LocalZones.Daemon.App"
2122
)
2223

24+
type ZoneType string
25+
26+
const (
27+
ZoneTypeBuiltin ZoneType = "builtin"
28+
ZoneTypeDelegationOnly ZoneType = "delegation-only"
29+
ZoneTypeForward ZoneType = "forward"
30+
ZoneTypeHint ZoneType = "hint"
31+
ZoneTypeMirror ZoneType = "mirror"
32+
ZoneTypePrimary ZoneType = "primary"
33+
ZoneTypeRedirect ZoneType = "redirect"
34+
ZoneTypeSecondary ZoneType = "secondary"
35+
ZoneTypeStaticStub ZoneType = "static-stub"
36+
ZoneTypeStub ZoneType = "stub"
37+
)
38+
39+
// Holds a set of zone types by which the zones should be filtered.
40+
// If there are no types specified, all zones are returned.
41+
// Otherwise, the zones matching the enabled filters are returned.
42+
type GetZonesFilterZoneTypes struct {
43+
types map[ZoneType]bool
44+
}
45+
46+
// Instantiates the zone types filter.
47+
func NewGetZonesFilterZoneTypes() *GetZonesFilterZoneTypes {
48+
return &GetZonesFilterZoneTypes{
49+
types: make(map[ZoneType]bool),
50+
}
51+
}
52+
53+
// Enables a filter for a specific zone type. The zones of the matching
54+
// type are returned.
55+
func (f *GetZonesFilterZoneTypes) Enable(zoneType ZoneType) {
56+
f.types[zoneType] = true
57+
}
58+
59+
// Returns true if any filter is specified (enabled or disabled).
60+
func (f *GetZonesFilterZoneTypes) IsAnySpecified() bool {
61+
return len(f.types) > 0
62+
}
63+
64+
// Returns an iterator over the enabled zone types.
65+
func (f *GetZonesFilterZoneTypes) GetEnabled() iter.Seq[ZoneType] {
66+
return func(yield func(ZoneType) bool) {
67+
for zoneType, enabled := range f.types {
68+
if enabled {
69+
if !yield(zoneType) {
70+
return
71+
}
72+
}
73+
}
74+
}
75+
}
76+
2377
// Filter used in the GetZones function for complex filtering of
2478
// the zones returned from the database.
2579
type GetZonesFilter struct {
@@ -38,11 +92,19 @@ type GetZonesFilter struct {
3892
// Filter by partial or exact zone serial.
3993
Serial *string
4094
// Filter by zone type (e.g., primary or secondary).
41-
Type *string
95+
Types *GetZonesFilterZoneTypes
4296
// Filter by partial zone name, app name or view.
4397
Text *string
4498
}
4599

100+
// Convenience function to enable a zone type filter.
101+
func (f *GetZonesFilter) EnableZoneType(zoneType ZoneType) {
102+
if f.Types == nil {
103+
f.Types = NewGetZonesFilterZoneTypes()
104+
}
105+
f.Types.Enable(zoneType)
106+
}
107+
46108
// Represents a zone in a database. The same zone can be shared between
47109
// many DNS servers. Associations with different servers is are created
48110
// by adding LocalZone instances to the zone.
@@ -123,7 +185,7 @@ func AddZones(dbi pg.DBI, zones ...*Zone) error {
123185
// would be negligible.
124186
func GetZones(db pg.DBI, filter *GetZonesFilter, relations ...ZoneRelation) ([]*Zone, int, error) {
125187
var zones []*Zone
126-
q := db.Model(&zones)
188+
q := db.Model(&zones).Distinct()
127189
// Add relations.
128190
for _, relation := range relations {
129191
q = q.Relation(string(relation))
@@ -132,62 +194,72 @@ func GetZones(db pg.DBI, filter *GetZonesFilter, relations ...ZoneRelation) ([]*
132194
q = q.OrderExpr("rname ASC")
133195

134196
// Filtering is optional.
135-
if filter != nil {
136-
// Limit the number of zones returned.
137-
if filter.Limit != nil {
138-
q = q.Limit(*filter.Limit)
197+
if filter == nil {
198+
count, err := q.SelectAndCount()
199+
if err != nil {
200+
return nil, count, errors.Wrapf(err, "failed to select unfiltered zones from the database")
139201
}
140-
// Paging from the last returned zone name.
141-
if filter.LowerBound != nil {
142-
labels := dns.SplitDomainName(*filter.LowerBound)
143-
slices.Reverse(labels)
144-
lowerBound := strings.Join(labels, ".")
145-
q = q.Where("rname > ?", lowerBound)
146-
}
147-
// Paging from offset.
148-
if filter.Offset != nil {
149-
q = q.Offset(*filter.Offset)
150-
}
151-
// Join relations required for filtering.
152-
if filter.Serial != nil || filter.Class != nil || filter.Type != nil || filter.AppID != nil || filter.AppType != nil || filter.Text != nil {
153-
q = q.Join("JOIN local_zone AS lz").JoinOn("lz.zone_id = zone.id")
154-
if filter.AppID != nil || filter.AppType != nil || filter.Text != nil {
155-
q = q.Join("JOIN daemon AS d").JoinOn("d.id = lz.daemon_id").
156-
Join("JOIN app AS a").JoinOn("a.id = d.app_id")
157-
}
158-
}
159-
// Filter by serial.
160-
if filter.Serial != nil {
161-
q = q.Where("lz.serial::text ILIKE ?", "%"+*filter.Serial+"%")
162-
}
163-
// Filter by class.
164-
if filter.Class != nil {
165-
q = q.Where("lz.class = ?", *filter.Class)
166-
}
167-
// Filter by type.
168-
if filter.Type != nil {
169-
q = q.Where("lz.type = ?", *filter.Type)
170-
}
171-
// Filter by app ID.
172-
if filter.AppID != nil {
173-
q = q.Where("a.id = ?", *filter.AppID)
174-
}
175-
// Filter by app type.
176-
if filter.AppType != nil {
177-
q = q.Where("a.type = ?", *filter.AppType)
202+
return zones, count, nil
203+
}
204+
205+
// Limit the number of zones returned.
206+
if filter.Limit != nil {
207+
q = q.Limit(*filter.Limit)
208+
}
209+
// Paging from the last returned zone name.
210+
if filter.LowerBound != nil {
211+
labels := dns.SplitDomainName(*filter.LowerBound)
212+
slices.Reverse(labels)
213+
lowerBound := strings.Join(labels, ".")
214+
q = q.Where("rname > ?", lowerBound)
215+
}
216+
// Paging from offset.
217+
if filter.Offset != nil {
218+
q = q.Offset(*filter.Offset)
219+
}
220+
// Join relations required for filtering.
221+
if filter.Serial != nil || filter.Class != nil || filter.Types != nil && filter.Types.IsAnySpecified() || filter.AppID != nil || filter.AppType != nil || filter.Text != nil {
222+
q = q.Join("JOIN local_zone AS lz").JoinOn("lz.zone_id = zone.id")
223+
if filter.AppID != nil || filter.AppType != nil || filter.Text != nil {
224+
q = q.Join("JOIN daemon AS d").JoinOn("d.id = lz.daemon_id").
225+
Join("JOIN app AS a").JoinOn("a.id = d.app_id")
178226
}
179-
// Filter by zone name, app name or local zone view using partial matching.
180-
if filter.Text != nil {
181-
q = q.WhereGroup(func(q *pg.Query) (*pg.Query, error) {
182-
return q.WhereOr("zone.name ILIKE ?", "%"+*filter.Text+"%").
183-
WhereOr("a.name ILIKE ?", "%"+*filter.Text+"%").
184-
WhereOr("lz.view ILIKE ?", "%"+*filter.Text+"%"), nil
185-
})
227+
}
228+
// Filter by serial.
229+
if filter.Serial != nil {
230+
q = q.Where("lz.serial::text ILIKE ?", "%"+*filter.Serial+"%")
231+
}
232+
// Filter by class.
233+
if filter.Class != nil {
234+
q = q.Where("lz.class = ?", *filter.Class)
235+
}
236+
// Filter by zone types.
237+
if filter.Types != nil {
238+
types := slices.Collect(filter.Types.GetEnabled())
239+
if len(types) > 0 {
240+
q = q.WhereIn("lz.type IN (?)", types)
186241
}
187242
}
243+
// Filter by app ID.
244+
if filter.AppID != nil {
245+
q = q.Where("a.id = ?", *filter.AppID)
246+
}
247+
// Filter by app type.
248+
if filter.AppType != nil {
249+
q = q.Where("a.type = ?", *filter.AppType)
250+
}
251+
// Filter by zone name, app name or local zone view using partial matching.
252+
if filter.Text != nil {
253+
q = q.WhereGroup(func(q *pg.Query) (*pg.Query, error) {
254+
return q.WhereOr("zone.name ILIKE ?", "%"+*filter.Text+"%").
255+
WhereOr("a.name ILIKE ?", "%"+*filter.Text+"%").
256+
WhereOr("lz.view ILIKE ?", "%"+*filter.Text+"%"), nil
257+
})
258+
}
259+
// Select and count the zones.
188260
count, err := q.SelectAndCount()
189261
if err != nil {
190-
return nil, count, errors.Wrapf(err, "failed to select zones from the database")
262+
return nil, count, errors.Wrapf(err, "failed to select filtered zones from the database")
191263
}
192264
return zones, count, nil
193265
}

0 commit comments

Comments
 (0)