From ebd379a00f1e97b630e6d0eb12afdf1de97a47c9 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Tue, 9 Jul 2024 11:08:27 +0200 Subject: [PATCH] Add size reservation filters. (#535) --- cmd/metal-api/internal/datastore/size.go | 62 ++++ .../datastore/size_integration_test.go | 315 ++++++++++++++++++ cmd/metal-api/internal/datastore/size_test.go | 166 --------- .../internal/service/size-service.go | 42 ++- .../internal/service/size-service_test.go | 129 ++++--- cmd/metal-api/internal/service/v1/size.go | 11 +- spec/metal-api.json | 24 +- 7 files changed, 533 insertions(+), 216 deletions(-) create mode 100644 cmd/metal-api/internal/datastore/size_integration_test.go diff --git a/cmd/metal-api/internal/datastore/size.go b/cmd/metal-api/internal/datastore/size.go index b3bbfb429..45f997662 100644 --- a/cmd/metal-api/internal/datastore/size.go +++ b/cmd/metal-api/internal/datastore/size.go @@ -4,8 +4,65 @@ import ( "errors" "github.com/metal-stack/metal-api/cmd/metal-api/internal/metal" + r "gopkg.in/rethinkdb/rethinkdb-go.v6" ) +// SizeSearchQuery can be used to search sizes. +type SizeSearchQuery struct { + ID *string `json:"id" optional:"true"` + Name *string `json:"name" optional:"true"` + Labels map[string]string `json:"labels" optional:"true"` + Reservation Reservation `json:"reservation" optional:"true"` +} + +type Reservation struct { + Partition *string `json:"partition" optional:"true"` + Project *string `json:"project" optional:"true"` +} + +// GenerateTerm generates the project search query term. +func (s *SizeSearchQuery) generateTerm(rs *RethinkStore) *r.Term { + q := *rs.sizeTable() + + if s.ID != nil { + q = q.Filter(func(row r.Term) r.Term { + return row.Field("id").Eq(*s.ID) + }) + } + + if s.Name != nil { + q = q.Filter(func(row r.Term) r.Term { + return row.Field("name").Eq(*s.Name) + }) + } + + for k, v := range s.Labels { + k := k + v := v + q = q.Filter(func(row r.Term) r.Term { + return row.Field("labels").Field(k).Eq(v) + }) + } + + if s.Reservation.Project != nil { + q = q.Filter(func(row r.Term) r.Term { + return row.Field("reservations").Contains(func(p r.Term) r.Term { + return p.Field("projectid").Eq(r.Expr(*s.Reservation.Project)) + }) + }) + } + + if s.Reservation.Partition != nil { + q = q.Filter(func(row r.Term) r.Term { + return row.Field("reservations").Contains(func(p r.Term) r.Term { + return p.Field("partitionids").Contains(r.Expr(*s.Reservation.Partition)) + }) + }) + } + + return &q +} + // FindSize return a size for a given id. func (rs *RethinkStore) FindSize(id string) (*metal.Size, error) { var s metal.Size @@ -16,6 +73,11 @@ func (rs *RethinkStore) FindSize(id string) (*metal.Size, error) { return &s, nil } +// SearchSizes returns the result of the sizes search request query. +func (rs *RethinkStore) SearchSizes(q *SizeSearchQuery, sizes *metal.Sizes) error { + return rs.searchEntities(q.generateTerm(rs), sizes) +} + // ListSizes returns all sizes. func (rs *RethinkStore) ListSizes() (metal.Sizes, error) { szs := make(metal.Sizes, 0) diff --git a/cmd/metal-api/internal/datastore/size_integration_test.go b/cmd/metal-api/internal/datastore/size_integration_test.go new file mode 100644 index 000000000..c774a92f5 --- /dev/null +++ b/cmd/metal-api/internal/datastore/size_integration_test.go @@ -0,0 +1,315 @@ +//go:build integration +// +build integration + +package datastore + +import ( + "testing" + + "github.com/metal-stack/metal-api/cmd/metal-api/internal/metal" + "github.com/metal-stack/metal-lib/pkg/pointer" + "github.com/stretchr/testify/require" +) + +type sizeTestable struct{} + +func (_ *sizeTestable) wipe() error { + _, err := sharedDS.sizeTable().Delete().RunWrite(sharedDS.session) + return err +} + +func (_ *sizeTestable) create(s *metal.Size) error { // nolint:unused + return sharedDS.CreateSize(s) +} + +func (_ *sizeTestable) delete(id string) error { // nolint:unused + return sharedDS.DeleteSize(&metal.Size{Base: metal.Base{ID: id}}) +} + +func (_ *sizeTestable) update(old *metal.Size, mutateFn func(s *metal.Size)) error { // nolint:unused + mod := *old + if mutateFn != nil { + mutateFn(&mod) + } + + return sharedDS.UpdateSize(old, &mod) +} + +func (_ *sizeTestable) find(id string) (*metal.Size, error) { // nolint:unused + return sharedDS.FindSize(id) +} + +func (_ *sizeTestable) list() ([]*metal.Size, error) { // nolint:unused + res, err := sharedDS.ListSizes() + if err != nil { + return nil, err + } + + return derefSlice(res), nil +} + +func (_ *sizeTestable) search(q *SizeSearchQuery) ([]*metal.Size, error) { // nolint:unused + var res metal.Sizes + err := sharedDS.SearchSizes(q, &res) + if err != nil { + return nil, err + } + + return derefSlice(res), nil +} + +func (_ *sizeTestable) defaultBody(s *metal.Size) *metal.Size { + if s.Constraints == nil { + s.Constraints = []metal.Constraint{} + } + if s.Reservations == nil { + s.Reservations = metal.Reservations{} + } + for i := range s.Reservations { + if s.Reservations[i].PartitionIDs == nil { + s.Reservations[i].PartitionIDs = []string{} + } + } + return s +} + +func TestRethinkStore_FindSize(t *testing.T) { + tt := &sizeTestable{} + defer func() { + require.NoError(t, tt.wipe()) + }() + + tests := []findTest[*metal.Size, *SizeSearchQuery]{ + { + name: "find", + id: "2", + + mock: []*metal.Size{ + {Base: metal.Base{ID: "1"}}, + {Base: metal.Base{ID: "2"}}, + {Base: metal.Base{ID: "3"}}, + }, + want: tt.defaultBody(&metal.Size{Base: metal.Base{ID: "2"}}), + wantErr: nil, + }, + { + name: "not found", + id: "4", + want: nil, + wantErr: metal.NotFound(`no size with id "4" found`), + }, + } + for i := range tests { + tests[i].run(t, tt) + } +} + +func TestRethinkStore_SearchSizes(t *testing.T) { + tt := &sizeTestable{} + defer func() { + require.NoError(t, tt.wipe()) + }() + + tests := []searchTest[*metal.Size, *SizeSearchQuery]{ + { + name: "empty result", + q: &SizeSearchQuery{ + ID: pointer.Pointer("2"), + }, + mock: []*metal.Size{ + {Base: metal.Base{ID: "1"}}, + }, + want: nil, + wantErr: nil, + }, + { + name: "search by id", + q: &SizeSearchQuery{ + ID: pointer.Pointer("2"), + }, + mock: []*metal.Size{ + {Base: metal.Base{ID: "1"}}, + {Base: metal.Base{ID: "2"}}, + {Base: metal.Base{ID: "3"}}, + }, + want: []*metal.Size{ + tt.defaultBody(&metal.Size{Base: metal.Base{ID: "2"}}), + }, + wantErr: nil, + }, + { + name: "search by name", + q: &SizeSearchQuery{ + Name: pointer.Pointer("b"), + }, + mock: []*metal.Size{ + {Base: metal.Base{ID: "1", Name: "a"}}, + {Base: metal.Base{ID: "2", Name: "b"}}, + {Base: metal.Base{ID: "3", Name: "c"}}, + }, + want: []*metal.Size{ + tt.defaultBody(&metal.Size{Base: metal.Base{ID: "2", Name: "b"}}), + }, + wantErr: nil, + }, + { + name: "search reservation project", + q: &SizeSearchQuery{ + Reservation: Reservation{ + Project: pointer.Pointer("2"), + }, + }, + mock: []*metal.Size{ + {Base: metal.Base{ID: "1"}, Reservations: metal.Reservations{{ProjectID: "1"}}}, + {Base: metal.Base{ID: "2"}, Reservations: metal.Reservations{{ProjectID: "2"}}}, + {Base: metal.Base{ID: "3"}, Reservations: metal.Reservations{{ProjectID: "3"}}}, + }, + want: []*metal.Size{ + tt.defaultBody(&metal.Size{Base: metal.Base{ID: "2"}, Reservations: metal.Reservations{{ProjectID: "2"}}}), + }, + wantErr: nil, + }, + { + name: "search reservation partition", + q: &SizeSearchQuery{ + Reservation: Reservation{ + Partition: pointer.Pointer("p1"), + }, + }, + mock: []*metal.Size{ + {Base: metal.Base{ID: "1"}, Reservations: metal.Reservations{{PartitionIDs: []string{"p1"}}}}, + {Base: metal.Base{ID: "2"}, Reservations: metal.Reservations{{PartitionIDs: []string{"p1", "p2"}}}}, + {Base: metal.Base{ID: "3"}, Reservations: metal.Reservations{{PartitionIDs: []string{"p3"}}}}, + }, + want: []*metal.Size{ + tt.defaultBody(&metal.Size{Base: metal.Base{ID: "1"}, Reservations: metal.Reservations{{PartitionIDs: []string{"p1"}}}}), + tt.defaultBody(&metal.Size{Base: metal.Base{ID: "2"}, Reservations: metal.Reservations{{PartitionIDs: []string{"p1", "p2"}}}}), + }, + wantErr: nil, + }, + } + + for i := range tests { + tests[i].run(t, tt) + } +} + +func TestRethinkStore_ListSizes(t *testing.T) { + tt := &sizeTestable{} + defer func() { + require.NoError(t, tt.wipe()) + }() + + tests := []listTest[*metal.Size, *SizeSearchQuery]{ + { + name: "list", + mock: []*metal.Size{ + {Base: metal.Base{ID: "1"}}, + {Base: metal.Base{ID: "2"}}, + {Base: metal.Base{ID: "3"}}, + }, + want: []*metal.Size{ + tt.defaultBody(&metal.Size{Base: metal.Base{ID: "1"}}), + tt.defaultBody(&metal.Size{Base: metal.Base{ID: "2"}}), + tt.defaultBody(&metal.Size{Base: metal.Base{ID: "3"}}), + }, + }, + } + for i := range tests { + tests[i].run(t, tt) + } +} + +func TestRethinkStore_CreateSize(t *testing.T) { + tt := &sizeTestable{} + defer func() { + require.NoError(t, tt.wipe()) + }() + + tests := []createTest[*metal.Size, *SizeSearchQuery]{ + { + name: "create", + want: tt.defaultBody(&metal.Size{Base: metal.Base{ID: "1"}}), + wantErr: nil, + }, + { + name: "already exists", + mock: []*metal.Size{ + {Base: metal.Base{ID: "1"}}, + }, + want: tt.defaultBody(&metal.Size{Base: metal.Base{ID: "1"}}), + wantErr: metal.Conflict(`cannot create size in database, entity already exists: 1`), + }, + } + for i := range tests { + tests[i].run(t, tt) + } +} + +func TestRethinkStore_DeleteSize(t *testing.T) { + tt := &sizeTestable{} + defer func() { + require.NoError(t, tt.wipe()) + }() + + tests := []deleteTest[*metal.Size, *SizeSearchQuery]{ + { + name: "delete", + id: "2", + mock: []*metal.Size{ + {Base: metal.Base{ID: "1"}}, + {Base: metal.Base{ID: "2"}}, + {Base: metal.Base{ID: "3"}}, + }, + want: []*metal.Size{ + tt.defaultBody(&metal.Size{Base: metal.Base{ID: "1"}}), + tt.defaultBody(&metal.Size{Base: metal.Base{ID: "3"}}), + }, + }, + { + name: "not exists results in noop", + id: "abc", + mock: []*metal.Size{ + {Base: metal.Base{ID: "1"}}, + {Base: metal.Base{ID: "2"}}, + {Base: metal.Base{ID: "3"}}, + }, + want: []*metal.Size{ + tt.defaultBody(&metal.Size{Base: metal.Base{ID: "1"}}), + tt.defaultBody(&metal.Size{Base: metal.Base{ID: "2"}}), + tt.defaultBody(&metal.Size{Base: metal.Base{ID: "3"}}), + }, + }, + } + for i := range tests { + tests[i].run(t, tt) + } +} + +func TestRethinkStore_UpdateSize(t *testing.T) { + tt := &sizeTestable{} + defer func() { + require.NoError(t, tt.wipe()) + }() + + tests := []updateTest[*metal.Size, *SizeSearchQuery]{ + { + name: "update", + mock: []*metal.Size{ + {Base: metal.Base{ID: "1"}}, + {Base: metal.Base{ID: "2"}}, + {Base: metal.Base{ID: "3"}}, + }, + mutateFn: func(s *metal.Size) { + s.Labels = map[string]string{"a": "b"} + }, + want: tt.defaultBody(&metal.Size{ + Base: metal.Base{ID: "1"}, + Labels: map[string]string{"a": "b"}, + }), + }, + } + for i := range tests { + tests[i].run(t, tt) + } +} diff --git a/cmd/metal-api/internal/datastore/size_test.go b/cmd/metal-api/internal/datastore/size_test.go index 5b7cfd9e4..2b9a64382 100644 --- a/cmd/metal-api/internal/datastore/size_test.go +++ b/cmd/metal-api/internal/datastore/size_test.go @@ -4,176 +4,10 @@ import ( "reflect" "testing" - "github.com/google/go-cmp/cmp" "github.com/metal-stack/metal-api/cmd/metal-api/internal/metal" "github.com/metal-stack/metal-api/cmd/metal-api/internal/testdata" ) -func TestRethinkStore_FindSize(t *testing.T) { - ds, mock := InitMockDB(t) - testdata.InitMockDBData(mock) - - tests := []struct { - name string - rs *RethinkStore - id string - want *metal.Size - wantErr bool - }{ - { - name: "TestRethinkStore_FindSize Test 1", - rs: ds, - id: "1", - want: &testdata.Sz1, - wantErr: false, - }, - } - - for i := range tests { - tt := tests[i] - t.Run(tt.name, func(t *testing.T) { - got, err := tt.rs.FindSize(tt.id) - if (err != nil) != tt.wantErr { - t.Errorf("RethinkStore.FindSize() error = %v, wantErr %v", err, tt.wantErr) - return - } - if diff := cmp.Diff(got, tt.want); diff != "" { - t.Errorf("RethinkStore.FindSize() mismatch (-want +got):\n%s", diff) - } - }) - } -} - -func TestRethinkStore_ListSizes(t *testing.T) { - ds, mock := InitMockDB(t) - testdata.InitMockDBData(mock) - - tests := []struct { - name string - rs *RethinkStore - want metal.Sizes - wantErr bool - }{ - { - name: "TestRethinkStore_ListSizes Test 1", - rs: ds, - want: testdata.TestSizes, - wantErr: false, - }, - } - for i := range tests { - tt := tests[i] - t.Run(tt.name, func(t *testing.T) { - got, err := tt.rs.ListSizes() - if (err != nil) != tt.wantErr { - t.Errorf("RethinkStore.ListSizes() error = %v, wantErr %v", err, tt.wantErr) - return - } - if diff := cmp.Diff(got, tt.want); diff != "" { - t.Errorf("RethinkStore.ListSizes() mismatch (-want +got):\n%s", diff) - } - }) - } -} - -func TestRethinkStore_CreateSize(t *testing.T) { - ds, mock := InitMockDB(t) - testdata.InitMockDBData(mock) - - tests := []struct { - name string - rs *RethinkStore - size *metal.Size - wantErr bool - }{ - { - name: "TestRethinkStore_CreateSize Test 1", - rs: ds, - size: &testdata.Sz1, - wantErr: false, - }, - } - for i := range tests { - tt := tests[i] - t.Run(tt.name, func(t *testing.T) { - if err := tt.rs.CreateSize(tt.size); (err != nil) != tt.wantErr { - t.Errorf("RethinkStore.CreateSize() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestRethinkStore_DeleteSize(t *testing.T) { - ds, mock := InitMockDB(t) - testdata.InitMockDBData(mock) - - tests := []struct { - name string - rs *RethinkStore - size *metal.Size - wantErr bool - }{ - { - name: "TestRethinkStore_DeleteSize Test 1", - rs: ds, - size: &testdata.Sz1, - wantErr: false, - }, - { - name: "TestRethinkStore_DeleteSize Test 2", - rs: ds, - size: &testdata.Sz2, - wantErr: false, - }, - } - for i := range tests { - tt := tests[i] - t.Run(tt.name, func(t *testing.T) { - err := tt.rs.DeleteSize(tt.size) - if (err != nil) != tt.wantErr { - t.Errorf("RethinkStore.DeleteSize() error = %v, wantErr %v", err, tt.wantErr) - return - } - }) - } -} - -func TestRethinkStore_UpdateSize(t *testing.T) { - ds, mock := InitMockDB(t) - testdata.InitMockDBData(mock) - - tests := []struct { - name string - rs *RethinkStore - oldSize *metal.Size - newSize *metal.Size - wantErr bool - }{ - { - name: "TestRethinkStore_UpdateSize Test 1", - rs: ds, - oldSize: &testdata.Sz1, - newSize: &testdata.Sz2, - wantErr: false, - }, - { - name: "TestRethinkStore_UpdateSize Test 2", - rs: ds, - oldSize: &testdata.Sz2, - newSize: &testdata.Sz1, - wantErr: false, - }, - } - for i := range tests { - tt := tests[i] - t.Run(tt.name, func(t *testing.T) { - if err := tt.rs.UpdateSize(tt.oldSize, tt.newSize); (err != nil) != tt.wantErr { - t.Errorf("RethinkStore.UpdateSize() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - func TestRethinkStore_FromHardware(t *testing.T) { ds, mock := InitMockDB(t) testdata.InitMockDBData(mock) diff --git a/cmd/metal-api/internal/service/size-service.go b/cmd/metal-api/internal/service/size-service.go index 91050a1ac..2be4d54a3 100644 --- a/cmd/metal-api/internal/service/size-service.go +++ b/cmd/metal-api/internal/service/size-service.go @@ -12,7 +12,7 @@ import ( "github.com/metal-stack/metal-api/cmd/metal-api/internal/metal" v1 "github.com/metal-stack/metal-api/cmd/metal-api/internal/service/v1" "github.com/metal-stack/metal-lib/auditing" - "github.com/metal-stack/metal-lib/pkg/pointer" + "google.golang.org/protobuf/types/known/wrapperspb" restfulspec "github.com/emicklei/go-restful-openapi/v2" restful "github.com/emicklei/go-restful/v3" @@ -70,7 +70,7 @@ func (r *sizeResource) webService() *restful.WebService { Doc("get all size reservations"). Metadata(restfulspec.KeyOpenAPITags, tags). Metadata(auditing.Exclude, true). - Reads(v1.EmptyBody{}). + Reads(v1.SizeReservationListRequest{}). Writes([]v1.SizeReservationResponse{}). Returns(http.StatusOK, "OK", []v1.SizeReservationResponse{}). DefaultReturns("Error", httperrors.HTTPErrorResponse{})) @@ -439,19 +439,45 @@ func (r *sizeResource) updateSize(request *restful.Request, response *restful.Re } func (r *sizeResource) listSizeReservations(request *restful.Request, response *restful.Response) { - ss, err := r.ds.ListSizes() + var requestPayload v1.SizeReservationListRequest + err := request.ReadEntity(&requestPayload) + if err != nil { + r.sendError(request, response, httperrors.BadRequest(err)) + return + } + + ss := metal.Sizes{} + err = r.ds.SearchSizes(&datastore.SizeSearchQuery{ + ID: requestPayload.SizeID, + Reservation: datastore.Reservation{ + Partition: requestPayload.PartitionID, + Project: requestPayload.ProjectID, + }, + }, &ss) if err != nil { r.sendError(request, response, defaultError(err)) return } - projects, err := r.mdc.Project().Find(request.Request.Context(), &mdmv1.ProjectFindRequest{}) + pfr := &mdmv1.ProjectFindRequest{} + + if requestPayload.ProjectID != nil { + pfr.Id = wrapperspb.String(*requestPayload.ProjectID) + } + if requestPayload.Tenant != nil { + pfr.TenantId = wrapperspb.String(*requestPayload.Tenant) + } + + projects, err := r.mdc.Project().Find(request.Request.Context(), pfr) if err != nil { r.sendError(request, response, defaultError(err)) return } - ms, err := r.ds.ListMachines() + ms := metal.Machines{} + err = r.ds.SearchMachines(&datastore.MachineSearchQuery{ + PartitionID: requestPayload.PartitionID, + }, &ms) if err != nil { r.sendError(request, response, defaultError(err)) return @@ -469,8 +495,12 @@ func (r *sizeResource) listSizeReservations(request *restful.Request, response * for _, reservation := range size.Reservations { reservation := reservation + project, ok := projectsByID[reservation.ProjectID] + if !ok { + continue + } + for _, partitionID := range reservation.PartitionIDs { - project := pointer.SafeDeref(projectsByID[reservation.ProjectID]) allocations := len(machinesByProjectID[reservation.ProjectID].WithPartition(partitionID).WithSize(size.ID)) result = append(result, &v1.SizeReservationResponse{ diff --git a/cmd/metal-api/internal/service/size-service_test.go b/cmd/metal-api/internal/service/size-service_test.go index 2a61238be..e51367803 100644 --- a/cmd/metal-api/internal/service/size-service_test.go +++ b/cmd/metal-api/internal/service/size-service_test.go @@ -18,10 +18,12 @@ import ( v1 "github.com/metal-stack/metal-api/cmd/metal-api/internal/service/v1" "github.com/metal-stack/metal-api/cmd/metal-api/internal/testdata" "github.com/metal-stack/metal-lib/httperrors" + "github.com/metal-stack/metal-lib/pkg/pointer" "github.com/stretchr/testify/assert" testifymock "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "gopkg.in/rethinkdb/rethinkdb-go.v6" + "google.golang.org/protobuf/types/known/wrapperspb" + r "gopkg.in/rethinkdb/rethinkdb-go.v6" ) func TestGetSizes(t *testing.T) { @@ -79,13 +81,13 @@ func TestGetSize(t *testing.T) { func TestSuggest(t *testing.T) { tests := []struct { name string - mockFn func(mock *rethinkdb.Mock) + mockFn func(mock *r.Mock) want []metal.Constraint }{ { name: "size", - mockFn: func(mock *rethinkdb.Mock) { - mock.On(rethinkdb.DB("mockdb").Table("machine").Get("1")).Return(&metal.Machine{ + mockFn: func(mock *r.Mock) { + mock.On(r.DB("mockdb").Table("machine").Get("1")).Return(&metal.Machine{ Hardware: metal.MachineHardware{ MetalCPUs: []metal.MetalCPU{ { @@ -341,46 +343,95 @@ func TestUpdateSize(t *testing.T) { } func TestListSizeReservations(t *testing.T) { - ds, mock := datastore.InitMockDB(t) - testdata.InitMockDBData(mock) - log := slog.Default() - - psc := &mdmv1mock.ProjectServiceClient{} - psc.On("Find", testifymock.Anything, &mdmv1.ProjectFindRequest{}).Return(&mdmv1.ProjectListResponse{Projects: []*mdmv1.Project{ - {Meta: &mdmv1.Meta{Id: "p1"}}, - }}, nil) - mdc := mdm.NewMock(psc, &mdmv1mock.TenantServiceClient{}, nil, nil) - - sizeservice := NewSize(log, ds, mdc) - container := restful.NewContainer().Add(sizeservice) - - req := httptest.NewRequest("POST", "/v1/size/reservations", nil) - req.Header.Add("Content-Type", "application/json") - container = injectAdmin(log, container, req) - w := httptest.NewRecorder() - container.ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - require.Equal(t, http.StatusOK, resp.StatusCode, w.Body.String()) - var result []*v1.SizeReservationResponse - err := json.NewDecoder(resp.Body).Decode(&result) - require.NoError(t, err) - - want := []*v1.SizeReservationResponse{ + tests := []struct { + name string + req *v1.SizeReservationListRequest + dbMockFn func(mock *r.Mock) + projectMockFn func(mock *testifymock.Mock) + want []*v1.SizeReservationResponse + }{ { - SizeID: testdata.Sz1.ID, - PartitionID: "1", - ProjectID: "p1", - Reservations: 3, - UsedReservations: 1, - ProjectAllocations: 1, + name: "list reservations", + req: &v1.SizeReservationListRequest{ + SizeID: pointer.Pointer("1"), + Tenant: pointer.Pointer("t1"), + ProjectID: pointer.Pointer("p1"), + PartitionID: pointer.Pointer("a"), + }, + dbMockFn: func(mock *r.Mock) { + mock.On(r.DB("mockdb").Table("size").Filter(r.MockAnything()).Filter(r.MockAnything()).Filter(r.MockAnything())).Return(metal.Sizes{ + { + Base: metal.Base{ + ID: "1", + }, + Reservations: metal.Reservations{ + { + Amount: 3, + PartitionIDs: []string{"a"}, + ProjectID: "p1", + }, + }, + }, + }, nil) + mock.On(r.DB("mockdb").Table("machine").Filter(r.MockAnything())).Return(metal.Machines{ + { + Base: metal.Base{ + ID: "1", + }, + SizeID: "1", + PartitionID: "a", + Allocation: &metal.MachineAllocation{ + Project: "p1", + }, + }, + }, nil) + }, + projectMockFn: func(mock *testifymock.Mock) { + mock.On("Find", testifymock.Anything, &mdmv1.ProjectFindRequest{ + Id: wrapperspb.String("p1"), + TenantId: wrapperspb.String("t1"), + }).Return(&mdmv1.ProjectListResponse{Projects: []*mdmv1.Project{ + {Meta: &mdmv1.Meta{Id: "p1"}, TenantId: "t1"}, + }}, nil) + }, + want: []*v1.SizeReservationResponse{ + { + SizeID: "1", + PartitionID: "a", + Tenant: "t1", + ProjectID: "p1", + Reservations: 3, + UsedReservations: 1, + ProjectAllocations: 1, + }, + }, }, } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + projectMock = mdmv1mock.NewProjectServiceClient(t) + m = mdm.NewMock(projectMock, nil, nil, nil) + ds, dbMock = datastore.InitMockDB(t) + ws = NewSize(slog.Default(), ds, m) + ) + + if tt.dbMockFn != nil { + tt.dbMockFn(dbMock) + } + if tt.projectMockFn != nil { + tt.projectMockFn(&projectMock.Mock) + } - if diff := cmp.Diff(result, want); diff != "" { - t.Errorf("diff (-want +got):\n%s", diff) + code, got := genericWebRequest[[]*v1.SizeReservationResponse](t, ws, testViewUser, tt.req, "POST", "/v1/size/reservations") + assert.Equal(t, http.StatusOK, code) + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("diff (-want +got):\n%s", diff) + } + }) } + } func Test_longestCommonPrefix(t *testing.T) { diff --git a/cmd/metal-api/internal/service/v1/size.go b/cmd/metal-api/internal/service/v1/size.go index b8a383fdb..d37eed0c9 100644 --- a/cmd/metal-api/internal/service/v1/size.go +++ b/cmd/metal-api/internal/service/v1/size.go @@ -16,7 +16,7 @@ type SizeReservation struct { Description string `json:"description,omitempty" description:"a description for this reservation"` ProjectID string `json:"projectid" description:"the project for which this size reservation is considered"` PartitionIDs []string `json:"partitionids" description:"the partitions in which this size reservation is considered, the amount is valid for every partition"` - Labels map[string]string `json:"labels" description:"free labels associated with this size reservation."` + Labels map[string]string `json:"labels,omitempty" description:"free labels associated with this size reservation."` } type SizeCreateRequest struct { @@ -50,7 +50,14 @@ type SizeReservationResponse struct { Reservations int `json:"reservations" description:"the amount of reservations of this size reservation"` UsedReservations int `json:"usedreservations" description:"the used amount of reservations of this size reservation"` ProjectAllocations int `json:"projectallocations" description:"the amount of allocations of this project referenced by this size reservation"` - Labels map[string]string `json:"labels" description:"free labels associated with this size reservation."` + Labels map[string]string `json:"labels,omitempty" description:"free labels associated with this size reservation."` +} + +type SizeReservationListRequest struct { + SizeID *string `json:"sizeid,omitempty" description:"the size id of this size reservation"` + Tenant *string `json:"tenant,omitempty" description:"the tenant of this size reservation"` + ProjectID *string `json:"projectid,omitempty" description:"the project id of this size reservation"` + PartitionID *string `json:"partitionid,omitempty" description:"the partition id of this size reservation"` } type SizeSuggestRequest struct { diff --git a/spec/metal-api.json b/spec/metal-api.json index 065bccfbd..163f5f1cb 100644 --- a/spec/metal-api.json +++ b/spec/metal-api.json @@ -4661,11 +4661,30 @@ }, "required": [ "amount", - "labels", "partitionids", "projectid" ] }, + "v1.SizeReservationListRequest": { + "properties": { + "partitionid": { + "description": "the partition id of this size reservation", + "type": "string" + }, + "projectid": { + "description": "the project id of this size reservation", + "type": "string" + }, + "sizeid": { + "description": "the size id of this size reservation", + "type": "string" + }, + "tenant": { + "description": "the tenant of this size reservation", + "type": "string" + } + } + }, "v1.SizeReservationResponse": { "properties": { "labels": { @@ -4712,7 +4731,6 @@ } }, "required": [ - "labels", "partitionid", "projectallocations", "projectid", @@ -9046,7 +9064,7 @@ "name": "body", "required": true, "schema": { - "$ref": "#/definitions/v1.EmptyBody" + "$ref": "#/definitions/v1.SizeReservationListRequest" } } ],