diff --git a/cmd/metal-api/internal/datastore/machine.go b/cmd/metal-api/internal/datastore/machine.go index 1a30c939..eb208e94 100644 --- a/cmd/metal-api/internal/datastore/machine.go +++ b/cmd/metal-api/internal/datastore/machine.go @@ -478,7 +478,16 @@ func (rs *RethinkStore) FindWaitingMachine(ctx context.Context, projectid, parti return nil, err } - ok := checkSizeReservations(available, projectid, partitionid, partitionMachines.WithSize(size.ID).ByProjectID(), size) + var reservations metal.SizeReservations + err = rs.SearchSizeReservations(&SizeReservationSearchQuery{ + Partition: &partitionid, + SizeID: &size.ID, + }, &reservations) + if err != nil { + return nil, err + } + + ok := checkSizeReservations(available, projectid, partitionMachines.WithSize(size.ID).ByProjectID(), reservations) if !ok { return nil, errors.New("no machine available") } @@ -504,20 +513,20 @@ func (rs *RethinkStore) FindWaitingMachine(ctx context.Context, projectid, parti // checkSizeReservations returns true when an allocation is possible and // false when size reservations prevent the allocation for the given project in the given partition -func checkSizeReservations(available metal.Machines, projectid, partitionid string, machinesByProject map[string]metal.Machines, size metal.Size) bool { - if size.Reservations == nil { +func checkSizeReservations(available metal.Machines, projectid string, machinesByProject map[string]metal.Machines, reservations metal.SizeReservations) bool { + if len(reservations) == 0 { return true } var ( - reservations = 0 + amount = 0 ) - for _, r := range size.Reservations.ForPartition(partitionid) { + for _, r := range reservations { r := r // sum up the amount of reservations - reservations += r.Amount + amount += r.Amount alreadyAllocated := len(machinesByProject[r.ProjectID]) @@ -527,10 +536,10 @@ func checkSizeReservations(available metal.Machines, projectid, partitionid stri } // subtract already used up reservations of the project - reservations = max(reservations-alreadyAllocated, 0) + amount = max(amount-alreadyAllocated, 0) } - return reservations < len(available) + return amount < len(available) } func spreadAcrossRacks(allMachines, projectMachines metal.Machines, tags []string) metal.Machines { diff --git a/cmd/metal-api/internal/datastore/machine_test.go b/cmd/metal-api/internal/datastore/machine_test.go index b745d10e..30ff5c50 100644 --- a/cmd/metal-api/internal/datastore/machine_test.go +++ b/cmd/metal-api/internal/datastore/machine_test.go @@ -735,21 +735,18 @@ func Test_checkSizeReservations(t *testing.T) { p1 = "1" p2 = "2" - size = metal.Size{ - Base: metal.Base{ - ID: "c1-xlarge-x86", - }, - Reservations: metal.Reservations{ - { - Amount: 1, - ProjectID: p1, - PartitionIDs: []string{partitionA}, - }, - { - Amount: 2, - ProjectID: p2, - PartitionIDs: []string{partitionA}, - }, + reservations = metal.SizeReservations{ + { + SizeID: "c1-xlarge-x86", + Amount: 1, + ProjectID: p1, + PartitionIDs: []string{partitionA}, + }, + { + SizeID: "c1-xlarge-x86", + Amount: 2, + ProjectID: p2, + PartitionIDs: []string{partitionA}, }, } @@ -764,7 +761,7 @@ func Test_checkSizeReservations(t *testing.T) { ) // 5 available, 3 reserved, project 0 can allocate - ok := checkSizeReservations(available, p0, partitionA, projectMachines, size) + ok := checkSizeReservations(available, p0, projectMachines, reservations) require.True(t, ok) allocate(available[0].ID, p0) @@ -781,7 +778,7 @@ func Test_checkSizeReservations(t *testing.T) { }, projectMachines) // 4 available, 3 reserved, project 2 can allocate - ok = checkSizeReservations(available, p2, partitionA, projectMachines, size) + ok = checkSizeReservations(available, p2, projectMachines, reservations) require.True(t, ok) allocate(available[0].ID, p2) @@ -800,7 +797,7 @@ func Test_checkSizeReservations(t *testing.T) { }, projectMachines) // 3 available, 3 reserved (1 used), project 0 can allocate - ok = checkSizeReservations(available, p0, partitionA, projectMachines, size) + ok = checkSizeReservations(available, p0, projectMachines, reservations) require.True(t, ok) allocate(available[0].ID, p0) @@ -819,11 +816,11 @@ func Test_checkSizeReservations(t *testing.T) { }, projectMachines) // 2 available, 3 reserved (1 used), project 0 cannot allocate anymore - ok = checkSizeReservations(available, p0, partitionA, projectMachines, size) + ok = checkSizeReservations(available, p0, projectMachines, reservations) require.False(t, ok) // 2 available, 3 reserved (1 used), project 2 can allocate - ok = checkSizeReservations(available, p2, partitionA, projectMachines, size) + ok = checkSizeReservations(available, p2, projectMachines, reservations) require.True(t, ok) allocate(available[0].ID, p2) @@ -842,13 +839,13 @@ func Test_checkSizeReservations(t *testing.T) { }, projectMachines) // 1 available, 3 reserved (2 used), project 0 and 2 cannot allocate anymore - ok = checkSizeReservations(available, p0, partitionA, projectMachines, size) + ok = checkSizeReservations(available, p0, projectMachines, reservations) require.False(t, ok) - ok = checkSizeReservations(available, p2, partitionA, projectMachines, size) + ok = checkSizeReservations(available, p2, projectMachines, reservations) require.False(t, ok) // 1 available, 3 reserved (2 used), project 1 can allocate - ok = checkSizeReservations(available, p1, partitionA, projectMachines, size) + ok = checkSizeReservations(available, p1, projectMachines, reservations) require.True(t, ok) allocate(available[0].ID, p1) diff --git a/cmd/metal-api/internal/datastore/migrations/07_size_reservations_table.go b/cmd/metal-api/internal/datastore/migrations/07_size_reservations_table.go new file mode 100644 index 00000000..ddbe8626 --- /dev/null +++ b/cmd/metal-api/internal/datastore/migrations/07_size_reservations_table.go @@ -0,0 +1,85 @@ +package migrations + +import ( + "fmt" + + r "gopkg.in/rethinkdb/rethinkdb-go.v6" + + "github.com/metal-stack/metal-api/cmd/metal-api/internal/datastore" + "github.com/metal-stack/metal-api/cmd/metal-api/internal/metal" +) + +type OldReservation_Mig07 struct { + Amount int `rethinkdb:"amount" json:"amount"` + Description string `rethinkdb:"description" json:"description"` + ProjectID string `rethinkdb:"projectid" json:"projectid"` + PartitionIDs []string `rethinkdb:"partitionids" json:"partitionids"` + Labels map[string]string `rethinkdb:"labels" json:"labels"` +} + +type OldReservations_Mig07 []OldReservation_Mig07 + +type OldSize_Mig07 struct { + metal.Base + Reservations OldReservations_Mig07 `rethinkdb:"reservations" json:"reservations"` +} + +func init() { + getOldSizes := func(db *r.Term, session r.QueryExecutor) ([]OldSize_Mig07, error) { + res, err := db.Table("size").Run(session) + if err != nil { + return nil, err + } + defer res.Close() + + var entities []OldSize_Mig07 + err = res.All(&entities) + if err != nil { + return nil, fmt.Errorf("cannot fetch all entities: %w", err) + } + + return entities, nil + } + + datastore.MustRegisterMigration(datastore.Migration{ + Name: "migrate size reservations to dedicated table", + Version: 7, + Up: func(db *r.Term, session r.QueryExecutor, rs *datastore.RethinkStore) error { + oldSizes, err := getOldSizes(db, session) + if err != nil { + return err + } + + for _, old := range oldSizes { + for _, rv := range old.Reservations { + err = rs.CreateSizeReservation(&metal.SizeReservation{ + Base: metal.Base{ + ID: "", + Name: "", + Description: rv.Description, + }, + SizeID: old.ID, + Amount: rv.Amount, + ProjectID: rv.ProjectID, + PartitionIDs: rv.PartitionIDs, + Labels: rv.Labels, + }) + if err != nil { + return err + } + } + } + + // now remove the old field + + _, err = db.Table("size").Replace(func(row r.Term) r.Term { + return row.Without("reservations") + }).RunWrite(session) + if err != nil { + return err + } + + return nil + }, + }) +} diff --git a/cmd/metal-api/internal/datastore/migrations_integration/migrate_integration_test.go b/cmd/metal-api/internal/datastore/migrations_integration/migrate_integration_test.go index 9e726347..07be572c 100644 --- a/cmd/metal-api/internal/datastore/migrations_integration/migrate_integration_test.go +++ b/cmd/metal-api/internal/datastore/migrations_integration/migrate_integration_test.go @@ -12,9 +12,11 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/metal-stack/metal-api/cmd/metal-api/internal/datastore" + "github.com/metal-stack/metal-api/cmd/metal-api/internal/datastore/migrations" _ "github.com/metal-stack/metal-api/cmd/metal-api/internal/datastore/migrations" "github.com/metal-stack/metal-api/cmd/metal-api/internal/metal" "github.com/metal-stack/metal-api/test" + r "gopkg.in/rethinkdb/rethinkdb-go.v6" "testing" @@ -86,14 +88,36 @@ func Test_Migration(t *testing.T) { err = rs.CreateNetwork(n) require.NoError(t, err) + oldSize := migrations.OldSize_Mig07{ + Base: metal.Base{ + ID: "c1-xlarge-x86", + }, + Reservations: []migrations.OldReservation_Mig07{ + { + Amount: 3, + Description: "a description", + ProjectID: "project-1", + PartitionIDs: []string{"partition-a"}, + Labels: map[string]string{ + "a": "b", + }, + }, + }, + } + + _, err = r.DB("metal").Table("size").Insert(oldSize).RunWrite(rs.Session()) + require.NoError(t, err) + updateM := *m updateM.Allocation = &metal.MachineAllocation{} err = rs.UpdateMachine(m, &updateM) require.NoError(t, err) + // now run the migration err = rs.Migrate(nil, false) require.NoError(t, err) + // assert m, err = rs.FindMachineByID("1") require.NoError(t, err) @@ -105,6 +129,32 @@ func Test_Migration(t *testing.T) { assert.NotEmpty(t, n) assert.Equal(t, []string{"10.240.0.0/12"}, n.AdditionalAnnouncableCIDRs) + rvs, err := rs.ListSizeReservations() + require.NoError(t, err) + + require.Len(t, rvs, 1) + require.NotEmpty(t, rvs[0].ID) + if diff := cmp.Diff(rvs, metal.SizeReservations{ + { + Base: metal.Base{ + Description: "a description", + }, + SizeID: "c1-xlarge-x86", + Amount: 3, + ProjectID: "project-1", + PartitionIDs: []string{"partition-a"}, + Labels: map[string]string{ + "a": "b", + }, + }, + }, cmpopts.IgnoreFields(metal.SizeReservation{}, "ID", "Created", "Changed")); diff != "" { + t.Errorf("size reservations diff: %s", diff) + } + + sizes, err := rs.ListSizes() + require.NoError(t, err) + require.Len(t, sizes, 1) + ec, err = rs.FindProvisioningEventContainer("1") require.NoError(t, err) require.NoError(t, ec.Validate()) diff --git a/cmd/metal-api/internal/datastore/rethinkdb.go b/cmd/metal-api/internal/datastore/rethinkdb.go index 38cbc07c..ea21a0ff 100644 --- a/cmd/metal-api/internal/datastore/rethinkdb.go +++ b/cmd/metal-api/internal/datastore/rethinkdb.go @@ -31,6 +31,7 @@ var tables = []string{ "sharedmutex", "size", "sizeimageconstraint", + "sizereservation", "switch", "switchstatus", VRFIntegerPool.String(), VRFIntegerPool.String() + "info", @@ -78,6 +79,11 @@ func New(log *slog.Logger, dbhost string, dbname string, dbuser string, dbpass s } } +// Session exported for migration unit test +func (rs *RethinkStore) Session() r.QueryExecutor { + return rs.session +} + func multi(session r.QueryExecutor, tt ...r.Term) error { for _, t := range tt { if err := t.Exec(session); err != nil { @@ -214,6 +220,11 @@ func (rs *RethinkStore) sizeImageConstraintTable() *r.Term { return &res } +func (rs *RethinkStore) sizeReservationTable() *r.Term { + res := r.DB(rs.dbname).Table("sizereservation") + return &res +} + func (rs *RethinkStore) asnTable() *r.Term { res := r.DB(rs.dbname).Table(ASNIntegerPool.String()) return &res diff --git a/cmd/metal-api/internal/datastore/size_integration_test.go b/cmd/metal-api/internal/datastore/size_integration_test.go index c774a92f..ccd3cba1 100644 --- a/cmd/metal-api/internal/datastore/size_integration_test.go +++ b/cmd/metal-api/internal/datastore/size_integration_test.go @@ -62,14 +62,6 @@ 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 } @@ -152,41 +144,6 @@ func TestRethinkStore_SearchSizes(t *testing.T) { }, 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 { diff --git a/cmd/metal-api/internal/datastore/size_reservation.go b/cmd/metal-api/internal/datastore/size_reservation.go new file mode 100644 index 00000000..52bea505 --- /dev/null +++ b/cmd/metal-api/internal/datastore/size_reservation.go @@ -0,0 +1,91 @@ +package datastore + +import ( + "github.com/metal-stack/metal-api/cmd/metal-api/internal/metal" + r "gopkg.in/rethinkdb/rethinkdb-go.v6" +) + +// SizeReservationSearchQuery can be used to search sizes. +type SizeReservationSearchQuery struct { + ID *string `json:"id" optional:"true"` + SizeID *string `json:"sizeid" optional:"true"` + Name *string `json:"name" optional:"true"` + Labels map[string]string `json:"labels" optional:"true"` + Partition *string `json:"partition" optional:"true"` + Project *string `json:"project" optional:"true"` +} + +func (s *SizeReservationSearchQuery) generateTerm(rs *RethinkStore) *r.Term { + q := *rs.sizeReservationTable() + + if s.ID != nil { + q = q.Filter(func(row r.Term) r.Term { + return row.Field("id").Eq(*s.ID) + }) + } + + if s.SizeID != nil { + q = q.Filter(func(row r.Term) r.Term { + return row.Field("sizeid").Eq(*s.SizeID) + }) + } + + 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.Project != nil { + q = q.Filter(func(row r.Term) r.Term { + return row.Field("projectid").Eq(*s.Project) + }) + } + + if s.Partition != nil { + q = q.Filter(func(row r.Term) r.Term { + return row.Field("partitionids").Contains(r.Expr(*s.Partition)) + }) + } + + return &q +} + +func (rs *RethinkStore) FindSizeReservation(id string) (*metal.SizeReservation, error) { + var s metal.SizeReservation + err := rs.findEntityByID(rs.sizeReservationTable(), &s, id) + if err != nil { + return nil, err + } + return &s, nil +} + +func (rs *RethinkStore) SearchSizeReservations(q *SizeReservationSearchQuery, rvs *metal.SizeReservations) error { + return rs.searchEntities(q.generateTerm(rs), rvs) +} + +func (rs *RethinkStore) ListSizeReservations() (metal.SizeReservations, error) { + szs := make(metal.SizeReservations, 0) + err := rs.listEntities(rs.sizeReservationTable(), &szs) + return szs, err +} + +func (rs *RethinkStore) CreateSizeReservation(rv *metal.SizeReservation) error { + return rs.createEntity(rs.sizeReservationTable(), rv) +} + +func (rs *RethinkStore) DeleteSizeReservation(rv *metal.SizeReservation) error { + return rs.deleteEntity(rs.sizeReservationTable(), rv) +} + +func (rs *RethinkStore) UpdateSizeReservation(oldRv *metal.SizeReservation, newRv *metal.SizeReservation) error { + return rs.updateEntity(rs.sizeReservationTable(), newRv, oldRv) +} diff --git a/cmd/metal-api/internal/datastore/size_reservation_integration_test.go b/cmd/metal-api/internal/datastore/size_reservation_integration_test.go new file mode 100644 index 00000000..54e1f1eb --- /dev/null +++ b/cmd/metal-api/internal/datastore/size_reservation_integration_test.go @@ -0,0 +1,336 @@ +//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 sizeReservationTestable struct{} + +func (_ *sizeReservationTestable) wipe() error { + _, err := sharedDS.sizeReservationTable().Delete().RunWrite(sharedDS.session) + return err +} + +func (_ *sizeReservationTestable) create(s *metal.SizeReservation) error { // nolint:unused + return sharedDS.CreateSizeReservation(s) +} + +func (_ *sizeReservationTestable) delete(id string) error { // nolint:unused + return sharedDS.DeleteSizeReservation(&metal.SizeReservation{Base: metal.Base{ID: id}}) +} + +func (_ *sizeReservationTestable) update(old *metal.SizeReservation, mutateFn func(s *metal.SizeReservation)) error { // nolint:unused + mod := *old + if mutateFn != nil { + mutateFn(&mod) + } + + return sharedDS.UpdateSizeReservation(old, &mod) +} + +func (_ *sizeReservationTestable) find(id string) (*metal.SizeReservation, error) { // nolint:unused + return sharedDS.FindSizeReservation(id) +} + +func (_ *sizeReservationTestable) list() ([]*metal.SizeReservation, error) { // nolint:unused + res, err := sharedDS.ListSizeReservations() + if err != nil { + return nil, err + } + + return derefSlice(res), nil +} + +func (_ *sizeReservationTestable) search(q *SizeReservationSearchQuery) ([]*metal.SizeReservation, error) { // nolint:unused + var res metal.SizeReservations + err := sharedDS.SearchSizeReservations(q, &res) + if err != nil { + return nil, err + } + + return derefSlice(res), nil +} + +func (_ *sizeReservationTestable) defaultBody(s *metal.SizeReservation) *metal.SizeReservation { + if s.PartitionIDs == nil { + s.PartitionIDs = []string{} + } + return s +} + +func TestRethinkStore_FindSizeReservation(t *testing.T) { + tt := &sizeReservationTestable{} + defer func() { + require.NoError(t, tt.wipe()) + }() + + tests := []findTest[*metal.SizeReservation, *SizeReservationSearchQuery]{ + { + name: "find", + id: "2", + + mock: []*metal.SizeReservation{ + {Base: metal.Base{ID: "1"}}, + {Base: metal.Base{ID: "2"}}, + {Base: metal.Base{ID: "3"}}, + }, + want: tt.defaultBody(&metal.SizeReservation{Base: metal.Base{ID: "2"}}), + wantErr: nil, + }, + { + name: "not found", + id: "4", + want: nil, + wantErr: metal.NotFound(`no sizereservation with id "4" found`), + }, + } + for i := range tests { + tests[i].run(t, tt) + } +} + +func TestRethinkStore_SearchSizeReservations(t *testing.T) { + tt := &sizeReservationTestable{} + defer func() { + require.NoError(t, tt.wipe()) + }() + + tests := []searchTest[*metal.SizeReservation, *SizeReservationSearchQuery]{ + { + name: "empty result", + q: &SizeReservationSearchQuery{ + ID: pointer.Pointer("2"), + }, + mock: []*metal.SizeReservation{ + {Base: metal.Base{ID: "1"}}, + }, + want: nil, + wantErr: nil, + }, + { + name: "search by id", + q: &SizeReservationSearchQuery{ + ID: pointer.Pointer("2"), + }, + mock: []*metal.SizeReservation{ + {Base: metal.Base{ID: "1"}}, + {Base: metal.Base{ID: "2"}}, + {Base: metal.Base{ID: "3"}}, + }, + want: []*metal.SizeReservation{ + tt.defaultBody(&metal.SizeReservation{Base: metal.Base{ID: "2"}}), + }, + wantErr: nil, + }, + { + name: "search by name", + q: &SizeReservationSearchQuery{ + Name: pointer.Pointer("b"), + }, + mock: []*metal.SizeReservation{ + {Base: metal.Base{ID: "1", Name: "a"}}, + {Base: metal.Base{ID: "2", Name: "b"}}, + {Base: metal.Base{ID: "3", Name: "c"}}, + }, + want: []*metal.SizeReservation{ + tt.defaultBody(&metal.SizeReservation{Base: metal.Base{ID: "2", Name: "b"}}), + }, + wantErr: nil, + }, + { + name: "search by size", + q: &SizeReservationSearchQuery{ + SizeID: pointer.Pointer("size-a"), + }, + mock: []*metal.SizeReservation{ + {Base: metal.Base{ID: "1"}, SizeID: "size-a"}, + {Base: metal.Base{ID: "2"}, SizeID: "size-b"}, + {Base: metal.Base{ID: "3"}, SizeID: "size-c"}, + }, + want: []*metal.SizeReservation{ + tt.defaultBody(&metal.SizeReservation{Base: metal.Base{ID: "1"}, SizeID: "size-a"}), + }, + wantErr: nil, + }, + { + name: "search by label", + q: &SizeReservationSearchQuery{ + Labels: map[string]string{ + "a": "b", + }, + }, + mock: []*metal.SizeReservation{ + {Base: metal.Base{ID: "1"}, Labels: map[string]string{"a": "x"}}, + {Base: metal.Base{ID: "2"}, Labels: map[string]string{"a": "b"}}, + {Base: metal.Base{ID: "3"}, Labels: map[string]string{"a": "b"}}, + }, + want: []*metal.SizeReservation{ + tt.defaultBody(&metal.SizeReservation{Base: metal.Base{ID: "2"}, Labels: map[string]string{"a": "b"}}), + tt.defaultBody(&metal.SizeReservation{Base: metal.Base{ID: "3"}, Labels: map[string]string{"a": "b"}}), + }, + wantErr: nil, + }, + { + name: "search by partition", + q: &SizeReservationSearchQuery{ + Partition: pointer.Pointer("b"), + }, + mock: []*metal.SizeReservation{ + {Base: metal.Base{ID: "1"}, PartitionIDs: []string{"b"}}, + {Base: metal.Base{ID: "2"}, PartitionIDs: []string{"a", "b"}}, + {Base: metal.Base{ID: "3"}, PartitionIDs: []string{"a"}}, + }, + want: []*metal.SizeReservation{ + tt.defaultBody(&metal.SizeReservation{Base: metal.Base{ID: "1"}, PartitionIDs: []string{"b"}}), + tt.defaultBody(&metal.SizeReservation{Base: metal.Base{ID: "2"}, PartitionIDs: []string{"a", "b"}}), + }, + wantErr: nil, + }, + { + name: "search by project", + q: &SizeReservationSearchQuery{ + Project: pointer.Pointer("3"), + }, + mock: []*metal.SizeReservation{ + {Base: metal.Base{ID: "1"}, ProjectID: "1"}, + {Base: metal.Base{ID: "2"}, ProjectID: "2"}, + {Base: metal.Base{ID: "3"}, ProjectID: "3"}, + }, + want: []*metal.SizeReservation{ + tt.defaultBody(&metal.SizeReservation{Base: metal.Base{ID: "3"}, ProjectID: "3"}), + }, + wantErr: nil, + }, + } + + for i := range tests { + tests[i].run(t, tt) + } +} + +func TestRethinkStore_ListSizeReservations(t *testing.T) { + tt := &sizeReservationTestable{} + defer func() { + require.NoError(t, tt.wipe()) + }() + + tests := []listTest[*metal.SizeReservation, *SizeReservationSearchQuery]{ + { + name: "list", + mock: []*metal.SizeReservation{ + {Base: metal.Base{ID: "1"}}, + {Base: metal.Base{ID: "2"}}, + {Base: metal.Base{ID: "3"}}, + }, + want: []*metal.SizeReservation{ + tt.defaultBody(&metal.SizeReservation{Base: metal.Base{ID: "1"}}), + tt.defaultBody(&metal.SizeReservation{Base: metal.Base{ID: "2"}}), + tt.defaultBody(&metal.SizeReservation{Base: metal.Base{ID: "3"}}), + }, + }, + } + for i := range tests { + tests[i].run(t, tt) + } +} + +func TestRethinkStore_CreateSizeReservation(t *testing.T) { + tt := &sizeReservationTestable{} + defer func() { + require.NoError(t, tt.wipe()) + }() + + tests := []createTest[*metal.SizeReservation, *SizeReservationSearchQuery]{ + { + name: "create", + want: tt.defaultBody(&metal.SizeReservation{Base: metal.Base{ID: "1"}}), + wantErr: nil, + }, + { + name: "already exists", + mock: []*metal.SizeReservation{ + {Base: metal.Base{ID: "1"}}, + }, + want: tt.defaultBody(&metal.SizeReservation{Base: metal.Base{ID: "1"}}), + wantErr: metal.Conflict(`cannot create sizereservation in database, entity already exists: 1`), + }, + } + for i := range tests { + tests[i].run(t, tt) + } +} + +func TestRethinkStore_DeleteSizeReservation(t *testing.T) { + tt := &sizeReservationTestable{} + defer func() { + require.NoError(t, tt.wipe()) + }() + + tests := []deleteTest[*metal.SizeReservation, *SizeReservationSearchQuery]{ + { + name: "delete", + id: "2", + mock: []*metal.SizeReservation{ + {Base: metal.Base{ID: "1"}}, + {Base: metal.Base{ID: "2"}}, + {Base: metal.Base{ID: "3"}}, + }, + want: []*metal.SizeReservation{ + tt.defaultBody(&metal.SizeReservation{Base: metal.Base{ID: "1"}}), + tt.defaultBody(&metal.SizeReservation{Base: metal.Base{ID: "3"}}), + }, + }, + { + name: "not exists results in noop", + id: "abc", + mock: []*metal.SizeReservation{ + {Base: metal.Base{ID: "1"}}, + {Base: metal.Base{ID: "2"}}, + {Base: metal.Base{ID: "3"}}, + }, + want: []*metal.SizeReservation{ + tt.defaultBody(&metal.SizeReservation{Base: metal.Base{ID: "1"}}), + tt.defaultBody(&metal.SizeReservation{Base: metal.Base{ID: "2"}}), + tt.defaultBody(&metal.SizeReservation{Base: metal.Base{ID: "3"}}), + }, + }, + } + for i := range tests { + tests[i].run(t, tt) + } +} + +func TestRethinkStore_UpdateSizeReservation(t *testing.T) { + tt := &sizeReservationTestable{} + defer func() { + require.NoError(t, tt.wipe()) + }() + + tests := []updateTest[*metal.SizeReservation, *SizeReservationSearchQuery]{ + { + name: "update", + mock: []*metal.SizeReservation{ + {Base: metal.Base{ID: "1"}}, + {Base: metal.Base{ID: "2"}}, + {Base: metal.Base{ID: "3"}}, + }, + mutateFn: func(s *metal.SizeReservation) { + s.Labels = map[string]string{"a": "b"} + }, + want: tt.defaultBody(&metal.SizeReservation{ + 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/metal/size.go b/cmd/metal-api/internal/metal/size.go index 13228b5b..8449fa17 100644 --- a/cmd/metal-api/internal/metal/size.go +++ b/cmd/metal-api/internal/metal/size.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "path/filepath" - "slices" mdmv1 "github.com/metal-stack/masterdata-api/api/v1" "github.com/samber/lo" @@ -13,22 +12,10 @@ import ( // A Size represents a supported machine size. type Size struct { Base - Constraints []Constraint `rethinkdb:"constraints" json:"constraints"` - Reservations Reservations `rethinkdb:"reservations" json:"reservations"` - Labels map[string]string `rethinkdb:"labels" json:"labels"` + Constraints []Constraint `rethinkdb:"constraints" json:"constraints"` + Labels map[string]string `rethinkdb:"labels" json:"labels"` } -// Reservation defines a reservation of a size for machine allocations -type Reservation struct { - Amount int `rethinkdb:"amount" json:"amount"` - Description string `rethinkdb:"description" json:"description"` - ProjectID string `rethinkdb:"projectid" json:"projectid"` - PartitionIDs []string `rethinkdb:"partitionids" json:"partitionids"` - Labels map[string]string `rethinkdb:"labels" json:"labels"` -} - -type Reservations []Reservation - // ConstraintType ... type ConstraintType string @@ -280,10 +267,6 @@ func (s *Size) Validate(partitions PartitionMap, projects map[string]*mdmv1.Proj } } - if err := s.Reservations.Validate(partitions, projects); err != nil { - errs = append(errs, fmt.Errorf("size reservations are invalid: %w", err)) - } - if len(errs) > 0 { return fmt.Errorf("size %q is invalid: %w", s.ID, errors.Join(errs...)) } @@ -304,70 +287,3 @@ func (s *Size) Overlaps(ss *Sizes) *Size { } return nil } - -func (rs *Reservations) ForPartition(partitionID string) Reservations { - if rs == nil { - return nil - } - - var result Reservations - for _, r := range *rs { - r := r - if slices.Contains(r.PartitionIDs, partitionID) { - result = append(result, r) - } - } - - return result -} - -func (rs *Reservations) ForProject(projectID string) Reservations { - if rs == nil { - return nil - } - - var result Reservations - for _, r := range *rs { - r := r - if r.ProjectID == projectID { - result = append(result, r) - } - } - - return result -} - -func (rs *Reservations) Validate(partitions PartitionMap, projects map[string]*mdmv1.Project) error { - if rs == nil { - return nil - } - - for _, r := range *rs { - if r.Amount <= 0 { - return fmt.Errorf("amount must be a positive integer") - } - - if len(r.PartitionIDs) == 0 { - return fmt.Errorf("at least one partition id must be specified") - } - ids := map[string]bool{} - for _, partition := range r.PartitionIDs { - ids[partition] = true - if _, ok := partitions[partition]; !ok { - return fmt.Errorf("partition must exist before creating a size reservation") - } - } - if len(ids) != len(r.PartitionIDs) { - return fmt.Errorf("partitions must not contain duplicates") - } - - if r.ProjectID == "" { - return fmt.Errorf("project id must be specified") - } - if _, ok := projects[r.ProjectID]; !ok { - return fmt.Errorf("project must exist before creating a size reservation") - } - } - - return nil -} diff --git a/cmd/metal-api/internal/metal/size_reservation.go b/cmd/metal-api/internal/metal/size_reservation.go new file mode 100644 index 00000000..8af03b35 --- /dev/null +++ b/cmd/metal-api/internal/metal/size_reservation.go @@ -0,0 +1,92 @@ +package metal + +import ( + "fmt" + "slices" + + mdmv1 "github.com/metal-stack/masterdata-api/api/v1" +) + +// SizeReservation defines a reservation of a size for machine allocations +type SizeReservation struct { + Base + SizeID string `rethinkdb:"sizeid" json:"sizeid"` + Amount int `rethinkdb:"amount" json:"amount"` + ProjectID string `rethinkdb:"projectid" json:"projectid"` + PartitionIDs []string `rethinkdb:"partitionids" json:"partitionids"` + Labels map[string]string `rethinkdb:"labels" json:"labels"` +} + +type SizeReservations []SizeReservation + +func (rs SizeReservations) BySize() map[string]SizeReservations { + res := map[string]SizeReservations{} + for _, rv := range rs { + res[rv.SizeID] = append(res[rv.SizeID], rv) + } + return res +} + +func (rs *SizeReservations) ForPartition(partitionID string) SizeReservations { + if rs == nil { + return nil + } + + var result SizeReservations + for _, r := range *rs { + r := r + if slices.Contains(r.PartitionIDs, partitionID) { + result = append(result, r) + } + } + + return result +} + +func (rs *SizeReservations) Validate(sizes SizeMap, partitions PartitionMap, projects map[string]*mdmv1.Project) error { + if rs == nil { + return nil + } + + for _, r := range *rs { + err := r.Validate(sizes, partitions, projects) + if err != nil { + return err + } + } + + return nil +} + +func (r *SizeReservation) Validate(sizes SizeMap, partitions PartitionMap, projects map[string]*mdmv1.Project) error { + if r.Amount <= 0 { + return fmt.Errorf("amount must be a positive integer") + } + + if _, ok := sizes[r.SizeID]; !ok { + return fmt.Errorf("size must exist before creating a size reservation") + } + + if len(r.PartitionIDs) == 0 { + return fmt.Errorf("at least one partition id must be specified") + } + ids := map[string]bool{} + for _, partition := range r.PartitionIDs { + ids[partition] = true + if _, ok := partitions[partition]; !ok { + return fmt.Errorf("partition must exist before creating a size reservation") + } + } + if len(ids) != len(r.PartitionIDs) { + return fmt.Errorf("partitions must not contain duplicates") + } + + if r.ProjectID == "" { + return fmt.Errorf("project id must be specified") + } + if _, ok := projects[r.ProjectID]; !ok { + return fmt.Errorf("project must exist before creating a size reservation") + } + + return nil +} diff --git a/cmd/metal-api/internal/metal/size_reservation_test.go b/cmd/metal-api/internal/metal/size_reservation_test.go new file mode 100644 index 00000000..8ab5458c --- /dev/null +++ b/cmd/metal-api/internal/metal/size_reservation_test.go @@ -0,0 +1,282 @@ +package metal + +import ( + "fmt" + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + mdmv1 "github.com/metal-stack/masterdata-api/api/v1" + "github.com/metal-stack/metal-lib/pkg/testcommon" +) + +func TestReservations_ForPartition(t *testing.T) { + tests := []struct { + name string + rs *SizeReservations + partitionID string + want SizeReservations + }{ + { + name: "nil", + rs: nil, + partitionID: "a", + want: nil, + }, + { + name: "correctly filtered", + rs: &SizeReservations{ + { + PartitionIDs: []string{"a", "b"}, + }, + { + PartitionIDs: []string{"c"}, + }, + { + PartitionIDs: []string{"a"}, + }, + }, + partitionID: "a", + want: SizeReservations{ + { + PartitionIDs: []string{"a", "b"}, + }, + { + PartitionIDs: []string{"a"}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.rs.ForPartition(tt.partitionID); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Reservations.ForPartition() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestReservations_Validate(t *testing.T) { + tests := []struct { + name string + sizes SizeMap + partitions PartitionMap + projects map[string]*mdmv1.Project + rs *SizeReservations + wantErr error + }{ + { + name: "empty reservations", + sizes: nil, + partitions: nil, + projects: nil, + rs: nil, + wantErr: nil, + }, + { + name: "invalid amount", + sizes: SizeMap{ + "c1": Size{}, + }, + partitions: PartitionMap{ + "a": Partition{}, + "b": Partition{}, + "c": Partition{}, + }, + projects: map[string]*mdmv1.Project{ + "1": {}, + "2": {}, + "3": {}, + }, + rs: &SizeReservations{ + { + SizeID: "c1", + Amount: -3, + ProjectID: "3", + PartitionIDs: []string{"b"}, + }, + }, + wantErr: fmt.Errorf("amount must be a positive integer"), + }, + { + name: "size does not exist", + sizes: SizeMap{ + "c1": Size{}, + }, + partitions: PartitionMap{ + "a": Partition{}, + "b": Partition{}, + "c": Partition{}, + }, + projects: map[string]*mdmv1.Project{ + "1": {}, + "2": {}, + "3": {}, + }, + rs: &SizeReservations{ + { + SizeID: "c2", + Amount: 3, + ProjectID: "3", + PartitionIDs: []string{"d"}, + }, + }, + wantErr: fmt.Errorf("size must exist before creating a size reservation"), + }, + { + name: "no partitions referenced", + sizes: SizeMap{ + "c1": Size{}, + }, + partitions: PartitionMap{ + "a": Partition{}, + "b": Partition{}, + "c": Partition{}, + }, + projects: map[string]*mdmv1.Project{ + "1": {}, + "2": {}, + "3": {}, + }, + rs: &SizeReservations{ + { + SizeID: "c1", + Amount: 3, + ProjectID: "3", + }, + }, + wantErr: fmt.Errorf("at least one partition id must be specified"), + }, + { + name: "partition does not exist", + sizes: SizeMap{ + "c1": Size{}, + }, + partitions: PartitionMap{ + "a": Partition{}, + "b": Partition{}, + "c": Partition{}, + }, + projects: map[string]*mdmv1.Project{ + "1": {}, + "2": {}, + "3": {}, + }, + rs: &SizeReservations{ + { + SizeID: "c1", + Amount: 3, + ProjectID: "3", + PartitionIDs: []string{"d"}, + }, + }, + wantErr: fmt.Errorf("partition must exist before creating a size reservation"), + }, + { + name: "partition duplicates", + sizes: SizeMap{ + "c1": Size{}, + }, + partitions: PartitionMap{ + "a": Partition{}, + "b": Partition{}, + "c": Partition{}, + }, + projects: map[string]*mdmv1.Project{ + "1": {}, + "2": {}, + "3": {}, + }, + rs: &SizeReservations{ + { + SizeID: "c1", + Amount: 3, + ProjectID: "3", + PartitionIDs: []string{"a", "b", "c", "b"}, + }, + }, + wantErr: fmt.Errorf("partitions must not contain duplicates"), + }, + { + name: "no project referenced", + sizes: SizeMap{ + "c1": Size{}, + }, + partitions: PartitionMap{ + "a": Partition{}, + "b": Partition{}, + "c": Partition{}, + }, + projects: map[string]*mdmv1.Project{ + "1": {}, + "2": {}, + "3": {}, + }, + rs: &SizeReservations{ + { + SizeID: "c1", + Amount: 3, + PartitionIDs: []string{"a"}, + }, + }, + wantErr: fmt.Errorf("project id must be specified"), + }, + { + name: "project does not exist", + sizes: SizeMap{ + "c1": Size{}, + }, + partitions: PartitionMap{ + "a": Partition{}, + "b": Partition{}, + "c": Partition{}, + }, + projects: map[string]*mdmv1.Project{ + "1": {}, + "2": {}, + "3": {}, + }, + rs: &SizeReservations{ + { + SizeID: "c1", + Amount: 3, + ProjectID: "4", + PartitionIDs: []string{"a"}, + }, + }, + wantErr: fmt.Errorf("project must exist before creating a size reservation"), + }, + { + name: "valid reservation", + sizes: SizeMap{ + "c1": Size{}, + }, + partitions: PartitionMap{ + "a": Partition{}, + "b": Partition{}, + "c": Partition{}, + }, + projects: map[string]*mdmv1.Project{ + "1": {}, + "2": {}, + "3": {}, + }, + rs: &SizeReservations{ + { + SizeID: "c1", + Amount: 3, + ProjectID: "2", + PartitionIDs: []string{"b", "c"}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.rs.Validate(tt.sizes, tt.partitions, tt.projects) + if diff := cmp.Diff(tt.wantErr, err, testcommon.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (-want +got):\n%s", diff) + } + }) + } +} diff --git a/cmd/metal-api/internal/metal/size_test.go b/cmd/metal-api/internal/metal/size_test.go index 97c4d4cd..e565740e 100644 --- a/cmd/metal-api/internal/metal/size_test.go +++ b/cmd/metal-api/internal/metal/size_test.go @@ -1,14 +1,11 @@ package metal import ( - "fmt" "reflect" "testing" "github.com/google/go-cmp/cmp" - mdmv1 "github.com/metal-stack/masterdata-api/api/v1" "github.com/metal-stack/metal-lib/pkg/pointer" - "github.com/metal-stack/metal-lib/pkg/testcommon" "github.com/stretchr/testify/require" ) @@ -1219,275 +1216,6 @@ func TestSize_Validate(t *testing.T) { } } -func TestReservations_ForPartition(t *testing.T) { - tests := []struct { - name string - rs *Reservations - partitionID string - want Reservations - }{ - { - name: "nil", - rs: nil, - partitionID: "a", - want: nil, - }, - { - name: "correctly filtered", - rs: &Reservations{ - { - PartitionIDs: []string{"a", "b"}, - }, - { - PartitionIDs: []string{"c"}, - }, - { - PartitionIDs: []string{"a"}, - }, - }, - partitionID: "a", - want: Reservations{ - { - PartitionIDs: []string{"a", "b"}, - }, - { - PartitionIDs: []string{"a"}, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.rs.ForPartition(tt.partitionID); !reflect.DeepEqual(got, tt.want) { - t.Errorf("Reservations.ForPartition() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestReservations_ForProject(t *testing.T) { - tests := []struct { - name string - rs *Reservations - projectID string - want Reservations - }{ - { - name: "nil", - rs: nil, - projectID: "a", - want: nil, - }, - { - name: "correctly filtered", - rs: &Reservations{ - { - ProjectID: "a", - }, - { - ProjectID: "c", - }, - { - ProjectID: "a", - }, - }, - projectID: "a", - want: Reservations{ - { - ProjectID: "a", - }, - { - ProjectID: "a", - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.rs.ForProject(tt.projectID); !reflect.DeepEqual(got, tt.want) { - t.Errorf("Reservations.ForProject() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestReservations_Validate(t *testing.T) { - tests := []struct { - name string - partitions PartitionMap - projects map[string]*mdmv1.Project - rs *Reservations - wantErr error - }{ - { - name: "empty reservations", - partitions: nil, - projects: nil, - rs: nil, - wantErr: nil, - }, - { - name: "invalid amount", - partitions: PartitionMap{ - "a": Partition{}, - "b": Partition{}, - "c": Partition{}, - }, - projects: map[string]*mdmv1.Project{ - "1": {}, - "2": {}, - "3": {}, - }, - rs: &Reservations{ - { - Amount: -3, - Description: "test", - ProjectID: "3", - PartitionIDs: []string{"b"}, - }, - }, - wantErr: fmt.Errorf("amount must be a positive integer"), - }, - { - name: "no partitions referenced", - partitions: PartitionMap{ - "a": Partition{}, - "b": Partition{}, - "c": Partition{}, - }, - projects: map[string]*mdmv1.Project{ - "1": {}, - "2": {}, - "3": {}, - }, - rs: &Reservations{ - { - Amount: 3, - Description: "test", - ProjectID: "3", - }, - }, - wantErr: fmt.Errorf("at least one partition id must be specified"), - }, - { - name: "partition does not exist", - partitions: PartitionMap{ - "a": Partition{}, - "b": Partition{}, - "c": Partition{}, - }, - projects: map[string]*mdmv1.Project{ - "1": {}, - "2": {}, - "3": {}, - }, - rs: &Reservations{ - { - Amount: 3, - Description: "test", - ProjectID: "3", - PartitionIDs: []string{"d"}, - }, - }, - wantErr: fmt.Errorf("partition must exist before creating a size reservation"), - }, - { - name: "partition duplicates", - partitions: PartitionMap{ - "a": Partition{}, - "b": Partition{}, - "c": Partition{}, - }, - projects: map[string]*mdmv1.Project{ - "1": {}, - "2": {}, - "3": {}, - }, - rs: &Reservations{ - { - Amount: 3, - Description: "test", - ProjectID: "3", - PartitionIDs: []string{"a", "b", "c", "b"}, - }, - }, - wantErr: fmt.Errorf("partitions must not contain duplicates"), - }, - { - name: "no project referenced", - partitions: PartitionMap{ - "a": Partition{}, - "b": Partition{}, - "c": Partition{}, - }, - projects: map[string]*mdmv1.Project{ - "1": {}, - "2": {}, - "3": {}, - }, - rs: &Reservations{ - { - Amount: 3, - Description: "test", - PartitionIDs: []string{"a"}, - }, - }, - wantErr: fmt.Errorf("project id must be specified"), - }, - { - name: "project does not exist", - partitions: PartitionMap{ - "a": Partition{}, - "b": Partition{}, - "c": Partition{}, - }, - projects: map[string]*mdmv1.Project{ - "1": {}, - "2": {}, - "3": {}, - }, - rs: &Reservations{ - { - Amount: 3, - Description: "test", - ProjectID: "4", - PartitionIDs: []string{"a"}, - }, - }, - wantErr: fmt.Errorf("project must exist before creating a size reservation"), - }, - { - name: "valid reservation", - partitions: PartitionMap{ - "a": Partition{}, - "b": Partition{}, - "c": Partition{}, - }, - projects: map[string]*mdmv1.Project{ - "1": {}, - "2": {}, - "3": {}, - }, - rs: &Reservations{ - { - Amount: 3, - Description: "test", - ProjectID: "2", - PartitionIDs: []string{"b", "c"}, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.rs.Validate(tt.partitions, tt.projects) - if diff := cmp.Diff(tt.wantErr, err, testcommon.ErrorStringComparer()); diff != "" { - t.Errorf("error diff (-want +got):\n%s", diff) - } - }) - } -} - func TestConstraint_overlaps(t *testing.T) { tests := []struct { name string diff --git a/cmd/metal-api/internal/service/partition-service.go b/cmd/metal-api/internal/service/partition-service.go index b0573768..5f5dfd7c 100644 --- a/cmd/metal-api/internal/service/partition-service.go +++ b/cmd/metal-api/internal/service/partition-service.go @@ -375,6 +375,11 @@ func (r *partitionResource) calcPartitionCapacity(pcr *v1.PartitionCapacityReque return nil, fmt.Errorf("unable to list sizes: %w", err) } + sizeReservations, err := r.ds.ListSizeReservations() + if err != nil { + return nil, fmt.Errorf("unable to list size reservations: %w", err) + } + machinesWithIssues, err := issues.Find(&issues.Config{ Machines: ms, EventContainers: ecs, @@ -389,6 +394,7 @@ func (r *partitionResource) calcPartitionCapacity(pcr *v1.PartitionCapacityReque ecsByID = ecs.ByID() sizesByID = sizes.ByID() machinesByProject = ms.ByProjectID() + rvsBySize = sizeReservations.BySize() ) for _, m := range ms { @@ -472,7 +478,12 @@ func (r *partitionResource) calcPartitionCapacity(pcr *v1.PartitionCapacityReque for _, cap := range pc.ServerCapacities { size := sizesByID[cap.Size] - for _, reservation := range size.Reservations.ForPartition(pc.ID) { + rvs, ok := rvsBySize[size.ID] + if !ok { + continue + } + + for _, reservation := range rvs.ForPartition(pc.ID) { usedReservations := min(len(machinesByProject[reservation.ProjectID].WithSize(size.ID).WithPartition(pc.ID)), reservation.Amount) cap.Reservations += reservation.Amount diff --git a/cmd/metal-api/internal/service/partition-service_test.go b/cmd/metal-api/internal/service/partition-service_test.go index 943492d9..06a03275 100644 --- a/cmd/metal-api/internal/service/partition-service_test.go +++ b/cmd/metal-api/internal/service/partition-service_test.go @@ -250,7 +250,7 @@ func TestUpdatePartition(t *testing.T) { func TestPartitionCapacity(t *testing.T) { var ( - mockMachines = func(mock *r.Mock, liveliness metal.MachineLiveliness, reservations []metal.Reservation, ms ...metal.Machine) { + mockMachines = func(mock *r.Mock, liveliness metal.MachineLiveliness, reservations metal.SizeReservations, ms ...metal.Machine) { var ( sizes metal.Sizes events metal.ProvisioningEventContainers @@ -283,12 +283,7 @@ func TestPartitionCapacity(t *testing.T) { } } - if len(reservations) > 0 { - for i := range sizes { - sizes[i].Reservations = append(sizes[i].Reservations, reservations...) - } - } - + mock.On(r.DB("mockdb").Table("sizereservation")).Return(reservations, nil) mock.On(r.DB("mockdb").Table("machine")).Return(ms, nil) mock.On(r.DB("mockdb").Table("event")).Return(events, nil) mock.On(r.DB("mockdb").Table("partition")).Return(partitions, nil) @@ -525,8 +520,9 @@ func TestPartitionCapacity(t *testing.T) { m1 := machineTpl("1", "partition-a", "size-a", "") m1.Waiting = true - reservations := []metal.Reservation{ + reservations := []metal.SizeReservation{ { + SizeID: "size-a", Amount: 1, ProjectID: "project-123", PartitionIDs: []string{"partition-a"}, @@ -561,13 +557,15 @@ func TestPartitionCapacity(t *testing.T) { m1 := machineTpl("1", "partition-a", "size-a", "") m1.Waiting = true - reservations := []metal.Reservation{ + reservations := []metal.SizeReservation{ { + SizeID: "size-a", Amount: 1, ProjectID: "project-123", PartitionIDs: []string{"partition-a"}, }, { + SizeID: "size-a", Amount: 2, ProjectID: "project-456", PartitionIDs: []string{"partition-a"}, @@ -604,8 +602,9 @@ func TestPartitionCapacity(t *testing.T) { m3 := machineTpl("3", "partition-a", "size-a", "") m3.Waiting = true - reservations := []metal.Reservation{ + reservations := []metal.SizeReservation{ { + SizeID: "size-a", Amount: 2, ProjectID: "project-123", PartitionIDs: []string{"partition-a"}, @@ -643,8 +642,9 @@ func TestPartitionCapacity(t *testing.T) { m3 := machineTpl("3", "partition-a", "size-a", "") m3.Waiting = true - reservations := []metal.Reservation{ + reservations := []metal.SizeReservation{ { + SizeID: "size-a", Amount: 1, ProjectID: "project-123", PartitionIDs: []string{"partition-a"}, @@ -682,13 +682,15 @@ func TestPartitionCapacity(t *testing.T) { m3 := machineTpl("3", "partition-a", "size-a", "") m3.Waiting = true - reservations := []metal.Reservation{ + reservations := []metal.SizeReservation{ { + SizeID: "size-a", Amount: 2, ProjectID: "project-123", PartitionIDs: []string{"partition-a"}, }, { + SizeID: "size-a", Amount: 2, ProjectID: "project-123", PartitionIDs: []string{"partition-b"}, diff --git a/cmd/metal-api/internal/service/project-service.go b/cmd/metal-api/internal/service/project-service.go index db773159..457237dc 100644 --- a/cmd/metal-api/internal/service/project-service.go +++ b/cmd/metal-api/internal/service/project-service.go @@ -245,19 +245,15 @@ func (r *projectResource) deleteProject(request *restful.Request, response *rest return } - sizes, err := r.ds.ListSizes() + var sizeReservations metal.SizeReservations + err = r.ds.SearchSizeReservations(&datastore.SizeReservationSearchQuery{ + Project: &id, + }, &sizeReservations) if err != nil { r.sendError(request, response, defaultError(err)) return } - var sizeReservations metal.Reservations - for _, size := range sizes { - size := size - - sizeReservations = size.Reservations.ForProject(id) - } - if len(sizeReservations) > 0 { r.sendError(request, response, httperrors.BadRequest(errors.New("there are still size reservations made by this project"))) return diff --git a/cmd/metal-api/internal/service/project-service_test.go b/cmd/metal-api/internal/service/project-service_test.go index 10f9b51f..ccc0c149 100644 --- a/cmd/metal-api/internal/service/project-service_test.go +++ b/cmd/metal-api/internal/service/project-service_test.go @@ -245,6 +245,7 @@ func Test_projectResource_deleteProject(t *testing.T) { mock.On(r.DB("mockdb").Table("network").Filter(r.MockAnything(), r.FilterOpts{})).Return([]metal.Networks{}, nil) mock.On(r.DB("mockdb").Table("ip").Filter(r.MockAnything(), r.FilterOpts{})).Return([]metal.IPs{}, nil) mock.On(r.DB("mockdb").Table("size")).Return([]metal.Size{}, nil) + mock.On(r.DB("mockdb").Table("sizereservation").Filter(r.MockAnything(), r.FilterOpts{})).Return([]metal.SizeReservation{}, nil) }, want: &v1.ProjectResponse{}, wantStatus: 200, diff --git a/cmd/metal-api/internal/service/size-service.go b/cmd/metal-api/internal/service/size-service.go index 2be4d54a..c5b31336 100644 --- a/cmd/metal-api/internal/service/size-service.go +++ b/cmd/metal-api/internal/service/size-service.go @@ -1,6 +1,7 @@ package service import ( + "errors" "fmt" "log/slog" "net/http" @@ -12,7 +13,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" - "google.golang.org/protobuf/types/known/wrapperspb" + "github.com/metal-stack/metal-lib/pkg/pointer" restfulspec "github.com/emicklei/go-restful-openapi/v2" restful "github.com/emicklei/go-restful/v3" @@ -64,27 +65,6 @@ func (r *sizeResource) webService() *restful.WebService { Returns(http.StatusOK, "OK", []v1.SizeResponse{}). DefaultReturns("Error", httperrors.HTTPErrorResponse{})) - ws.Route(ws.POST("/reservations"). - To(r.listSizeReservations). - Operation("listSizeReservations"). - Doc("get all size reservations"). - Metadata(restfulspec.KeyOpenAPITags, tags). - Metadata(auditing.Exclude, true). - Reads(v1.SizeReservationListRequest{}). - Writes([]v1.SizeReservationResponse{}). - Returns(http.StatusOK, "OK", []v1.SizeReservationResponse{}). - DefaultReturns("Error", httperrors.HTTPErrorResponse{})) - - ws.Route(ws.POST("/suggest"). - To(r.suggestSize). - Operation("suggest"). - Doc("from a given machine id returns the appropriate size"). - Metadata(restfulspec.KeyOpenAPITags, tags). - Metadata(auditing.Exclude, true). - Reads(v1.SizeSuggestRequest{}). - Returns(http.StatusOK, "OK", []v1.SizeConstraint{}). - DefaultReturns("Error", httperrors.HTTPErrorResponse{})) - ws.Route(ws.DELETE("/{id}"). To(admin(r.deleteSize)). Operation("deleteSize"). @@ -115,6 +95,92 @@ func (r *sizeResource) webService() *restful.WebService { Returns(http.StatusConflict, "Conflict", httperrors.HTTPErrorResponse{}). DefaultReturns("Error", httperrors.HTTPErrorResponse{})) + // suggest + + ws.Route(ws.POST("/suggest"). + To(r.suggestSize). + Operation("suggest"). + Doc("from a given machine id returns the appropriate size"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Metadata(auditing.Exclude, true). + Reads(v1.SizeSuggestRequest{}). + Returns(http.StatusOK, "OK", []v1.SizeConstraint{}). + DefaultReturns("Error", httperrors.HTTPErrorResponse{})) + + // size reservations + + ws.Route(ws.GET("/reservations"). + To(r.listSizeReservations). + Operation("listSizeReservations"). + Doc("get all size reservations"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Metadata(auditing.Exclude, true). + Writes([]v1.SizeReservationResponse{}). + Returns(http.StatusOK, "OK", []v1.SizeReservationResponse{}). + DefaultReturns("Error", httperrors.HTTPErrorResponse{})) + + ws.Route(ws.POST("/reservations/find"). + To(r.findSizeReservations). + Operation("findSizeReservations"). + Doc("get all size reservations"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Metadata(auditing.Exclude, true). + Reads(v1.SizeReservationListRequest{}). + Writes([]v1.SizeReservationResponse{}). + Returns(http.StatusOK, "OK", []v1.SizeReservationResponse{}). + DefaultReturns("Error", httperrors.HTTPErrorResponse{})) + + ws.Route(ws.GET("/reservations/{id}"). + To(r.getSizeReservation). + Operation("getSizeReservation"). + Doc("get size reservation by id"). + Param(ws.PathParameter("id", "identifier of the size reservation").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Writes(v1.SizeReservationResponse{}). + Returns(http.StatusOK, "OK", v1.SizeReservationResponse{}). + DefaultReturns("Error", httperrors.HTTPErrorResponse{})) + + ws.Route(ws.DELETE("/reservations/{id}"). + To(editor(r.deleteSizeReservation)). + Operation("deleteSizeReservation"). + Doc("deletes a size reservation and returns the deleted entity"). + Param(ws.PathParameter("id", "identifier of the size reservation").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Writes(v1.SizeReservationResponse{}). + Returns(http.StatusOK, "OK", v1.SizeReservationResponse{}). + DefaultReturns("Error", httperrors.HTTPErrorResponse{})) + + ws.Route(ws.PUT("/reservations"). + To(editor(r.createSizeReservation)). + Operation("createSizeReservation"). + Doc("create a size reservation. if the given ID already exists a conflict is returned"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Reads(v1.SizeReservationCreateRequest{}). + Returns(http.StatusCreated, "Created", v1.SizeReservationResponse{}). + Returns(http.StatusConflict, "Conflict", httperrors.HTTPErrorResponse{}). + DefaultReturns("Error", httperrors.HTTPErrorResponse{})) + + ws.Route(ws.POST("/reservations"). + To(editor(r.updateSizeReservation)). + Operation("updateSizeReservation"). + Doc("updates a size reservation. if the size reservation was changed since this one was read, a conflict is returned"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Reads(v1.SizeReservationUpdateRequest{}). + Returns(http.StatusOK, "OK", v1.SizeReservationResponse{}). + Returns(http.StatusConflict, "Conflict", httperrors.HTTPErrorResponse{}). + DefaultReturns("Error", httperrors.HTTPErrorResponse{})) + + ws.Route(ws.POST("/reservations/usage"). + To(r.sizeReservationsUsage). + Operation("sizeReservationsUsage"). + Doc("get all size reservations"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Metadata(auditing.Exclude, true). + Reads(v1.SizeReservationListRequest{}). + Writes([]v1.SizeReservationUsageResponse{}). + Returns(http.StatusOK, "OK", []v1.SizeReservationUsageResponse{}). + DefaultReturns("Error", httperrors.HTTPErrorResponse{})) + return ws } @@ -269,16 +335,6 @@ func (r *sizeResource) createSize(request *restful.Request, response *restful.Re } constraints = append(constraints, constraint) } - var reservations metal.Reservations - for _, r := range requestPayload.SizeReservations { - reservations = append(reservations, metal.Reservation{ - Amount: r.Amount, - Description: r.Description, - ProjectID: r.ProjectID, - PartitionIDs: r.PartitionIDs, - Labels: r.Labels, - }) - } s := &metal.Size{ Base: metal.Base{ @@ -286,9 +342,8 @@ func (r *sizeResource) createSize(request *restful.Request, response *restful.Re Name: name, Description: description, }, - Constraints: constraints, - Reservations: reservations, - Labels: labels, + Constraints: constraints, + Labels: labels, } ss, err := r.ds.ListSizes() @@ -338,6 +393,20 @@ func (r *sizeResource) deleteSize(request *restful.Request, response *restful.Re return } + var rvs metal.SizeReservations + err = r.ds.SearchSizeReservations(&datastore.SizeReservationSearchQuery{ + SizeID: &s.ID, + }, &rvs) + if err != nil { + r.sendError(request, response, defaultError(err)) + return + } + + if len(rvs) > 0 { + r.sendError(request, response, httperrors.UnprocessableEntity(errors.New("cannot delete size before all size reservations were removed"))) + return + } + err = r.ds.DeleteSize(s) if err != nil { r.sendError(request, response, defaultError(err)) @@ -386,19 +455,6 @@ func (r *sizeResource) updateSize(request *restful.Request, response *restful.Re } newSize.Constraints = constraints } - var reservations metal.Reservations - if requestPayload.SizeReservations != nil { - for _, r := range requestPayload.SizeReservations { - reservations = append(reservations, metal.Reservation{ - Amount: r.Amount, - Description: r.Description, - ProjectID: r.ProjectID, - PartitionIDs: r.PartitionIDs, - Labels: r.Labels, - }) - } - newSize.Reservations = reservations - } ss, err := r.ds.ListSizes() if err != nil { @@ -439,6 +495,21 @@ func (r *sizeResource) updateSize(request *restful.Request, response *restful.Re } func (r *sizeResource) listSizeReservations(request *restful.Request, response *restful.Response) { + rvs, err := r.ds.ListSizeReservations() + if err != nil { + r.sendError(request, response, defaultError(err)) + return + } + + result := []*v1.SizeReservationResponse{} + for i := range rvs { + result = append(result, v1.NewSizeReservationResponse(&rvs[i])) + } + + r.send(request, response, http.StatusOK, result) +} + +func (r *sizeResource) findSizeReservations(request *restful.Request, response *restful.Response) { var requestPayload v1.SizeReservationListRequest err := request.ReadEntity(&requestPayload) if err != nil { @@ -446,29 +517,194 @@ func (r *sizeResource) listSizeReservations(request *restful.Request, response * return } - ss := metal.Sizes{} - err = r.ds.SearchSizes(&datastore.SizeSearchQuery{ - ID: requestPayload.SizeID, - Reservation: datastore.Reservation{ - Partition: requestPayload.PartitionID, - Project: requestPayload.ProjectID, + var rvs metal.SizeReservations + err = r.ds.SearchSizeReservations(&datastore.SizeReservationSearchQuery{ + SizeID: requestPayload.SizeID, + Partition: requestPayload.PartitionID, + Project: requestPayload.ProjectID, + ID: requestPayload.ID, + }, &rvs) + if err != nil { + r.sendError(request, response, defaultError(err)) + return + } + + result := []*v1.SizeReservationResponse{} + for i := range rvs { + result = append(result, v1.NewSizeReservationResponse(&rvs[i])) + } + + r.send(request, response, http.StatusOK, result) +} + +func (r *sizeResource) getSizeReservation(request *restful.Request, response *restful.Response) { + id := request.PathParameter("id") + + rv, err := r.ds.FindSizeReservation(id) + if err != nil { + r.sendError(request, response, defaultError(err)) + return + } + + r.send(request, response, http.StatusOK, v1.NewSizeReservationResponse(rv)) +} + +func (r *sizeResource) createSizeReservation(request *restful.Request, response *restful.Response) { + var requestPayload v1.SizeReservationCreateRequest + err := request.ReadEntity(&requestPayload) + if err != nil { + r.sendError(request, response, httperrors.BadRequest(err)) + return + } + + rv := &metal.SizeReservation{ + Base: metal.Base{ + ID: requestPayload.ID, + Name: pointer.SafeDeref(requestPayload.Name), + Description: pointer.SafeDeref(requestPayload.Description), }, - }, &ss) + SizeID: requestPayload.SizeID, + Amount: requestPayload.Amount, + ProjectID: requestPayload.ProjectID, + PartitionIDs: requestPayload.PartitionIDs, + Labels: requestPayload.Labels, + } + + size, err := r.ds.FindSize(requestPayload.SizeID) + if err != nil { + r.sendError(request, response, defaultError(err)) + return + } + + ps, err := r.ds.ListPartitions() + if err != nil { + r.sendError(request, response, defaultError(err)) + return + } + + project, err := r.mdc.Project().Get(request.Request.Context(), &mdmv1.ProjectGetRequest{Id: requestPayload.ProjectID}) + if err != nil { + r.sendError(request, response, defaultError(err)) + return + } + + err = rv.Validate(metal.SizeMap{requestPayload.SizeID: *size}, ps.ByID(), map[string]*mdmv1.Project{requestPayload.ProjectID: project.Project}) + if err != nil { + r.sendError(request, response, httperrors.BadRequest(err)) + return + } + + err = r.ds.CreateSizeReservation(rv) + if err != nil { + r.sendError(request, response, defaultError(err)) + return + } + + r.send(request, response, http.StatusCreated, v1.NewSizeReservationResponse(rv)) +} + +func (r *sizeResource) updateSizeReservation(request *restful.Request, response *restful.Response) { + var requestPayload v1.SizeReservationUpdateRequest + err := request.ReadEntity(&requestPayload) + if err != nil { + r.sendError(request, response, httperrors.BadRequest(err)) + return + } + + oldRv, err := r.ds.FindSizeReservation(requestPayload.ID) + if err != nil { + r.sendError(request, response, defaultError(err)) + return + } + + rv := *oldRv + + if requestPayload.Name != nil { + rv.Name = *requestPayload.Name + } + if requestPayload.Description != nil { + rv.Description = *requestPayload.Description + } + if requestPayload.Labels != nil { + if len(requestPayload.Labels) == 0 { + rv.Labels = nil + } else { + rv.Labels = requestPayload.Labels + } + } + if requestPayload.Amount != nil { + rv.Amount = *requestPayload.Amount + } + if len(requestPayload.PartitionIDs) > 0 { + rv.PartitionIDs = requestPayload.PartitionIDs + } + + size, err := r.ds.FindSize(rv.SizeID) if err != nil { r.sendError(request, response, defaultError(err)) return } - pfr := &mdmv1.ProjectFindRequest{} + ps, err := r.ds.ListPartitions() + if err != nil { + r.sendError(request, response, defaultError(err)) + return + } - if requestPayload.ProjectID != nil { - pfr.Id = wrapperspb.String(*requestPayload.ProjectID) + project, err := r.mdc.Project().Get(request.Request.Context(), &mdmv1.ProjectGetRequest{Id: rv.ProjectID}) + if err != nil { + r.sendError(request, response, defaultError(err)) + return } - if requestPayload.Tenant != nil { - pfr.TenantId = wrapperspb.String(*requestPayload.Tenant) + + err = rv.Validate(metal.SizeMap{rv.SizeID: *size}, ps.ByID(), map[string]*mdmv1.Project{rv.ProjectID: project.Project}) + if err != nil { + r.sendError(request, response, httperrors.BadRequest(err)) + return } - projects, err := r.mdc.Project().Find(request.Request.Context(), pfr) + err = r.ds.UpdateSizeReservation(oldRv, &rv) + if err != nil { + r.sendError(request, response, defaultError(err)) + return + } + + r.send(request, response, http.StatusOK, v1.NewSizeReservationResponse(&rv)) +} + +func (r *sizeResource) deleteSizeReservation(request *restful.Request, response *restful.Response) { + id := request.PathParameter("id") + + rv, err := r.ds.FindSizeReservation(id) + if err != nil { + r.sendError(request, response, defaultError(err)) + return + } + + err = r.ds.DeleteSizeReservation(rv) + if err != nil { + r.sendError(request, response, defaultError(err)) + return + } + + r.send(request, response, http.StatusOK, v1.NewSizeReservationResponse(rv)) +} + +func (r *sizeResource) sizeReservationsUsage(request *restful.Request, response *restful.Response) { + var requestPayload v1.SizeReservationListRequest + err := request.ReadEntity(&requestPayload) + if err != nil { + r.sendError(request, response, httperrors.BadRequest(err)) + return + } + + rvs := metal.SizeReservations{} + err = r.ds.SearchSizeReservations(&datastore.SizeReservationSearchQuery{ + ID: requestPayload.ID, + SizeID: requestPayload.SizeID, + Partition: requestPayload.PartitionID, + Project: requestPayload.ProjectID, + }, &rvs) if err != nil { r.sendError(request, response, defaultError(err)) return @@ -484,37 +720,27 @@ func (r *sizeResource) listSizeReservations(request *restful.Request, response * } var ( - result []*v1.SizeReservationResponse - projectsByID = projectsByID(projects.Projects) + result []*v1.SizeReservationUsageResponse machinesByProjectID = ms.ByProjectID() ) - for _, size := range ss { - size := size - - for _, reservation := range size.Reservations { - reservation := reservation - - project, ok := projectsByID[reservation.ProjectID] - if !ok { - continue - } - - for _, partitionID := range reservation.PartitionIDs { - allocations := len(machinesByProjectID[reservation.ProjectID].WithPartition(partitionID).WithSize(size.ID)) - - result = append(result, &v1.SizeReservationResponse{ - SizeID: size.ID, - PartitionID: partitionID, - Tenant: project.TenantId, - ProjectID: reservation.ProjectID, - ProjectName: project.Name, - Reservations: reservation.Amount, - UsedReservations: min(reservation.Amount, allocations), - ProjectAllocations: allocations, - Labels: reservation.Labels, - }) - } + for _, reservation := range rvs { + for _, partitionID := range reservation.PartitionIDs { + allocations := len(machinesByProjectID[reservation.ProjectID].WithPartition(partitionID).WithSize(reservation.SizeID)) + + result = append(result, &v1.SizeReservationUsageResponse{ + Common: v1.Common{ + Identifiable: v1.Identifiable{ID: reservation.ID}, + Describable: v1.Describable{Name: &reservation.Name, Description: &reservation.Description}, + }, + SizeID: reservation.SizeID, + PartitionID: partitionID, + ProjectID: reservation.ProjectID, + Amount: reservation.Amount, + UsedAmount: min(reservation.Amount, allocations), + ProjectAllocations: allocations, + Labels: reservation.Labels, + }) } } diff --git a/cmd/metal-api/internal/service/size-service_test.go b/cmd/metal-api/internal/service/size-service_test.go index e5136780..87c9a041 100644 --- a/cmd/metal-api/internal/service/size-service_test.go +++ b/cmd/metal-api/internal/service/size-service_test.go @@ -10,6 +10,7 @@ import ( restful "github.com/emicklei/go-restful/v3" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" mdmv1 "github.com/metal-stack/masterdata-api/api/v1" mdmv1mock "github.com/metal-stack/masterdata-api/api/v1/mocks" mdm "github.com/metal-stack/masterdata-api/pkg/client" @@ -22,7 +23,6 @@ import ( "github.com/stretchr/testify/assert" testifymock "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/wrapperspb" r "gopkg.in/rethinkdb/rethinkdb-go.v6" ) @@ -253,15 +253,6 @@ func TestCreateSize(t *testing.T) { Max: 100, }, }, - SizeReservations: []v1.SizeReservation{ - { - Amount: 3, - ProjectID: "a", - PartitionIDs: []string{testdata.Partition1.ID}, - Description: "test", - Labels: map[string]string{"a": "b"}, - }, - }, } js, err := json.Marshal(createRequest) require.NoError(t, err) @@ -342,35 +333,31 @@ func TestUpdateSize(t *testing.T) { require.Equal(t, maxCores, result.SizeConstraints[0].Max) } -func TestListSizeReservations(t *testing.T) { +func Test_ListSizeReservationsUsage(t *testing.T) { tests := []struct { name string req *v1.SizeReservationListRequest dbMockFn func(mock *r.Mock) projectMockFn func(mock *testifymock.Mock) - want []*v1.SizeReservationResponse + want []*v1.SizeReservationUsageResponse }{ { - name: "list reservations", + name: "list reservations usage", 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{ + mock.On(r.DB("mockdb").Table("sizereservation").Filter(r.MockAnything()).Filter(r.MockAnything()).Filter(r.MockAnything())).Return(metal.SizeReservations{ { Base: metal.Base{ ID: "1", }, - Reservations: metal.Reservations{ - { - Amount: 3, - PartitionIDs: []string{"a"}, - ProjectID: "p1", - }, - }, + SizeID: "1", + Amount: 3, + PartitionIDs: []string{"a"}, + ProjectID: "p1", }, }, nil) mock.On(r.DB("mockdb").Table("machine").Filter(r.MockAnything())).Return(metal.Machines{ @@ -386,22 +373,22 @@ func TestListSizeReservations(t *testing.T) { }, }, 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{ + want: []*v1.SizeReservationUsageResponse{ { + Common: v1.Common{ + Identifiable: v1.Identifiable{ + ID: "1", + }, + Describable: v1.Describable{ + Name: pointer.Pointer(""), + Description: pointer.Pointer(""), + }, + }, SizeID: "1", PartitionID: "a", - Tenant: "t1", ProjectID: "p1", - Reservations: 3, - UsedReservations: 1, + Amount: 3, + UsedAmount: 1, ProjectAllocations: 1, }, }, @@ -423,7 +410,69 @@ func TestListSizeReservations(t *testing.T) { tt.projectMockFn(&projectMock.Mock) } - code, got := genericWebRequest[[]*v1.SizeReservationResponse](t, ws, testViewUser, tt.req, "POST", "/v1/size/reservations") + code, got := genericWebRequest[[]*v1.SizeReservationUsageResponse](t, ws, testViewUser, tt.req, "POST", "/v1/size/reservations/usage") + assert.Equal(t, http.StatusOK, code) + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("diff (-want +got):\n%s", diff) + } + }) + } +} + +func Test_ListSizeReservations(t *testing.T) { + tests := []struct { + name string + dbMockFn func(mock *r.Mock) + projectMockFn func(mock *testifymock.Mock) + want []*v1.SizeReservationResponse + }{ + { + name: "list reservations", + dbMockFn: func(mock *r.Mock) { + mock.On(r.DB("mockdb").Table("sizereservation")).Return(metal.SizeReservations{ + { + Base: metal.Base{ID: "1"}, + SizeID: "1", + Amount: 3, + PartitionIDs: []string{"a"}, + ProjectID: "p1", + }, + }, nil) + }, + projectMockFn: func(mock *testifymock.Mock) { + }, + want: []*v1.SizeReservationResponse{ + { + Common: v1.Common{ + Identifiable: v1.Identifiable{ID: "1"}, + Describable: v1.Describable{Name: pointer.Pointer(""), Description: pointer.Pointer("")}, + }, + SizeID: "1", + PartitionIDs: []string{"a"}, + ProjectID: "p1", + Amount: 3, + }, + }, + }, + } + 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) + } + + code, got := genericWebRequest[[]*v1.SizeReservationResponse](t, ws, testViewUser, nil, "GET", "/v1/size/reservations") assert.Equal(t, http.StatusOK, code) if diff := cmp.Diff(tt.want, got); diff != "" { @@ -431,7 +480,366 @@ func TestListSizeReservations(t *testing.T) { } }) } +} +func Test_FindSizeReservationsUsage(t *testing.T) { + tests := []struct { + name string + req *v1.SizeReservationListRequest + dbMockFn func(mock *r.Mock) + projectMockFn func(mock *testifymock.Mock) + want []*v1.SizeReservationResponse + }{ + { + name: "find reservations", + req: &v1.SizeReservationListRequest{ + SizeID: pointer.Pointer("1"), + ProjectID: pointer.Pointer("p1"), + PartitionID: pointer.Pointer("a"), + }, + dbMockFn: func(mock *r.Mock) { + mock.On(r.DB("mockdb").Table("sizereservation").Filter(r.MockAnything()).Filter(r.MockAnything()).Filter(r.MockAnything())).Return(metal.SizeReservations{ + { + Base: metal.Base{ + ID: "1", + }, + SizeID: "1", + Amount: 3, + PartitionIDs: []string{"a"}, + ProjectID: "p1", + }, + }, nil) + }, + want: []*v1.SizeReservationResponse{ + { + Common: v1.Common{ + Identifiable: v1.Identifiable{ + ID: "1", + }, + Describable: v1.Describable{ + Name: pointer.Pointer(""), + Description: pointer.Pointer(""), + }, + }, + SizeID: "1", + PartitionIDs: []string{"a"}, + ProjectID: "p1", + Amount: 3, + }, + }, + }, + } + 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) + } + + code, got := genericWebRequest[[]*v1.SizeReservationResponse](t, ws, testViewUser, tt.req, "POST", "/v1/size/reservations/find") + assert.Equal(t, http.StatusOK, code) + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("diff (-want +got):\n%s", diff) + } + }) + } +} + +func Test_GetSizeReservationsUsage(t *testing.T) { + tests := []struct { + name string + id string + dbMockFn func(mock *r.Mock) + projectMockFn func(mock *testifymock.Mock) + want *v1.SizeReservationResponse + }{ + { + name: "get reservation", + id: "1", + dbMockFn: func(mock *r.Mock) { + mock.On(r.DB("mockdb").Table("sizereservation").Get("1")).Return(metal.SizeReservation{ + Base: metal.Base{ + ID: "1", + }, + SizeID: "1", + Amount: 3, + PartitionIDs: []string{"a"}, + ProjectID: "p1", + }, nil) + }, + want: &v1.SizeReservationResponse{ + Common: v1.Common{ + Identifiable: v1.Identifiable{ + ID: "1", + }, + Describable: v1.Describable{ + Name: pointer.Pointer(""), + Description: pointer.Pointer(""), + }, + }, + SizeID: "1", + PartitionIDs: []string{"a"}, + ProjectID: "p1", + Amount: 3, + }, + }, + } + 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) + } + + code, got := genericWebRequest[*v1.SizeReservationResponse](t, ws, testViewUser, nil, "GET", "/v1/size/reservations/"+tt.id) + assert.Equal(t, http.StatusOK, code) + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("diff (-want +got):\n%s", diff) + } + }) + } +} + +func Test_DeleteSizeReservationsUsage(t *testing.T) { + tests := []struct { + name string + id string + dbMockFn func(mock *r.Mock) + projectMockFn func(mock *testifymock.Mock) + want *v1.SizeReservationResponse + }{ + { + name: "delete reservation", + id: "1", + dbMockFn: func(mock *r.Mock) { + mock.On(r.DB("mockdb").Table("sizereservation").Get("1")).Return(metal.SizeReservation{ + Base: metal.Base{ + ID: "1", + }, + SizeID: "1", + Amount: 3, + PartitionIDs: []string{"a"}, + ProjectID: "p1", + }, nil) + + mock.On(r.DB("mockdb").Table("sizereservation").Get("1").Delete()).Return(testdata.EmptyResult, nil) + }, + want: &v1.SizeReservationResponse{ + Common: v1.Common{ + Identifiable: v1.Identifiable{ + ID: "1", + }, + Describable: v1.Describable{ + Name: pointer.Pointer(""), + Description: pointer.Pointer(""), + }, + }, + SizeID: "1", + PartitionIDs: []string{"a"}, + ProjectID: "p1", + Amount: 3, + }, + }, + } + 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) + } + + code, got := genericWebRequest[*v1.SizeReservationResponse](t, ws, testAdminUser, nil, "DELETE", "/v1/size/reservations/"+tt.id) + assert.Equal(t, http.StatusOK, code) + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("diff (-want +got):\n%s", diff) + } + }) + } +} + +func Test_CreateSizeReservationsUsage(t *testing.T) { + tests := []struct { + name string + req *v1.SizeReservationCreateRequest + dbMockFn func(mock *r.Mock) + projectMockFn func(mock *testifymock.Mock) + want *v1.SizeReservationResponse + }{ + { + name: "create reservation", + req: &v1.SizeReservationCreateRequest{ + Common: v1.Common{ + Identifiable: v1.Identifiable{ + ID: "1", + }, + Describable: v1.Describable{ + Description: pointer.Pointer("a description"), + }, + }, + SizeID: "1", + PartitionIDs: []string{"a"}, + ProjectID: "p1", + Amount: 3, + Labels: map[string]string{ + "a": "b", + }, + }, + dbMockFn: func(mock *r.Mock) { + mock.On(r.DB("mockdb").Table("size").Get("1")).Return(metal.Size{Base: metal.Base{ID: "1"}}, nil) + mock.On(r.DB("mockdb").Table("partition")).Return(metal.Partitions{{Base: metal.Base{ID: "a"}}}, nil) + mock.On(r.DB("mockdb").Table("sizereservation").Insert(r.MockAnything())).Return(testdata.EmptyResult, nil) + }, + projectMockFn: func(mock *testifymock.Mock) { + mock.On("Get", testifymock.Anything, &mdmv1.ProjectGetRequest{Id: "p1"}).Return(&mdmv1.ProjectResponse{Project: &mdmv1.Project{Meta: &mdmv1.Meta{Id: "p1"}}}, nil) + }, + want: &v1.SizeReservationResponse{ + Common: v1.Common{ + Identifiable: v1.Identifiable{ + ID: "1", + }, + Describable: v1.Describable{ + Name: pointer.Pointer(""), + Description: pointer.Pointer("a description"), + }, + }, + SizeID: "1", + PartitionIDs: []string{"a"}, + ProjectID: "p1", + Amount: 3, + Labels: map[string]string{ + "a": "b", + }, + }, + }, + } + 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) + } + + code, got := genericWebRequest[*v1.SizeReservationResponse](t, ws, testAdminUser, tt.req, "PUT", "/v1/size/reservations") + assert.Equal(t, http.StatusCreated, code) + + if diff := cmp.Diff(tt.want, got, cmpopts.IgnoreFields(v1.SizeReservationResponse{}, "Timestamps")); diff != "" { + t.Errorf("diff (-want +got):\n%s", diff) + } + }) + } +} + +func Test_UpdateSizeReservationsUsage(t *testing.T) { + tests := []struct { + name string + req *v1.SizeReservationUpdateRequest + dbMockFn func(mock *r.Mock) + projectMockFn func(mock *testifymock.Mock) + want *v1.SizeReservationResponse + }{ + { + name: "update reservation", + req: &v1.SizeReservationUpdateRequest{ + Common: v1.Common{ + Identifiable: v1.Identifiable{ID: "1"}, + Describable: v1.Describable{Description: pointer.Pointer("b description")}, + }, + PartitionIDs: []string{"b"}, + Amount: pointer.Pointer(4), + Labels: map[string]string{"c": "d"}, + }, + dbMockFn: func(mock *r.Mock) { + mock.On(r.DB("mockdb").Table("size").Get("1")).Return(metal.Size{Base: metal.Base{ID: "1"}}, nil) + mock.On(r.DB("mockdb").Table("sizereservation").Get("1")).Return(metal.SizeReservation{Base: metal.Base{ID: "1"}, SizeID: "1", ProjectID: "p1"}, nil) + mock.On(r.DB("mockdb").Table("partition")).Return(metal.Partitions{{Base: metal.Base{ID: "b"}}}, nil) + mock.On(r.DB("mockdb").Table("sizereservation").Get("1").Replace(r.MockAnything())).Return(testdata.EmptyResult, nil) + }, + projectMockFn: func(mock *testifymock.Mock) { + mock.On("Get", testifymock.Anything, &mdmv1.ProjectGetRequest{Id: "p1"}).Return(&mdmv1.ProjectResponse{Project: &mdmv1.Project{Meta: &mdmv1.Meta{Id: "p1"}}}, nil) + }, + want: &v1.SizeReservationResponse{ + Common: v1.Common{ + Identifiable: v1.Identifiable{ + ID: "1", + }, + Describable: v1.Describable{ + Name: pointer.Pointer(""), + Description: pointer.Pointer("b description"), + }, + }, + SizeID: "1", + PartitionIDs: []string{"b"}, + ProjectID: "p1", + Amount: 4, + Labels: map[string]string{ + "c": "d", + }, + }, + }, + } + 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) + } + + code, got := genericWebRequest[*v1.SizeReservationResponse](t, ws, testAdminUser, tt.req, "POST", "/v1/size/reservations") + assert.Equal(t, http.StatusOK, code) + + if diff := cmp.Diff(tt.want, got, cmpopts.IgnoreFields(v1.SizeReservationResponse{}, "Timestamps")); 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 d37eed0c..0b828724 100644 --- a/cmd/metal-api/internal/service/v1/size.go +++ b/cmd/metal-api/internal/service/v1/size.go @@ -11,51 +11,65 @@ type SizeConstraint struct { Identifier string `json:"identifier,omitempty" description:"glob pattern which matches to the given type, for example gpu pci id"` } -type SizeReservation struct { - Amount int `json:"amount" description:"the amount of reserved machine allocations for this size"` - 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,omitempty" description:"free labels associated with this size reservation."` -} - type SizeCreateRequest struct { Common - SizeConstraints []SizeConstraint `json:"constraints" description:"a list of constraints that defines this size"` - SizeReservations []SizeReservation `json:"reservations,omitempty" description:"reservations for this size, which are considered during machine allocation" optional:"true"` - Labels map[string]string `json:"labels" description:"free labels that you associate with this size." optional:"true"` + SizeConstraints []SizeConstraint `json:"constraints" description:"a list of constraints that defines this size"` + Labels map[string]string `json:"labels" description:"free labels that you associate with this size." optional:"true"` } type SizeUpdateRequest struct { Common - SizeConstraints *[]SizeConstraint `json:"constraints" description:"a list of constraints that defines this size" optional:"true"` - SizeReservations []SizeReservation `json:"reservations,omitempty" description:"reservations for this size, which are considered during machine allocation" optional:"true"` - Labels map[string]string `json:"labels" description:"free labels that you associate with this size." optional:"true"` + SizeConstraints *[]SizeConstraint `json:"constraints" description:"a list of constraints that defines this size" optional:"true"` + Labels map[string]string `json:"labels" description:"free labels that you associate with this size." optional:"true"` } type SizeResponse struct { Common - SizeConstraints []SizeConstraint `json:"constraints" description:"a list of constraints that defines this size"` - SizeReservations []SizeReservation `json:"reservations,omitempty" description:"reservations for this size, which are considered during machine allocation" optional:"true"` - Labels map[string]string `json:"labels" description:"free labels that you associate with this size."` + SizeConstraints []SizeConstraint `json:"constraints" description:"a list of constraints that defines this size"` + Labels map[string]string `json:"labels" description:"free labels that you associate with this size."` Timestamps } +type SizeReservationCreateRequest struct { + Common + SizeID string `json:"sizeid" description:"the size id of this size reservation"` + PartitionIDs []string `json:"partitionids" description:"the partition id of this size reservation"` + ProjectID string `json:"projectid" description:"the project id of this size reservation"` + Amount int `json:"amount" description:"the amount of reservations of this size reservation"` + Labels map[string]string `json:"labels,omitempty" description:"free labels associated with this size reservation."` +} + +type SizeReservationUpdateRequest struct { + Common + PartitionIDs []string `json:"partitionids" description:"the partition id of this size reservation"` + Amount *int `json:"amount" description:"the amount of reservations of this size reservation"` + Labels map[string]string `json:"labels,omitempty" description:"free labels associated with this size reservation."` +} + type SizeReservationResponse struct { + Common + Timestamps + SizeID string `json:"sizeid" description:"the size id of this size reservation"` + PartitionIDs []string `json:"partitionids" description:"the partition id of this size reservation"` + ProjectID string `json:"projectid" description:"the project id of this size reservation"` + Amount int `json:"amount" description:"the amount of reservations of this size reservation"` + Labels map[string]string `json:"labels,omitempty" description:"free labels associated with this size reservation."` +} + +type SizeReservationUsageResponse struct { + Common SizeID string `json:"sizeid" description:"the size id of this size reservation"` PartitionID string `json:"partitionid" description:"the partition id of this size reservation"` - Tenant string `json:"tenant" description:"the tenant of this size reservation"` ProjectID string `json:"projectid" description:"the project id of this size reservation"` - ProjectName string `json:"projectname" description:"the project name of this size reservation"` - 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"` + Amount int `json:"amount" description:"the amount of reservations of this size reservation"` + UsedAmount int `json:"usedamount" 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,omitempty" description:"free labels associated with this size reservation."` } type SizeReservationListRequest struct { + ID *string `json:"id,omitempty" description:"the id of this size reservation"` 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"` } @@ -93,18 +107,6 @@ func NewSizeResponse(s *metal.Size) *SizeResponse { constraints = append(constraints, constraint) } - reservations := []SizeReservation{} - for _, r := range s.Reservations { - reservation := SizeReservation{ - Amount: r.Amount, - Description: r.Description, - ProjectID: r.ProjectID, - PartitionIDs: r.PartitionIDs, - Labels: r.Labels, - } - reservations = append(reservations, reservation) - } - labels := map[string]string{} if s.Labels != nil { labels = s.Labels @@ -120,8 +122,7 @@ func NewSizeResponse(s *metal.Size) *SizeResponse { Description: &s.Description, }, }, - SizeReservations: reservations, - SizeConstraints: constraints, + SizeConstraints: constraints, Timestamps: Timestamps{ Created: s.Created, Changed: s.Changed, @@ -129,3 +130,15 @@ func NewSizeResponse(s *metal.Size) *SizeResponse { Labels: labels, } } + +func NewSizeReservationResponse(rv *metal.SizeReservation) *SizeReservationResponse { + return &SizeReservationResponse{ + Common: Common{Identifiable: Identifiable{ID: rv.ID}, Describable: Describable{Name: &rv.Name, Description: &rv.Description}}, + Timestamps: Timestamps{Created: rv.Created, Changed: rv.Changed}, + SizeID: rv.SizeID, + PartitionIDs: rv.PartitionIDs, + ProjectID: rv.ProjectID, + Amount: rv.Amount, + Labels: rv.Labels, + } +} diff --git a/cmd/metal-api/internal/testdata/testdata.go b/cmd/metal-api/internal/testdata/testdata.go index f59c7140..0f8a5b9a 100644 --- a/cmd/metal-api/internal/testdata/testdata.go +++ b/cmd/metal-api/internal/testdata/testdata.go @@ -191,13 +191,6 @@ var ( Max: 1000000000000, }, }, - Reservations: metal.Reservations{ - { - Amount: 3, - PartitionIDs: []string{Partition1.ID}, - ProjectID: "p1", - }, - }, } Sz2 = metal.Size{ Base: metal.Base{ @@ -874,6 +867,9 @@ func InitMockDBData(mock *r.Mock) { }, nil) mock.On(r.DB("mockdb").Table("integerpool").Get(r.MockAnything()).Delete(r.DeleteOpts{ReturnChanges: true})).Return(r.WriteResponse{Changes: []r.ChangeResponse{r.ChangeResponse{OldValue: map[string]interface{}{"id": float64(12345)}}}}, nil) + // Find + mock.On(r.DB("mockdb").Table("sizereservation").Filter(r.MockAnything())).Return(metal.SizeReservations{}, nil) + // Default: Return Empty result mock.On(r.DB("mockdb").Table("size").Get(r.MockAnything())).Return(EmptyResult, nil) mock.On(r.DB("mockdb").Table("partition").Get(r.MockAnything())).Return(EmptyResult, nil) diff --git a/spec/metal-api.json b/spec/metal-api.json index a8625172..b929e738 100644 --- a/spec/metal-api.json +++ b/spec/metal-api.json @@ -4541,13 +4541,6 @@ "name": { "description": "a readable name for this entity", "type": "string" - }, - "reservations": { - "description": "reservations for this size, which are considered during machine allocation", - "items": { - "$ref": "#/definitions/v1.SizeReservation" - }, - "type": "array" } }, "required": [ @@ -4653,15 +4646,19 @@ "id" ] }, - "v1.SizeReservation": { + "v1.SizeReservationCreateRequest": { "properties": { "amount": { - "description": "the amount of reserved machine allocations for this size", + "description": "the amount of reservations of this size reservation", "format": "int32", "type": "integer" }, "description": { - "description": "a description for this reservation", + "description": "a description for this entity", + "type": "string" + }, + "id": { + "description": "the unique ID of this entity", "type": "string" }, "labels": { @@ -4671,26 +4668,40 @@ "description": "free labels associated with this size reservation.", "type": "object" }, + "name": { + "description": "a readable name for this entity", + "type": "string" + }, "partitionids": { - "description": "the partitions in which this size reservation is considered, the amount is valid for every partition", + "description": "the partition id of this size reservation", "items": { "type": "string" }, "type": "array" }, "projectid": { - "description": "the project for which this size reservation is considered", + "description": "the project id of this size reservation", + "type": "string" + }, + "sizeid": { + "description": "the size id of this size reservation", "type": "string" } }, "required": [ "amount", + "id", "partitionids", - "projectid" + "projectid", + "sizeid" ] }, "v1.SizeReservationListRequest": { "properties": { + "id": { + "description": "the id of this size reservation", + "type": "string" + }, "partitionid": { "description": "the partition id of this size reservation", "type": "string" @@ -4702,15 +4713,36 @@ "sizeid": { "description": "the size id of this size reservation", "type": "string" - }, - "tenant": { - "description": "the tenant of this size reservation", - "type": "string" } } }, "v1.SizeReservationResponse": { "properties": { + "amount": { + "description": "the amount of reservations of this size reservation", + "format": "int32", + "type": "integer" + }, + "changed": { + "description": "the last changed timestamp of this entity", + "format": "date-time", + "readOnly": true, + "type": "string" + }, + "created": { + "description": "the creation time of this entity", + "format": "date-time", + "readOnly": true, + "type": "string" + }, + "description": { + "description": "a description for this entity", + "type": "string" + }, + "id": { + "description": "the unique ID of this entity", + "type": "string" + }, "labels": { "additionalProperties": { "type": "string" @@ -4718,51 +4750,131 @@ "description": "free labels associated with this size reservation.", "type": "object" }, - "partitionid": { + "name": { + "description": "a readable name for this entity", + "type": "string" + }, + "partitionids": { "description": "the partition id of this size reservation", + "items": { + "type": "string" + }, + "type": "array" + }, + "projectid": { + "description": "the project id of this size reservation", "type": "string" }, - "projectallocations": { - "description": "the amount of allocations of this project referenced by this size reservation", + "sizeid": { + "description": "the size id of this size reservation", + "type": "string" + } + }, + "required": [ + "amount", + "id", + "partitionids", + "projectid", + "sizeid" + ] + }, + "v1.SizeReservationUpdateRequest": { + "properties": { + "amount": { + "description": "the amount of reservations of this size reservation", "format": "int32", "type": "integer" }, - "projectid": { - "description": "the project id of this size reservation", + "description": { + "description": "a description for this entity", "type": "string" }, - "projectname": { - "description": "the project name of this size reservation", + "id": { + "description": "the unique ID of this entity", "type": "string" }, - "reservations": { + "labels": { + "additionalProperties": { + "type": "string" + }, + "description": "free labels associated with this size reservation.", + "type": "object" + }, + "name": { + "description": "a readable name for this entity", + "type": "string" + }, + "partitionids": { + "description": "the partition id of this size reservation", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "amount", + "id", + "partitionids" + ] + }, + "v1.SizeReservationUsageResponse": { + "properties": { + "amount": { "description": "the amount of reservations of this size reservation", "format": "int32", "type": "integer" }, - "sizeid": { - "description": "the size id of this size reservation", + "description": { + "description": "a description for this entity", "type": "string" }, - "tenant": { - "description": "the tenant of this size reservation", + "id": { + "description": "the unique ID of this entity", "type": "string" }, - "usedreservations": { + "labels": { + "additionalProperties": { + "type": "string" + }, + "description": "free labels associated with this size reservation.", + "type": "object" + }, + "name": { + "description": "a readable name for this entity", + "type": "string" + }, + "partitionid": { + "description": "the partition id of this size reservation", + "type": "string" + }, + "projectallocations": { + "description": "the amount of allocations of this project referenced by this size reservation", + "format": "int32", + "type": "integer" + }, + "projectid": { + "description": "the project id of this size reservation", + "type": "string" + }, + "sizeid": { + "description": "the size id of this size reservation", + "type": "string" + }, + "usedamount": { "description": "the used amount of reservations of this size reservation", "format": "int32", "type": "integer" } }, "required": [ + "amount", + "id", "partitionid", "projectallocations", "projectid", - "projectname", - "reservations", "sizeid", - "tenant", - "usedreservations" + "usedamount" ] }, "v1.SizeResponse": { @@ -4804,13 +4916,6 @@ "name": { "description": "a readable name for this entity", "type": "string" - }, - "reservations": { - "description": "reservations for this size, which are considered during machine allocation", - "items": { - "$ref": "#/definitions/v1.SizeReservation" - }, - "type": "array" } }, "required": [ @@ -4857,13 +4962,6 @@ "name": { "description": "a readable name for this entity", "type": "string" - }, - "reservations": { - "description": "reservations for this size, which are considered during machine allocation", - "items": { - "$ref": "#/definitions/v1.SizeReservation" - }, - "type": "array" } }, "required": [ @@ -9084,11 +9182,129 @@ } }, "/v1/size/reservations": { - "post": { + "get": { "consumes": [ "application/json" ], "operationId": "listSizeReservations", + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/v1.SizeReservationResponse" + }, + "type": "array" + } + }, + "default": { + "description": "Error", + "schema": { + "$ref": "#/definitions/httperrors.HTTPErrorResponse" + } + } + }, + "summary": "get all size reservations", + "tags": [ + "size" + ] + }, + "post": { + "consumes": [ + "application/json" + ], + "operationId": "updateSizeReservation", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.SizeReservationUpdateRequest" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.SizeReservationResponse" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/httperrors.HTTPErrorResponse" + } + }, + "default": { + "description": "Error", + "schema": { + "$ref": "#/definitions/httperrors.HTTPErrorResponse" + } + } + }, + "summary": "updates a size reservation. if the size reservation was changed since this one was read, a conflict is returned", + "tags": [ + "size" + ] + }, + "put": { + "consumes": [ + "application/json" + ], + "operationId": "createSizeReservation", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.SizeReservationCreateRequest" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/v1.SizeReservationResponse" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/httperrors.HTTPErrorResponse" + } + }, + "default": { + "description": "Error", + "schema": { + "$ref": "#/definitions/httperrors.HTTPErrorResponse" + } + } + }, + "summary": "create a size reservation. if the given ID already exists a conflict is returned", + "tags": [ + "size" + ] + } + }, + "/v1/size/reservations/find": { + "post": { + "consumes": [ + "application/json" + ], + "operationId": "findSizeReservations", "parameters": [ { "in": "body", @@ -9125,6 +9341,122 @@ ] } }, + "/v1/size/reservations/usage": { + "post": { + "consumes": [ + "application/json" + ], + "operationId": "sizeReservationsUsage", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.SizeReservationListRequest" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/v1.SizeReservationUsageResponse" + }, + "type": "array" + } + }, + "default": { + "description": "Error", + "schema": { + "$ref": "#/definitions/httperrors.HTTPErrorResponse" + } + } + }, + "summary": "get all size reservations", + "tags": [ + "size" + ] + } + }, + "/v1/size/reservations/{id}": { + "delete": { + "consumes": [ + "application/json" + ], + "operationId": "deleteSizeReservation", + "parameters": [ + { + "description": "identifier of the size reservation", + "in": "path", + "name": "id", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.SizeReservationResponse" + } + }, + "default": { + "description": "Error", + "schema": { + "$ref": "#/definitions/httperrors.HTTPErrorResponse" + } + } + }, + "summary": "deletes a size reservation and returns the deleted entity", + "tags": [ + "size" + ] + }, + "get": { + "consumes": [ + "application/json" + ], + "operationId": "getSizeReservation", + "parameters": [ + { + "description": "identifier of the size reservation", + "in": "path", + "name": "id", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.SizeReservationResponse" + } + }, + "default": { + "description": "Error", + "schema": { + "$ref": "#/definitions/httperrors.HTTPErrorResponse" + } + } + }, + "summary": "get size reservation by id", + "tags": [ + "size" + ] + } + }, "/v1/size/suggest": { "post": { "consumes": [