Skip to content

Commit ebd379a

Browse files
authored
Add size reservation filters. (#535)
1 parent cead5f8 commit ebd379a

File tree

7 files changed

+533
-216
lines changed

7 files changed

+533
-216
lines changed

cmd/metal-api/internal/datastore/size.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,65 @@ import (
44
"errors"
55

66
"github.com/metal-stack/metal-api/cmd/metal-api/internal/metal"
7+
r "gopkg.in/rethinkdb/rethinkdb-go.v6"
78
)
89

10+
// SizeSearchQuery can be used to search sizes.
11+
type SizeSearchQuery struct {
12+
ID *string `json:"id" optional:"true"`
13+
Name *string `json:"name" optional:"true"`
14+
Labels map[string]string `json:"labels" optional:"true"`
15+
Reservation Reservation `json:"reservation" optional:"true"`
16+
}
17+
18+
type Reservation struct {
19+
Partition *string `json:"partition" optional:"true"`
20+
Project *string `json:"project" optional:"true"`
21+
}
22+
23+
// GenerateTerm generates the project search query term.
24+
func (s *SizeSearchQuery) generateTerm(rs *RethinkStore) *r.Term {
25+
q := *rs.sizeTable()
26+
27+
if s.ID != nil {
28+
q = q.Filter(func(row r.Term) r.Term {
29+
return row.Field("id").Eq(*s.ID)
30+
})
31+
}
32+
33+
if s.Name != nil {
34+
q = q.Filter(func(row r.Term) r.Term {
35+
return row.Field("name").Eq(*s.Name)
36+
})
37+
}
38+
39+
for k, v := range s.Labels {
40+
k := k
41+
v := v
42+
q = q.Filter(func(row r.Term) r.Term {
43+
return row.Field("labels").Field(k).Eq(v)
44+
})
45+
}
46+
47+
if s.Reservation.Project != nil {
48+
q = q.Filter(func(row r.Term) r.Term {
49+
return row.Field("reservations").Contains(func(p r.Term) r.Term {
50+
return p.Field("projectid").Eq(r.Expr(*s.Reservation.Project))
51+
})
52+
})
53+
}
54+
55+
if s.Reservation.Partition != nil {
56+
q = q.Filter(func(row r.Term) r.Term {
57+
return row.Field("reservations").Contains(func(p r.Term) r.Term {
58+
return p.Field("partitionids").Contains(r.Expr(*s.Reservation.Partition))
59+
})
60+
})
61+
}
62+
63+
return &q
64+
}
65+
966
// FindSize return a size for a given id.
1067
func (rs *RethinkStore) FindSize(id string) (*metal.Size, error) {
1168
var s metal.Size
@@ -16,6 +73,11 @@ func (rs *RethinkStore) FindSize(id string) (*metal.Size, error) {
1673
return &s, nil
1774
}
1875

76+
// SearchSizes returns the result of the sizes search request query.
77+
func (rs *RethinkStore) SearchSizes(q *SizeSearchQuery, sizes *metal.Sizes) error {
78+
return rs.searchEntities(q.generateTerm(rs), sizes)
79+
}
80+
1981
// ListSizes returns all sizes.
2082
func (rs *RethinkStore) ListSizes() (metal.Sizes, error) {
2183
szs := make(metal.Sizes, 0)
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
//go:build integration
2+
// +build integration
3+
4+
package datastore
5+
6+
import (
7+
"testing"
8+
9+
"github.com/metal-stack/metal-api/cmd/metal-api/internal/metal"
10+
"github.com/metal-stack/metal-lib/pkg/pointer"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
type sizeTestable struct{}
15+
16+
func (_ *sizeTestable) wipe() error {
17+
_, err := sharedDS.sizeTable().Delete().RunWrite(sharedDS.session)
18+
return err
19+
}
20+
21+
func (_ *sizeTestable) create(s *metal.Size) error { // nolint:unused
22+
return sharedDS.CreateSize(s)
23+
}
24+
25+
func (_ *sizeTestable) delete(id string) error { // nolint:unused
26+
return sharedDS.DeleteSize(&metal.Size{Base: metal.Base{ID: id}})
27+
}
28+
29+
func (_ *sizeTestable) update(old *metal.Size, mutateFn func(s *metal.Size)) error { // nolint:unused
30+
mod := *old
31+
if mutateFn != nil {
32+
mutateFn(&mod)
33+
}
34+
35+
return sharedDS.UpdateSize(old, &mod)
36+
}
37+
38+
func (_ *sizeTestable) find(id string) (*metal.Size, error) { // nolint:unused
39+
return sharedDS.FindSize(id)
40+
}
41+
42+
func (_ *sizeTestable) list() ([]*metal.Size, error) { // nolint:unused
43+
res, err := sharedDS.ListSizes()
44+
if err != nil {
45+
return nil, err
46+
}
47+
48+
return derefSlice(res), nil
49+
}
50+
51+
func (_ *sizeTestable) search(q *SizeSearchQuery) ([]*metal.Size, error) { // nolint:unused
52+
var res metal.Sizes
53+
err := sharedDS.SearchSizes(q, &res)
54+
if err != nil {
55+
return nil, err
56+
}
57+
58+
return derefSlice(res), nil
59+
}
60+
61+
func (_ *sizeTestable) defaultBody(s *metal.Size) *metal.Size {
62+
if s.Constraints == nil {
63+
s.Constraints = []metal.Constraint{}
64+
}
65+
if s.Reservations == nil {
66+
s.Reservations = metal.Reservations{}
67+
}
68+
for i := range s.Reservations {
69+
if s.Reservations[i].PartitionIDs == nil {
70+
s.Reservations[i].PartitionIDs = []string{}
71+
}
72+
}
73+
return s
74+
}
75+
76+
func TestRethinkStore_FindSize(t *testing.T) {
77+
tt := &sizeTestable{}
78+
defer func() {
79+
require.NoError(t, tt.wipe())
80+
}()
81+
82+
tests := []findTest[*metal.Size, *SizeSearchQuery]{
83+
{
84+
name: "find",
85+
id: "2",
86+
87+
mock: []*metal.Size{
88+
{Base: metal.Base{ID: "1"}},
89+
{Base: metal.Base{ID: "2"}},
90+
{Base: metal.Base{ID: "3"}},
91+
},
92+
want: tt.defaultBody(&metal.Size{Base: metal.Base{ID: "2"}}),
93+
wantErr: nil,
94+
},
95+
{
96+
name: "not found",
97+
id: "4",
98+
want: nil,
99+
wantErr: metal.NotFound(`no size with id "4" found`),
100+
},
101+
}
102+
for i := range tests {
103+
tests[i].run(t, tt)
104+
}
105+
}
106+
107+
func TestRethinkStore_SearchSizes(t *testing.T) {
108+
tt := &sizeTestable{}
109+
defer func() {
110+
require.NoError(t, tt.wipe())
111+
}()
112+
113+
tests := []searchTest[*metal.Size, *SizeSearchQuery]{
114+
{
115+
name: "empty result",
116+
q: &SizeSearchQuery{
117+
ID: pointer.Pointer("2"),
118+
},
119+
mock: []*metal.Size{
120+
{Base: metal.Base{ID: "1"}},
121+
},
122+
want: nil,
123+
wantErr: nil,
124+
},
125+
{
126+
name: "search by id",
127+
q: &SizeSearchQuery{
128+
ID: pointer.Pointer("2"),
129+
},
130+
mock: []*metal.Size{
131+
{Base: metal.Base{ID: "1"}},
132+
{Base: metal.Base{ID: "2"}},
133+
{Base: metal.Base{ID: "3"}},
134+
},
135+
want: []*metal.Size{
136+
tt.defaultBody(&metal.Size{Base: metal.Base{ID: "2"}}),
137+
},
138+
wantErr: nil,
139+
},
140+
{
141+
name: "search by name",
142+
q: &SizeSearchQuery{
143+
Name: pointer.Pointer("b"),
144+
},
145+
mock: []*metal.Size{
146+
{Base: metal.Base{ID: "1", Name: "a"}},
147+
{Base: metal.Base{ID: "2", Name: "b"}},
148+
{Base: metal.Base{ID: "3", Name: "c"}},
149+
},
150+
want: []*metal.Size{
151+
tt.defaultBody(&metal.Size{Base: metal.Base{ID: "2", Name: "b"}}),
152+
},
153+
wantErr: nil,
154+
},
155+
{
156+
name: "search reservation project",
157+
q: &SizeSearchQuery{
158+
Reservation: Reservation{
159+
Project: pointer.Pointer("2"),
160+
},
161+
},
162+
mock: []*metal.Size{
163+
{Base: metal.Base{ID: "1"}, Reservations: metal.Reservations{{ProjectID: "1"}}},
164+
{Base: metal.Base{ID: "2"}, Reservations: metal.Reservations{{ProjectID: "2"}}},
165+
{Base: metal.Base{ID: "3"}, Reservations: metal.Reservations{{ProjectID: "3"}}},
166+
},
167+
want: []*metal.Size{
168+
tt.defaultBody(&metal.Size{Base: metal.Base{ID: "2"}, Reservations: metal.Reservations{{ProjectID: "2"}}}),
169+
},
170+
wantErr: nil,
171+
},
172+
{
173+
name: "search reservation partition",
174+
q: &SizeSearchQuery{
175+
Reservation: Reservation{
176+
Partition: pointer.Pointer("p1"),
177+
},
178+
},
179+
mock: []*metal.Size{
180+
{Base: metal.Base{ID: "1"}, Reservations: metal.Reservations{{PartitionIDs: []string{"p1"}}}},
181+
{Base: metal.Base{ID: "2"}, Reservations: metal.Reservations{{PartitionIDs: []string{"p1", "p2"}}}},
182+
{Base: metal.Base{ID: "3"}, Reservations: metal.Reservations{{PartitionIDs: []string{"p3"}}}},
183+
},
184+
want: []*metal.Size{
185+
tt.defaultBody(&metal.Size{Base: metal.Base{ID: "1"}, Reservations: metal.Reservations{{PartitionIDs: []string{"p1"}}}}),
186+
tt.defaultBody(&metal.Size{Base: metal.Base{ID: "2"}, Reservations: metal.Reservations{{PartitionIDs: []string{"p1", "p2"}}}}),
187+
},
188+
wantErr: nil,
189+
},
190+
}
191+
192+
for i := range tests {
193+
tests[i].run(t, tt)
194+
}
195+
}
196+
197+
func TestRethinkStore_ListSizes(t *testing.T) {
198+
tt := &sizeTestable{}
199+
defer func() {
200+
require.NoError(t, tt.wipe())
201+
}()
202+
203+
tests := []listTest[*metal.Size, *SizeSearchQuery]{
204+
{
205+
name: "list",
206+
mock: []*metal.Size{
207+
{Base: metal.Base{ID: "1"}},
208+
{Base: metal.Base{ID: "2"}},
209+
{Base: metal.Base{ID: "3"}},
210+
},
211+
want: []*metal.Size{
212+
tt.defaultBody(&metal.Size{Base: metal.Base{ID: "1"}}),
213+
tt.defaultBody(&metal.Size{Base: metal.Base{ID: "2"}}),
214+
tt.defaultBody(&metal.Size{Base: metal.Base{ID: "3"}}),
215+
},
216+
},
217+
}
218+
for i := range tests {
219+
tests[i].run(t, tt)
220+
}
221+
}
222+
223+
func TestRethinkStore_CreateSize(t *testing.T) {
224+
tt := &sizeTestable{}
225+
defer func() {
226+
require.NoError(t, tt.wipe())
227+
}()
228+
229+
tests := []createTest[*metal.Size, *SizeSearchQuery]{
230+
{
231+
name: "create",
232+
want: tt.defaultBody(&metal.Size{Base: metal.Base{ID: "1"}}),
233+
wantErr: nil,
234+
},
235+
{
236+
name: "already exists",
237+
mock: []*metal.Size{
238+
{Base: metal.Base{ID: "1"}},
239+
},
240+
want: tt.defaultBody(&metal.Size{Base: metal.Base{ID: "1"}}),
241+
wantErr: metal.Conflict(`cannot create size in database, entity already exists: 1`),
242+
},
243+
}
244+
for i := range tests {
245+
tests[i].run(t, tt)
246+
}
247+
}
248+
249+
func TestRethinkStore_DeleteSize(t *testing.T) {
250+
tt := &sizeTestable{}
251+
defer func() {
252+
require.NoError(t, tt.wipe())
253+
}()
254+
255+
tests := []deleteTest[*metal.Size, *SizeSearchQuery]{
256+
{
257+
name: "delete",
258+
id: "2",
259+
mock: []*metal.Size{
260+
{Base: metal.Base{ID: "1"}},
261+
{Base: metal.Base{ID: "2"}},
262+
{Base: metal.Base{ID: "3"}},
263+
},
264+
want: []*metal.Size{
265+
tt.defaultBody(&metal.Size{Base: metal.Base{ID: "1"}}),
266+
tt.defaultBody(&metal.Size{Base: metal.Base{ID: "3"}}),
267+
},
268+
},
269+
{
270+
name: "not exists results in noop",
271+
id: "abc",
272+
mock: []*metal.Size{
273+
{Base: metal.Base{ID: "1"}},
274+
{Base: metal.Base{ID: "2"}},
275+
{Base: metal.Base{ID: "3"}},
276+
},
277+
want: []*metal.Size{
278+
tt.defaultBody(&metal.Size{Base: metal.Base{ID: "1"}}),
279+
tt.defaultBody(&metal.Size{Base: metal.Base{ID: "2"}}),
280+
tt.defaultBody(&metal.Size{Base: metal.Base{ID: "3"}}),
281+
},
282+
},
283+
}
284+
for i := range tests {
285+
tests[i].run(t, tt)
286+
}
287+
}
288+
289+
func TestRethinkStore_UpdateSize(t *testing.T) {
290+
tt := &sizeTestable{}
291+
defer func() {
292+
require.NoError(t, tt.wipe())
293+
}()
294+
295+
tests := []updateTest[*metal.Size, *SizeSearchQuery]{
296+
{
297+
name: "update",
298+
mock: []*metal.Size{
299+
{Base: metal.Base{ID: "1"}},
300+
{Base: metal.Base{ID: "2"}},
301+
{Base: metal.Base{ID: "3"}},
302+
},
303+
mutateFn: func(s *metal.Size) {
304+
s.Labels = map[string]string{"a": "b"}
305+
},
306+
want: tt.defaultBody(&metal.Size{
307+
Base: metal.Base{ID: "1"},
308+
Labels: map[string]string{"a": "b"},
309+
}),
310+
},
311+
}
312+
for i := range tests {
313+
tests[i].run(t, tt)
314+
}
315+
}

0 commit comments

Comments
 (0)