From 60893ddeb7ad901f85b8102e73c5ba5530c6b30c Mon Sep 17 00:00:00 2001 From: Eslam-Nawara Date: Mon, 4 Dec 2023 17:25:15 +0200 Subject: [PATCH 1/7] add voucher duration to the voucher model and set it on voucher creation --- server/app/user_handler.go | 23 ++++++++++++++--------- server/app/voucher_handler.go | 21 ++++++++++++++------- server/internal/config_parser.go | 1 + server/models/voucher.go | 23 ++++++++++++----------- 4 files changed, 41 insertions(+), 27 deletions(-) diff --git a/server/app/user_handler.go b/server/app/user_handler.go index 18efa7d8..7081ba0e 100644 --- a/server/app/user_handler.go +++ b/server/app/user_handler.go @@ -66,9 +66,10 @@ type EmailInput struct { // ApplyForVoucherInput struct for user to apply for voucher type ApplyForVoucherInput struct { - VMs int `json:"vms" binding:"required" validate:"min=0"` - PublicIPs int `json:"public_ips" binding:"required" validate:"min=0"` - Reason string `json:"reason" binding:"required" validate:"nonzero"` + VMs int `json:"vms" binding:"required" validate:"min=0"` + PublicIPs int `json:"public_ips" binding:"required" validate:"min=0"` + Reason string `json:"reason" binding:"required" validate:"nonzero"` + VoucherDuration int `json:"voucher_duration" binding:"required"` } // AddVoucherInput struct for voucher applied by user @@ -80,7 +81,6 @@ type AddVoucherInput struct { func (a *App) SignUpHandler(req *http.Request) (interface{}, Response) { var signUp SignUpInput err := json.NewDecoder(req.Body).Decode(&signUp) - if err != nil { log.Error().Err(err).Send() return nil, BadRequest(errors.New("failed to read sign up data")) @@ -573,14 +573,19 @@ func (a *App) ApplyForVoucherHandler(req *http.Request) (interface{}, Response) return nil, BadRequest(errors.New("invalid voucher data")) } + if input.VoucherDuration > a.config.VouchersMaxDuration { + return nil, BadRequest(fmt.Errorf("invalid voucher duration, max duration is %d", a.config.VouchersMaxDuration)) + } + // generate voucher for user but can't use it until admin approves it v := internal.GenerateRandomVoucher(5) voucher := models.Voucher{ - Voucher: v, - UserID: userID, - VMs: input.VMs, - Reason: input.Reason, - PublicIPs: input.PublicIPs, + Voucher: v, + UserID: userID, + VMs: input.VMs, + Reason: input.Reason, + PublicIPs: input.PublicIPs, + VoucherDuration: input.VoucherDuration, } err = a.db.CreateVoucher(&voucher) diff --git a/server/app/voucher_handler.go b/server/app/voucher_handler.go index 90a24aca..a46a62e8 100644 --- a/server/app/voucher_handler.go +++ b/server/app/voucher_handler.go @@ -4,6 +4,7 @@ package app import ( "encoding/json" "errors" + "fmt" "net/http" "strconv" @@ -17,9 +18,10 @@ import ( // GenerateVoucherInput struct for data needed when user generate vouchers type GenerateVoucherInput struct { - Length int `json:"length" binding:"required" validate:"min=3,max=20"` - VMs int `json:"vms" binding:"required"` - PublicIPs int `json:"public_ips" binding:"required"` + Length int `json:"length" binding:"required" validate:"min=3,max=20"` + VMs int `json:"vms" binding:"required"` + PublicIPs int `json:"public_ips" binding:"required"` + VoucherDuration int `json:"voucher_duration" binding:"required"` } // UpdateVoucherInput struct for data needed when user update voucher @@ -43,11 +45,16 @@ func (a *App) GenerateVoucherHandler(req *http.Request) (interface{}, Response) } voucher := internal.GenerateRandomVoucher(input.Length) + if input.VoucherDuration > a.config.VouchersMaxDuration { + return nil, BadRequest(fmt.Errorf("invalid voucher duration, max duration is %d", a.config.VouchersMaxDuration)) + } + v := models.Voucher{ - Voucher: voucher, - VMs: input.VMs, - PublicIPs: input.PublicIPs, - Approved: true, + Voucher: voucher, + VMs: input.VMs, + PublicIPs: input.PublicIPs, + VoucherDuration: input.VoucherDuration, + Approved: true, } err = a.db.CreateVoucher(&v) diff --git a/server/internal/config_parser.go b/server/internal/config_parser.go index 678de081..9dc606b7 100644 --- a/server/internal/config_parser.go +++ b/server/internal/config_parser.go @@ -21,6 +21,7 @@ type Configuration struct { NotifyAdminsIntervalHours int `json:"notifyAdminsIntervalHours"` AdminSSHKey string `json:"adminSSHKey"` BalanceThreshold int `json:"balanceThreshold"` + VouchersMaxDuration int `json:"voucherMaxDuration"` } // Server struct to hold server's information diff --git a/server/models/voucher.go b/server/models/voucher.go index 8c3c8f56..e3b3208c 100644 --- a/server/models/voucher.go +++ b/server/models/voucher.go @@ -5,15 +5,16 @@ import "time" // Voucher struct holds data of vouchers type Voucher struct { - ID int `json:"id" gorm:"primaryKey"` - UserID string `json:"user_id" binding:"required"` - Voucher string `json:"voucher" gorm:"unique"` - VMs int `json:"vms" binding:"required"` - PublicIPs int `json:"public_ips" binding:"required"` - Reason string `json:"reason" binding:"required"` - Used bool `json:"used" binding:"required"` - Approved bool `json:"approved" binding:"required"` - Rejected bool `json:"rejected" binding:"required"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID int `json:"id" gorm:"primaryKey"` + UserID string `json:"user_id" binding:"required"` + Voucher string `json:"voucher" gorm:"unique"` + VMs int `json:"vms" binding:"required"` + PublicIPs int `json:"public_ips" binding:"required"` + Reason string `json:"reason" binding:"required"` + Used bool `json:"used" binding:"required"` + Approved bool `json:"approved" binding:"required"` + Rejected bool `json:"rejected" binding:"required"` + VoucherDuration int `json:"voucher_duration" binding:"required"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } From 36c1bb88ed3e67444b0918db06c511f00c2bc890 Mon Sep 17 00:00:00 2001 From: Eslam-Nawara Date: Thu, 7 Dec 2023 10:16:20 +0200 Subject: [PATCH 2/7] set vms as map in quota --- server/app/quota_handler_test.go | 3 ++- server/app/user_handler.go | 12 ++++++++---- server/models/database.go | 2 +- server/models/database_test.go | 28 +++++++++++++--------------- server/models/quota.go | 8 +++++--- 5 files changed, 29 insertions(+), 24 deletions(-) diff --git a/server/app/quota_handler_test.go b/server/app/quota_handler_test.go index 97838e35..b1a22348 100644 --- a/server/app/quota_handler_test.go +++ b/server/app/quota_handler_test.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "testing" + "time" "github.com/codescalers/cloud4students/internal" "github.com/codescalers/cloud4students/models" @@ -44,7 +45,7 @@ func TestQuotaRouter(t *testing.T) { err = app.db.CreateQuota( &models.Quota{ UserID: user.ID.String(), - Vms: 10, + Vms: map[time.Time]int{time.Now().Add(time.Hour): 10}, PublicIPs: 1, }, ) diff --git a/server/app/user_handler.go b/server/app/user_handler.go index 7081ba0e..218b78b9 100644 --- a/server/app/user_handler.go +++ b/server/app/user_handler.go @@ -74,7 +74,8 @@ type ApplyForVoucherInput struct { // AddVoucherInput struct for voucher applied by user type AddVoucherInput struct { - Voucher string `json:"voucher" binding:"required"` + Voucher string `json:"voucher" binding:"required"` + RequestedDuration int `json:"requestedDuration" binding:"required"` } // SignUpHandler creates account for user @@ -163,7 +164,7 @@ func (a *App) SignUpHandler(req *http.Request) (interface{}, Response) { // create empty quota quota := models.Quota{ UserID: u.ID.String(), - Vms: 0, + Vms: make(map[time.Time]int), } err = a.db.CreateQuota("a) if err != nil { @@ -612,7 +613,7 @@ func (a *App) ActivateVoucherHandler(req *http.Request) (interface{}, Response) return nil, BadRequest(errors.New("failed to read voucher data")) } - oldQuota, err := a.db.GetUserQuota(userID) + userQuota, err := a.db.GetUserQuota(userID) if err == gorm.ErrRecordNotFound { return nil, NotFound(errors.New("user quota is not found")) } @@ -648,7 +649,10 @@ func (a *App) ActivateVoucherHandler(req *http.Request) (interface{}, Response) return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - err = a.db.UpdateUserQuota(userID, oldQuota.Vms+voucherQuota.VMs, oldQuota.PublicIPs+voucherQuota.PublicIPs) + expirationDate := time.Now().Add(time.Duration(voucherQuota.VoucherDuration) * 30 * 24 * time.Hour) + userQuota.Vms[expirationDate] += voucherQuota.VMs + + err = a.db.UpdateUserQuota(userID, userQuota.Vms, userQuota.PublicIPs+voucherQuota.PublicIPs) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) diff --git a/server/models/database.go b/server/models/database.go index 7b11f7f2..2e133c95 100644 --- a/server/models/database.go +++ b/server/models/database.go @@ -217,7 +217,7 @@ func (d *DB) CreateQuota(q *Quota) error { } // UpdateUserQuota updates quota -func (d *DB) UpdateUserQuota(userID string, vms int, publicIPs int) error { +func (d *DB) UpdateUserQuota(userID string, vms map[time.Time]int, publicIPs int) error { return d.db.Model(&Quota{}).Where("user_id = ?", userID).Updates(map[string]interface{}{"vms": vms, "public_ips": publicIPs}).Error } diff --git a/server/models/database_test.go b/server/models/database_test.go index c831abc0..05f80b05 100644 --- a/server/models/database_test.go +++ b/server/models/database_test.go @@ -3,6 +3,7 @@ package models import ( "testing" + "time" "github.com/stretchr/testify/assert" "gorm.io/gorm" @@ -116,7 +117,6 @@ func TestListAllUsers(t *testing.T) { assert.Equal(t, users[0].Name, user1.Name) assert.Equal(t, users[0].Email, user1.Email) assert.Equal(t, users[0].HashedPassword, user1.HashedPassword) - }) } @@ -142,7 +142,6 @@ func TestGetCodeByEmail(t *testing.T) { assert.NoError(t, err) assert.Equal(t, code, user.Code) }) - } func TestUpdatePassword(t *testing.T) { @@ -232,6 +231,7 @@ func TestUpdateVerification(t *testing.T) { assert.Equal(t, u.Verified, true) }) } + func TestAddUserVoucher(t *testing.T) { db := setupDB(t) t.Run("user and voucher not found so nothing updated", func(t *testing.T) { @@ -347,6 +347,7 @@ func TestGetVMByID(t *testing.T) { assert.NoError(t, err) }) } + func TestGetAllVMs(t *testing.T) { db := setupDB(t) t.Run("no vms with user", func(t *testing.T) { @@ -374,7 +375,6 @@ func TestGetAllVMs(t *testing.T) { assert.Equal(t, vms, []VM{vm3}) assert.NoError(t, err) }) - } func TestAvailableVMName(t *testing.T) { @@ -393,7 +393,6 @@ func TestAvailableVMName(t *testing.T) { valid, err := db.AvailableVMName("vm1") assert.NoError(t, err) assert.Equal(t, false, valid) - }) t.Run("test with new name", func(t *testing.T) { @@ -404,10 +403,9 @@ func TestAvailableVMName(t *testing.T) { valid, err := db.AvailableVMName("vm") assert.NoError(t, err) assert.Equal(t, true, valid) - }) - } + func TestDeleteVMByID(t *testing.T) { db := setupDB(t) t.Run("delete non existing vm", func(t *testing.T) { @@ -484,7 +482,7 @@ func TestCreateQuota(t *testing.T) { func TestUpdateUserQuota(t *testing.T) { db := setupDB(t) t.Run("quota not found so no updates", func(t *testing.T) { - err := db.UpdateUserQuota("user", 5, 0) + err := db.UpdateUserQuota("user", map[time.Time]int{time.Now(): 5}, 0) assert.NoError(t, err) }) t.Run("quota found", func(t *testing.T) { @@ -496,7 +494,7 @@ func TestUpdateUserQuota(t *testing.T) { err = db.CreateQuota("a2) assert.NoError(t, err) - err = db.UpdateUserQuota("user", 5, 10) + err = db.UpdateUserQuota("user", map[time.Time]int{time.Now().Add(time.Hour): 5}, 10) assert.NoError(t, err) var q Quota @@ -507,17 +505,17 @@ func TestUpdateUserQuota(t *testing.T) { err = db.db.First(&q, "user_id = 'new-user'").Error assert.NoError(t, err) assert.Equal(t, q.Vms, 0) - }) t.Run("quota found with zero values", func(t *testing.T) { quota := Quota{UserID: "1"} err := db.CreateQuota("a) assert.NoError(t, err) - err = db.UpdateUserQuota("1", 0, 0) + err = db.UpdateUserQuota("1", map[time.Time]int{time.Now(): 0}, 0) assert.NoError(t, err) }) } + func TestGetUserQuota(t *testing.T) { db := setupDB(t) t.Run("quota not found", func(t *testing.T) { @@ -570,6 +568,7 @@ func TestGetVoucher(t *testing.T) { assert.NoError(t, err) }) } + func TestGetVoucherByID(t *testing.T) { db := setupDB(t) t.Run("voucher not found", func(t *testing.T) { @@ -693,6 +692,7 @@ func TestCreateK8s(t *testing.T) { assert.Equal(t, w[1].Name, "worker2") assert.Equal(t, w[1].ClusterID, 1) } + func TestGetK8s(t *testing.T) { db := setupDB(t) t.Run("K8s not found", func(t *testing.T) { @@ -726,6 +726,7 @@ func TestGetK8s(t *testing.T) { assert.NotEqual(t, k, k8s2) }) } + func TestGetAllK8s(t *testing.T) { db := setupDB(t) t.Run("K8s not found", func(t *testing.T) { @@ -771,8 +772,8 @@ func TestGetAllK8s(t *testing.T) { assert.NoError(t, err) assert.Equal(t, k, []K8sCluster{k8s3}) }) - } + func TestDeleteK8s(t *testing.T) { db := setupDB(t) t.Run("K8s not found", func(t *testing.T) { @@ -813,6 +814,7 @@ func TestDeleteK8s(t *testing.T) { assert.Equal(t, k, k8s2) }) } + func TestDeleteAllK8s(t *testing.T) { db := setupDB(t) t.Run("K8s not found", func(t *testing.T) { @@ -891,7 +893,6 @@ func TestAvailableK8sName(t *testing.T) { valid, err := db.AvailableK8sName("master") assert.NoError(t, err) assert.Equal(t, false, valid) - }) t.Run("test with new name", func(t *testing.T) { @@ -908,16 +909,13 @@ func TestAvailableK8sName(t *testing.T) { valid, err := db.AvailableK8sName("new-master") assert.NoError(t, err) assert.Equal(t, true, valid) - }) - } func TestUpdateMaintenance(t *testing.T) { db := setupDB(t) err := db.UpdateMaintenance(true) assert.NoError(t, err) - } func TestGetMaintenance(t *testing.T) { diff --git a/server/models/quota.go b/server/models/quota.go index 35a1c934..3769cd9f 100644 --- a/server/models/quota.go +++ b/server/models/quota.go @@ -1,9 +1,11 @@ // Package models for database models package models +import "time" + // Quota struct holds available vms for each user type Quota struct { - UserID string `json:"user_id"` - Vms int `json:"vms"` - PublicIPs int `json:"public_ips"` + UserID string `json:"user_id"` + Vms map[time.Time]int `json:"vms"` + PublicIPs int `json:"public_ips"` } From 089c83736c13e63d4c872f10d2b2f3cc6a22acaa Mon Sep 17 00:00:00 2001 From: Eslam-Nawara Date: Thu, 7 Dec 2023 16:46:12 +0200 Subject: [PATCH 3/7] create quotaVMs model to store all vms with their expiration time --- server/app/quota_handler.go | 18 +++++++++++++- server/app/user_handler.go | 24 +++++++++++++++---- server/app/vm_handler.go | 11 ++++++++- server/deployer/vms_deployer.go | 42 ++++++++++++++++++++++++++------- server/models/api_inputs.go | 2 ++ server/models/database.go | 35 +++++++++++++++++++++++---- server/models/quota.go | 8 +++---- server/models/quota_vms.go | 11 +++++++++ server/models/vm.go | 27 +++++++++++---------- 9 files changed, 142 insertions(+), 36 deletions(-) create mode 100644 server/models/quota_vms.go diff --git a/server/app/quota_handler.go b/server/app/quota_handler.go index 193de4e1..1e497fc0 100644 --- a/server/app/quota_handler.go +++ b/server/app/quota_handler.go @@ -6,10 +6,17 @@ import ( "net/http" "github.com/codescalers/cloud4students/middlewares" + "github.com/codescalers/cloud4students/models" "github.com/rs/zerolog/log" "gorm.io/gorm" ) +// QuotaData represents the structure for Quota and QuotaVMs data. +type QuotaData struct { + Quota models.Quota + QuotaVMs []models.QuotaVM +} + // GetQuotaHandler gets quota func (a *App) GetQuotaHandler(req *http.Request) (interface{}, Response) { userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) @@ -23,8 +30,17 @@ func (a *App) GetQuotaHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + quotaVMs, err := a.db.ListUserQuotaVMs(quota.ID.String()) + if err == gorm.ErrRecordNotFound { + return nil, NotFound(errors.New("user quota vms is not found")) + } + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "Quota is found", - Data: quota, + Data: QuotaData{Quota: quota, QuotaVMs: quotaVMs}, }, Ok() } diff --git a/server/app/user_handler.go b/server/app/user_handler.go index 218b78b9..f50464af 100644 --- a/server/app/user_handler.go +++ b/server/app/user_handler.go @@ -164,7 +164,6 @@ func (a *App) SignUpHandler(req *http.Request) (interface{}, Response) { // create empty quota quota := models.Quota{ UserID: u.ID.String(), - Vms: make(map[time.Time]int), } err = a.db.CreateQuota("a) if err != nil { @@ -613,7 +612,7 @@ func (a *App) ActivateVoucherHandler(req *http.Request) (interface{}, Response) return nil, BadRequest(errors.New("failed to read voucher data")) } - userQuota, err := a.db.GetUserQuota(userID) + quota, err := a.db.GetUserQuota(userID) if err == gorm.ErrRecordNotFound { return nil, NotFound(errors.New("user quota is not found")) } @@ -631,6 +630,17 @@ func (a *App) ActivateVoucherHandler(req *http.Request) (interface{}, Response) return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + expirationDate := time.Now().Add(time.Duration(voucherQuota.VoucherDuration) * 30 * 24 * time.Hour) + + userQuotaVMs, err := a.db.GetUserQuotaVMs(quota.ID.String(), expirationDate) + if err == gorm.ErrRecordNotFound { + return nil, NotFound(errors.New("user quota vms is not found")) + } + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + if voucherQuota.Rejected { return nil, BadRequest(errors.New("voucher is rejected")) } @@ -649,14 +659,18 @@ func (a *App) ActivateVoucherHandler(req *http.Request) (interface{}, Response) return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - expirationDate := time.Now().Add(time.Duration(voucherQuota.VoucherDuration) * 30 * 24 * time.Hour) - userQuota.Vms[expirationDate] += voucherQuota.VMs + err = a.db.UpdateUserQuota(userID, quota.PublicIPs+voucherQuota.PublicIPs) + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } - err = a.db.UpdateUserQuota(userID, userQuota.Vms, userQuota.PublicIPs+voucherQuota.PublicIPs) + err = a.db.UpdateUserQuotaVMs(quota.ID.String(), expirationDate, userQuotaVMs.Vms+voucherQuota.VMs) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + middlewares.VoucherActivated.WithLabelValues(userID, voucherQuota.Voucher, fmt.Sprint(voucherQuota.VMs), fmt.Sprint(voucherQuota.PublicIPs)).Inc() return ResponseMsg{ diff --git a/server/app/vm_handler.go b/server/app/vm_handler.go index 17bfface..b18af815 100644 --- a/server/app/vm_handler.go +++ b/server/app/vm_handler.go @@ -54,7 +54,16 @@ func (a *App) DeployVMHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - _, err = deployer.ValidateVMQuota(input, quota.Vms, quota.PublicIPs) + quotaVMs, err := a.db.ListUserQuotaVMs(quota.ID.String()) + if err == gorm.ErrRecordNotFound { + return nil, NotFound(errors.New("user quota vms is not found")) + } + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + _, _, err = deployer.ValidateVMQuota(input, quotaVMs, quota.PublicIPs) if err != nil { return nil, BadRequest(errors.New(err.Error())) } diff --git a/server/deployer/vms_deployer.go b/server/deployer/vms_deployer.go index cf045b46..14aecb8c 100644 --- a/server/deployer/vms_deployer.go +++ b/server/deployer/vms_deployer.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "net/http" + "time" "github.com/codescalers/cloud4students/middlewares" "github.com/codescalers/cloud4students/models" @@ -87,20 +88,24 @@ func (d *Deployer) deployVM(ctx context.Context, vmInput models.DeployVMInput, s } // ValidateVMQuota validates the quota a vm deployment need -func ValidateVMQuota(vm models.DeployVMInput, availableResourcesQuota, availablePublicIPsQuota int) (int, error) { +func ValidateVMQuota(vm models.DeployVMInput, availableResourcesQuota []models.QuotaVM, availablePublicIPsQuota int) (time.Time, int, error) { neededQuota, err := calcNeededQuota(vm.Resources) if err != nil { - return 0, err + return time.Now(), 0, err } - if availableResourcesQuota < neededQuota { - return 0, fmt.Errorf("no available quota %d for deployment for resources %s, you can request a new voucher", availableResourcesQuota, vm.Resources) - } if vm.Public && availablePublicIPsQuota < publicQuota { - return 0, fmt.Errorf("no available quota %d for public ips", availablePublicIPsQuota) + return time.Now(), 0, fmt.Errorf("no available quota %d for public ips", availablePublicIPsQuota) } - return neededQuota, nil + requestedExpirationDate := time.Now().Add(time.Duration(vm.Duration) * 30 * 24 * time.Hour) + for _, vms := range availableResourcesQuota { + if requestedExpirationDate.Before(vms.ExpirationDate) && neededQuota <= vms.Vms { + return vms.ExpirationDate, vms.Vms - neededQuota, nil + } + } + + return time.Now(), 0, fmt.Errorf("no available quota %v for deployment for resources %s, you can request a new voucher", availableResourcesQuota, vm.Resources) } func (d *Deployer) deployVMRequest(ctx context.Context, user models.User, input models.DeployVMInput, adminSSHKey string) (int, error) { @@ -114,7 +119,16 @@ func (d *Deployer) deployVMRequest(ctx context.Context, user models.User, input return http.StatusInternalServerError, errors.New(internalServerErrorMsg) } - neededQuota, err := ValidateVMQuota(input, quota.Vms, quota.PublicIPs) + quotaVM, err := d.db.ListUserQuotaVMs(quota.ID.String()) + if err == gorm.ErrRecordNotFound { + return http.StatusNotFound, errors.New("user quota vm is not found") + } + if err != nil { + log.Error().Err(err).Send() + return http.StatusInternalServerError, errors.New(internalServerErrorMsg) + } + + expirationDate, newQuotaVMs, err := ValidateVMQuota(input, quotaVM, quota.PublicIPs) if err != nil { return http.StatusBadRequest, err } @@ -137,6 +151,7 @@ func (d *Deployer) deployVMRequest(ctx context.Context, user models.User, input MRU: uint64(vm.Memory), ContractID: contractID, NetworkContractID: networkContractID, + ExpirationDate: expirationDate, } err = d.db.CreateVM(&userVM) @@ -150,7 +165,7 @@ func (d *Deployer) deployVMRequest(ctx context.Context, user models.User, input publicIPsQuota -= publicQuota } // update quota of user - err = d.db.UpdateUserQuota(user.ID.String(), quota.Vms-neededQuota, publicIPsQuota) + err = d.db.UpdateUserQuota(user.ID.String(), publicIPsQuota) if err == gorm.ErrRecordNotFound { return http.StatusNotFound, errors.New("User quota is not found") } @@ -159,6 +174,15 @@ func (d *Deployer) deployVMRequest(ctx context.Context, user models.User, input return http.StatusInternalServerError, errors.New(internalServerErrorMsg) } + err = d.db.UpdateUserQuotaVMs(quota.ID.String(), expirationDate, newQuotaVMs) + if err == gorm.ErrRecordNotFound { + return http.StatusNotFound, errors.New("User quota vms is not found") + } + if err != nil { + log.Error().Err(err).Send() + return http.StatusInternalServerError, errors.New(internalServerErrorMsg) + } + middlewares.Deployments.WithLabelValues(user.ID.String(), input.Resources, "vm").Inc() return 0, nil } diff --git a/server/models/api_inputs.go b/server/models/api_inputs.go index dd42792b..f22fe429 100644 --- a/server/models/api_inputs.go +++ b/server/models/api_inputs.go @@ -5,6 +5,7 @@ package models type DeployVMInput struct { Name string `json:"name" binding:"required" validate:"min=3,max=20"` Resources string `json:"resources" binding:"required"` + Duration int `json:"duration" binding:"required"` Public bool `json:"public"` } @@ -14,6 +15,7 @@ type K8sDeployInput struct { Resources string `json:"resources"` Public bool `json:"public"` Workers []Worker `json:"workers"` + Duration int `json:"duration" binding:"required"` } // WorkerInput deploy k8s worker input diff --git a/server/models/database.go b/server/models/database.go index 2e133c95..87add831 100644 --- a/server/models/database.go +++ b/server/models/database.go @@ -162,7 +162,6 @@ func (d *DB) GetNotUsedVoucherByUserID(id string) (Voucher, error) { func (d *DB) CreateVM(vm *VM) error { result := d.db.Create(&vm) return result.Error - } // GetVMByID return vm by its id @@ -216,18 +215,46 @@ func (d *DB) CreateQuota(q *Quota) error { return result.Error } +// CreateQuota creates a new quota vm +func (d *DB) CreateQuotaVM(q *QuotaVM) error { + result := d.db.Create(&q) + return result.Error +} + // UpdateUserQuota updates quota -func (d *DB) UpdateUserQuota(userID string, vms map[time.Time]int, publicIPs int) error { - return d.db.Model(&Quota{}).Where("user_id = ?", userID).Updates(map[string]interface{}{"vms": vms, "public_ips": publicIPs}).Error +func (d *DB) UpdateUserQuota(userID string, publicIPs int) error { + return d.db.Model(&Quota{}).Where("user_id = ?", userID).Update("public_ips", publicIPs).Error +} + +// UpdateUserQuotaVMs updates quota vms +func (d *DB) UpdateUserQuotaVMs(QuotaID string, expirationDate time.Time, vms int) error { + return d.db.Model(&QuotaVM{}). + Where(&QuotaVM{QoutaID: QuotaID, ExpirationDate: expirationDate}). + Update("vms", vms).Error } -// GetUserQuota gets user quota available vms (vms will be used for both vms and k8s clusters) +// GetUserQuota gets user quota available publicIPs func (d *DB) GetUserQuota(userID string) (Quota, error) { var res Quota query := d.db.First(&res, "user_id = ?", userID) return res, query.Error } +// GetUserQuotaVMs gets user quota available vms (vms will be used for both vms and k8s clusters) +func (d *DB) GetUserQuotaVMs(quotaID string, expirationDate time.Time) (QuotaVM, error) { + var res QuotaVM + query := d.db.Select("expiration_date", "vms"). + FirstOrCreate(&res, &QuotaVM{QoutaID: quotaID, ExpirationDate: expirationDate}) + return res, query.Error +} + +// ListUserQuotaVMs gets user quota available vms (vms will be used for both vms and k8s clusters) +func (d *DB) ListUserQuotaVMs(quotaID string) ([]QuotaVM, error) { + var res []QuotaVM + query := d.db.Find(&res, "quota_id = ?", quotaID) + return res, query.Error +} + // CreateVoucher creates a new voucher func (d *DB) CreateVoucher(v *Voucher) error { result := d.db.Create(&v) diff --git a/server/models/quota.go b/server/models/quota.go index 3769cd9f..1f5f3e6a 100644 --- a/server/models/quota.go +++ b/server/models/quota.go @@ -1,11 +1,11 @@ // Package models for database models package models -import "time" +import "github.com/google/uuid" // Quota struct holds available vms for each user type Quota struct { - UserID string `json:"user_id"` - Vms map[time.Time]int `json:"vms"` - PublicIPs int `json:"public_ips"` + ID uuid.UUID `gorm:"primary_key; unique; type:uuid; column:id"` + UserID string `json:"user_id"` + PublicIPs int `json:"public_ips"` } diff --git a/server/models/quota_vms.go b/server/models/quota_vms.go new file mode 100644 index 00000000..5470b27f --- /dev/null +++ b/server/models/quota_vms.go @@ -0,0 +1,11 @@ +// Package models for database models +package models + +import "time" + +// QuotaVM struct holds available vms and their expiration date for each user +type QuotaVM struct { + QoutaID string `json:"qouta_id"` + Vms int `json:"vms"` + ExpirationDate time.Time `json:"expiration_date"` +} diff --git a/server/models/vm.go b/server/models/vm.go index 762e7bda..a459b1d7 100644 --- a/server/models/vm.go +++ b/server/models/vm.go @@ -1,20 +1,23 @@ // Package models for database models package models +import "time" + // VM struct for vms data type VM struct { - ID int `json:"id" gorm:"primaryKey"` - UserID string `json:"user_id"` - Name string `json:"name" gorm:"unique" binding:"required"` - YggIP string `json:"ygg_ip"` - Public bool `json:"public"` - PublicIP string `json:"public_ip"` - Resources string `json:"resources"` - SRU uint64 `json:"sru"` - CRU uint64 `json:"cru"` - MRU uint64 `json:"mru"` - ContractID uint64 `json:"contractID"` - NetworkContractID uint64 `json:"networkContractID"` + ID int `json:"id" gorm:"primaryKey"` + UserID string `json:"user_id"` + Name string `json:"name" gorm:"unique" binding:"required"` + YggIP string `json:"ygg_ip"` + Public bool `json:"public"` + PublicIP string `json:"public_ip"` + Resources string `json:"resources"` + SRU uint64 `json:"sru"` + CRU uint64 `json:"cru"` + MRU uint64 `json:"mru"` + ContractID uint64 `json:"contractID"` + NetworkContractID uint64 `json:"networkContractID"` + ExpirationDate time.Time `json:"expirationDate" binding:"required"` } // DeploymentsCount has the vms and ips reserved in the grid From 80da291e0c08eb20fa29b4e96dc40a73e7c7c0fc Mon Sep 17 00:00:00 2001 From: Eslam-Nawara Date: Mon, 11 Dec 2023 10:08:56 +0200 Subject: [PATCH 4/7] add expiration date to vms and k8s --- server/app/app.go | 2 ++ server/app/k8s_handler.go | 11 ++++++- server/app/quota_handler_test.go | 2 -- server/app/setup.go | 25 ++++++++++----- server/app/user_handler.go | 19 ++++++----- server/app/voucher_handler.go | 10 +++--- server/deployer/k8s_deployer.go | 54 +++++++++++++++++++++++++------- server/deployer/vms_deployer.go | 33 ++++++++++++------- server/models/database.go | 25 +++++++++------ server/models/database_test.go | 20 ++++++++---- server/models/quota.go | 17 +++++++++- server/models/quota_vms.go | 8 ++--- server/models/voucher.go | 24 +++++++------- 13 files changed, 168 insertions(+), 82 deletions(-) diff --git a/server/app/app.go b/server/app/app.go index cc6dc3bf..1a6a310e 100644 --- a/server/app/app.go +++ b/server/app/app.go @@ -87,6 +87,8 @@ func (a *App) startBackgroundWorkers(ctx context.Context) { go a.deployer.PeriodicRequests(ctx, substrateBlockDiffInSeconds) go a.deployer.PeriodicDeploy(ctx, substrateBlockDiffInSeconds) + // remove expired vms and k8s + // check pending deployments a.deployer.ConsumeVMRequest(ctx, true) a.deployer.ConsumeK8sRequest(ctx, true) diff --git a/server/app/k8s_handler.go b/server/app/k8s_handler.go index 3ae5e66e..7d555f26 100644 --- a/server/app/k8s_handler.go +++ b/server/app/k8s_handler.go @@ -54,7 +54,16 @@ func (a *App) K8sDeployHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - _, err = deployer.ValidateK8sQuota(k8sDeployInput, quota.Vms, quota.PublicIPs) + allQuotaVMs, err := a.db.ListUserQuotaVMs(quota.ID.String()) + if err == gorm.ErrRecordNotFound { + return nil, NotFound(errors.New("user quota vms are not found")) + } + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + _, _, err = deployer.ValidateK8sQuota(k8sDeployInput, allQuotaVMs, quota.PublicIPs) if err != nil { log.Error().Err(err).Send() return nil, BadRequest(errors.New(err.Error())) diff --git a/server/app/quota_handler_test.go b/server/app/quota_handler_test.go index b1a22348..f31aea0a 100644 --- a/server/app/quota_handler_test.go +++ b/server/app/quota_handler_test.go @@ -5,7 +5,6 @@ import ( "fmt" "net/http" "testing" - "time" "github.com/codescalers/cloud4students/internal" "github.com/codescalers/cloud4students/models" @@ -45,7 +44,6 @@ func TestQuotaRouter(t *testing.T) { err = app.db.CreateQuota( &models.Quota{ UserID: user.ID.String(), - Vms: map[time.Time]int{time.Now().Add(time.Hour): 10}, PublicIPs: 1, }, ) diff --git a/server/app/setup.go b/server/app/setup.go index 0d83c327..06325454 100644 --- a/server/app/setup.go +++ b/server/app/setup.go @@ -8,7 +8,6 @@ import ( "net/http/httptest" "os" "path/filepath" - "testing" c4sDeployer "github.com/codescalers/cloud4students/deployer" @@ -73,23 +72,35 @@ func SetUp(t testing.TB) *App { `, dbPath) err := os.WriteFile(configPath, []byte(config), 0644) - assert.NoError(t, err) + if !assert.NoError(t, err) { + return &App{} + } configuration, err := internal.ReadConfFile(configPath) - assert.NoError(t, err) + if !assert.NoError(t, err) { + return &App{} + } db := models.NewDB() err = db.Connect(configuration.Database.File) - assert.NoError(t, err) + if !assert.NoError(t, err) { + return &App{} + } err = db.Migrate() - assert.NoError(t, err) + if !assert.NoError(t, err) { + return &App{} + } tfPluginClient, err := deployer.NewTFPluginClient(configuration.Account.Mnemonics, "sr25519", configuration.Account.Network, "", "", "", 0, false) - assert.NoError(t, err) + if !assert.NoError(t, err) { + return &App{} + } newDeployer, err := c4sDeployer.NewDeployer(db, streams.RedisClient{}, tfPluginClient) - assert.NoError(t, err) + if !assert.NoError(t, err) { + return &App{} + } app := &App{ config: configuration, diff --git a/server/app/user_handler.go b/server/app/user_handler.go index f50464af..fbc70250 100644 --- a/server/app/user_handler.go +++ b/server/app/user_handler.go @@ -573,6 +573,7 @@ func (a *App) ApplyForVoucherHandler(req *http.Request) (interface{}, Response) return nil, BadRequest(errors.New("invalid voucher data")) } + // make sure the requested duration is less that the maximum allowed duration if input.VoucherDuration > a.config.VouchersMaxDuration { return nil, BadRequest(fmt.Errorf("invalid voucher duration, max duration is %d", a.config.VouchersMaxDuration)) } @@ -580,12 +581,12 @@ func (a *App) ApplyForVoucherHandler(req *http.Request) (interface{}, Response) // generate voucher for user but can't use it until admin approves it v := internal.GenerateRandomVoucher(5) voucher := models.Voucher{ - Voucher: v, - UserID: userID, - VMs: input.VMs, - Reason: input.Reason, - PublicIPs: input.PublicIPs, - VoucherDuration: input.VoucherDuration, + Voucher: v, + UserID: userID, + VMs: input.VMs, + Reason: input.Reason, + PublicIPs: input.PublicIPs, + Duration: input.VoucherDuration, } err = a.db.CreateVoucher(&voucher) @@ -630,9 +631,7 @@ func (a *App) ActivateVoucherHandler(req *http.Request) (interface{}, Response) return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - expirationDate := time.Now().Add(time.Duration(voucherQuota.VoucherDuration) * 30 * 24 * time.Hour) - - userQuotaVMs, err := a.db.GetUserQuotaVMs(quota.ID.String(), expirationDate) + userQuotaVMs, err := a.db.GetUserQuotaVMs(quota.ID.String(), voucherQuota.Duration) if err == gorm.ErrRecordNotFound { return nil, NotFound(errors.New("user quota vms is not found")) } @@ -665,7 +664,7 @@ func (a *App) ActivateVoucherHandler(req *http.Request) (interface{}, Response) return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - err = a.db.UpdateUserQuotaVMs(quota.ID.String(), expirationDate, userQuotaVMs.Vms+voucherQuota.VMs) + err = a.db.UpdateUserQuotaVMs(quota.ID.String(), voucherQuota.Duration, userQuotaVMs.Vms+voucherQuota.VMs) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) diff --git a/server/app/voucher_handler.go b/server/app/voucher_handler.go index a46a62e8..11fdd2ec 100644 --- a/server/app/voucher_handler.go +++ b/server/app/voucher_handler.go @@ -50,11 +50,11 @@ func (a *App) GenerateVoucherHandler(req *http.Request) (interface{}, Response) } v := models.Voucher{ - Voucher: voucher, - VMs: input.VMs, - PublicIPs: input.PublicIPs, - VoucherDuration: input.VoucherDuration, - Approved: true, + Voucher: voucher, + VMs: input.VMs, + PublicIPs: input.PublicIPs, + Approved: true, + Duration: input.VoucherDuration, } err = a.db.CreateVoucher(&v) diff --git a/server/deployer/k8s_deployer.go b/server/deployer/k8s_deployer.go index 17f5f28e..bb059ac3 100644 --- a/server/deployer/k8s_deployer.go +++ b/server/deployer/k8s_deployer.go @@ -199,28 +199,31 @@ func (d *Deployer) getK8sAvailableNode(ctx context.Context, k models.K8sDeployIn } // ValidateK8sQuota validates the quota a k8s deployment need -func ValidateK8sQuota(k models.K8sDeployInput, availableResourcesQuota, availablePublicIPsQuota int) (int, error) { +func ValidateK8sQuota(k models.K8sDeployInput, availableResourcesQuota []models.QuotaVM, availablePublicIPsQuota int) (int, int, error) { neededQuota, err := calcNeededQuota(k.Resources) if err != nil { - return 0, err + return 0, 0, err + } + + if k.Public && availablePublicIPsQuota < publicQuota { + return 0, 0, fmt.Errorf("no available quota %d for public ips", availablePublicIPsQuota) } for _, worker := range k.Workers { workerQuota, err := calcNeededQuota(worker.Resources) if err != nil { - return 0, err + return 0, 0, err } neededQuota += workerQuota } - if availableResourcesQuota < neededQuota { - return 0, fmt.Errorf("no available quota %d for kubernetes deployment, you can request a new voucher", availableResourcesQuota) - } - if k.Public && availablePublicIPsQuota < publicQuota { - return 0, fmt.Errorf("no available quota %d for public ips", availablePublicIPsQuota) + for _, quotaVMs := range availableResourcesQuota { + if quotaVMs.Duration >= k.Duration && quotaVMs.Vms >= neededQuota { + return quotaVMs.Duration, neededQuota, nil + } } - return neededQuota, nil + return 0, 0, fmt.Errorf("no available quota %v for kubernetes deployment, you can request a new voucher", availableResourcesQuota) } func (d *Deployer) deployK8sRequest(ctx context.Context, user models.User, k8sDeployInput models.K8sDeployInput, adminSSHKey string) (int, error) { @@ -235,12 +238,30 @@ func (d *Deployer) deployK8sRequest(ctx context.Context, user models.User, k8sDe return http.StatusInternalServerError, errors.New(internalServerErrorMsg) } - neededQuota, err := ValidateK8sQuota(k8sDeployInput, quota.Vms, quota.PublicIPs) + allQuotaVMs, err := d.db.ListUserQuotaVMs(quota.ID.String()) + if err == gorm.ErrRecordNotFound { + return http.StatusNotFound, errors.New("user quota vms are not found") + } + if err != nil { + log.Error().Err(err).Send() + return http.StatusInternalServerError, errors.New(internalServerErrorMsg) + } + + neededQuotaDuration, neededQuota, err := ValidateK8sQuota(k8sDeployInput, allQuotaVMs, quota.PublicIPs) if err != nil { log.Error().Err(err).Send() return http.StatusBadRequest, err } + quotaVMs, err := d.db.GetUserQuotaVMs(quota.ID.String(), neededQuotaDuration) + if err == gorm.ErrRecordNotFound { + return http.StatusNotFound, errors.New("user quota vm is not found") + } + if err != nil { + log.Error().Err(err).Send() + return http.StatusInternalServerError, errors.New(internalServerErrorMsg) + } + // deploy network and cluster node, networkContractID, k8sContractID, err := d.deployK8sClusterWithNetwork(ctx, k8sDeployInput, user.SSHKey, adminSSHKey) if err != nil { @@ -253,12 +274,14 @@ func (d *Deployer) deployK8sRequest(ctx context.Context, user models.User, k8sDe log.Error().Err(err).Send() return http.StatusInternalServerError, errors.New(internalServerErrorMsg) } + publicIPsQuota := quota.PublicIPs if k8sDeployInput.Public { publicIPsQuota -= publicQuota } + // update quota - err = d.db.UpdateUserQuota(user.ID.String(), quota.Vms-neededQuota, publicIPsQuota) + err = d.db.UpdateUserQuota(user.ID.String(), publicIPsQuota) if err == gorm.ErrRecordNotFound { return http.StatusNotFound, errors.New("user quota is not found") } @@ -267,6 +290,15 @@ func (d *Deployer) deployK8sRequest(ctx context.Context, user models.User, k8sDe return http.StatusInternalServerError, errors.New(internalServerErrorMsg) } + err = d.db.UpdateUserQuotaVMs(quota.ID.String(), neededQuotaDuration, quotaVMs.Vms-neededQuota) + if err == gorm.ErrRecordNotFound { + return http.StatusNotFound, errors.New("User quota vms is not found") + } + if err != nil { + log.Error().Err(err).Send() + return http.StatusInternalServerError, errors.New(internalServerErrorMsg) + } + err = d.db.CreateK8s(&k8sCluster) if err != nil { log.Error().Err(err).Send() diff --git a/server/deployer/vms_deployer.go b/server/deployer/vms_deployer.go index 14aecb8c..c4f7567b 100644 --- a/server/deployer/vms_deployer.go +++ b/server/deployer/vms_deployer.go @@ -88,24 +88,23 @@ func (d *Deployer) deployVM(ctx context.Context, vmInput models.DeployVMInput, s } // ValidateVMQuota validates the quota a vm deployment need -func ValidateVMQuota(vm models.DeployVMInput, availableResourcesQuota []models.QuotaVM, availablePublicIPsQuota int) (time.Time, int, error) { +func ValidateVMQuota(vm models.DeployVMInput, availableResourcesQuota []models.QuotaVM, availablePublicIPsQuota int) (int, int, error) { neededQuota, err := calcNeededQuota(vm.Resources) if err != nil { - return time.Now(), 0, err + return 0, 0, err } if vm.Public && availablePublicIPsQuota < publicQuota { - return time.Now(), 0, fmt.Errorf("no available quota %d for public ips", availablePublicIPsQuota) + return 0, 0, fmt.Errorf("no available quota %d for public ips", availablePublicIPsQuota) } - requestedExpirationDate := time.Now().Add(time.Duration(vm.Duration) * 30 * 24 * time.Hour) - for _, vms := range availableResourcesQuota { - if requestedExpirationDate.Before(vms.ExpirationDate) && neededQuota <= vms.Vms { - return vms.ExpirationDate, vms.Vms - neededQuota, nil + for _, quotaVMs := range availableResourcesQuota { + if quotaVMs.Duration >= vm.Duration && quotaVMs.Vms >= neededQuota { + return quotaVMs.Duration, neededQuota, nil } } - return time.Now(), 0, fmt.Errorf("no available quota %v for deployment for resources %s, you can request a new voucher", availableResourcesQuota, vm.Resources) + return 0, 0, fmt.Errorf("no available quota %v for deployment for resources %s, you can request a new voucher", availableResourcesQuota, vm.Resources) } func (d *Deployer) deployVMRequest(ctx context.Context, user models.User, input models.DeployVMInput, adminSSHKey string) (int, error) { @@ -119,7 +118,7 @@ func (d *Deployer) deployVMRequest(ctx context.Context, user models.User, input return http.StatusInternalServerError, errors.New(internalServerErrorMsg) } - quotaVM, err := d.db.ListUserQuotaVMs(quota.ID.String()) + allQuotaVMs, err := d.db.ListUserQuotaVMs(quota.ID.String()) if err == gorm.ErrRecordNotFound { return http.StatusNotFound, errors.New("user quota vm is not found") } @@ -128,11 +127,21 @@ func (d *Deployer) deployVMRequest(ctx context.Context, user models.User, input return http.StatusInternalServerError, errors.New(internalServerErrorMsg) } - expirationDate, newQuotaVMs, err := ValidateVMQuota(input, quotaVM, quota.PublicIPs) + neededQuotaDuration, neededQuota, err := ValidateVMQuota(input, allQuotaVMs, quota.PublicIPs) if err != nil { return http.StatusBadRequest, err } + quotaVMs, err := d.db.GetUserQuotaVMs(quota.ID.String(), neededQuotaDuration) + if err == gorm.ErrRecordNotFound { + return http.StatusNotFound, errors.New("user quota vm is not found") + } + if err != nil { + log.Error().Err(err).Send() + return http.StatusInternalServerError, errors.New(internalServerErrorMsg) + } + + // deploy network and vm vm, contractID, networkContractID, diskSize, err := d.deployVM(ctx, input, user.SSHKey, adminSSHKey) if err != nil { log.Error().Err(err).Send() @@ -151,7 +160,7 @@ func (d *Deployer) deployVMRequest(ctx context.Context, user models.User, input MRU: uint64(vm.Memory), ContractID: contractID, NetworkContractID: networkContractID, - ExpirationDate: expirationDate, + ExpirationDate: time.Now().Add(time.Duration(quotaVMs.Duration) * 30 * 24 * time.Hour).Truncate(24 * time.Hour), } err = d.db.CreateVM(&userVM) @@ -174,7 +183,7 @@ func (d *Deployer) deployVMRequest(ctx context.Context, user models.User, input return http.StatusInternalServerError, errors.New(internalServerErrorMsg) } - err = d.db.UpdateUserQuotaVMs(quota.ID.String(), expirationDate, newQuotaVMs) + err = d.db.UpdateUserQuotaVMs(quota.ID.String(), neededQuotaDuration, quotaVMs.Vms-neededQuota) if err == gorm.ErrRecordNotFound { return http.StatusNotFound, errors.New("User quota vms is not found") } diff --git a/server/models/database.go b/server/models/database.go index 87add831..fc67d65a 100644 --- a/server/models/database.go +++ b/server/models/database.go @@ -32,7 +32,7 @@ func (d *DB) Connect(file string) error { // Migrate migrates db schema func (d *DB) Migrate() error { - err := d.db.AutoMigrate(&User{}, &Quota{}, &VM{}, &K8sCluster{}, &Master{}, &Worker{}, &Voucher{}, &Maintenance{}, &Notification{}) + err := d.db.AutoMigrate(&User{}, &Quota{}, &QuotaVM{}, &VM{}, &K8sCluster{}, &Master{}, &Worker{}, &Voucher{}, &Maintenance{}, &Notification{}) if err != nil { return err } @@ -68,11 +68,12 @@ func (d *DB) GetUserByID(id string) (User, error) { func (d *DB) ListAllUsers() ([]UserUsedQuota, error) { var res []UserUsedQuota query := d.db.Table("users"). - Select("*, users.id as user_id, sum(vouchers.vms) as vms, sum(vouchers.public_ips) as public_ips, sum(vouchers.vms) - quota.vms as used_vms, sum(vouchers.public_ips) - quota.public_ips as used_public_ips"). + Select("*, users.id as user_id, sum(vouchers.vms) as vms, sum(vouchers.public_ips) as public_ips, sum(vouchers.vms) - sum(quota_vms.vms) as used_vms, sum(vouchers.public_ips) - quota.public_ips as used_public_ips"). Joins("left join quota on quota.user_id = users.id"). + Joins("left join quota_vms on quota.id = quota_vms.quota_id"). Joins("left join vouchers on vouchers.used = true and vouchers.user_id = users.id"). Where("verified = true"). - Group("users.id"). + Group("users.id, quota.id"). Scan(&res) return res, query.Error } @@ -227,10 +228,15 @@ func (d *DB) UpdateUserQuota(userID string, publicIPs int) error { } // UpdateUserQuotaVMs updates quota vms -func (d *DB) UpdateUserQuotaVMs(QuotaID string, expirationDate time.Time, vms int) error { - return d.db.Model(&QuotaVM{}). - Where(&QuotaVM{QoutaID: QuotaID, ExpirationDate: expirationDate}). - Update("vms", vms).Error +func (d *DB) UpdateUserQuotaVMs(QuotaID string, duration int, vms int) error { + query := d.db.Model(&QuotaVM{}). + Where(&QuotaVM{QuotaID: QuotaID, Duration: duration}). + Update("vms", vms) + + if query.RowsAffected == 0 { + return d.CreateQuotaVM(&QuotaVM{QuotaID: QuotaID, Duration: duration, Vms: vms}) + } + return query.Error } // GetUserQuota gets user quota available publicIPs @@ -241,10 +247,9 @@ func (d *DB) GetUserQuota(userID string) (Quota, error) { } // GetUserQuotaVMs gets user quota available vms (vms will be used for both vms and k8s clusters) -func (d *DB) GetUserQuotaVMs(quotaID string, expirationDate time.Time) (QuotaVM, error) { +func (d *DB) GetUserQuotaVMs(quotaID string, duration int) (QuotaVM, error) { var res QuotaVM - query := d.db.Select("expiration_date", "vms"). - FirstOrCreate(&res, &QuotaVM{QoutaID: quotaID, ExpirationDate: expirationDate}) + query := d.db.FirstOrCreate(&res, &QuotaVM{QuotaID: quotaID, Duration: duration}) return res, query.Error } diff --git a/server/models/database_test.go b/server/models/database_test.go index 05f80b05..f13fb6a1 100644 --- a/server/models/database_test.go +++ b/server/models/database_test.go @@ -3,7 +3,6 @@ package models import ( "testing" - "time" "github.com/stretchr/testify/assert" "gorm.io/gorm" @@ -482,7 +481,7 @@ func TestCreateQuota(t *testing.T) { func TestUpdateUserQuota(t *testing.T) { db := setupDB(t) t.Run("quota not found so no updates", func(t *testing.T) { - err := db.UpdateUserQuota("user", map[time.Time]int{time.Now(): 5}, 0) + err := db.UpdateUserQuota("user", 0) assert.NoError(t, err) }) t.Run("quota found", func(t *testing.T) { @@ -494,24 +493,33 @@ func TestUpdateUserQuota(t *testing.T) { err = db.CreateQuota("a2) assert.NoError(t, err) - err = db.UpdateUserQuota("user", map[time.Time]int{time.Now().Add(time.Hour): 5}, 10) + err = db.UpdateUserQuota("user", 10) assert.NoError(t, err) var q Quota err = db.db.First(&q, "user_id = 'user'").Error assert.NoError(t, err) - assert.Equal(t, q.Vms, 5) + assert.Equal(t, q.PublicIPs, 10) + err = db.UpdateUserQuotaVMs(q.ID.String(), 1, 5) + assert.NoError(t, err) + + var qvm QuotaVM + err = db.db.First(&qvm, "quota_id = ? AND duration = ?", q.ID.String(), 1).Error + assert.NoError(t, err) + assert.Equal(t, qvm.Vms, 5) + + q = Quota{} err = db.db.First(&q, "user_id = 'new-user'").Error assert.NoError(t, err) - assert.Equal(t, q.Vms, 0) + assert.Equal(t, q.PublicIPs, 0) }) t.Run("quota found with zero values", func(t *testing.T) { quota := Quota{UserID: "1"} err := db.CreateQuota("a) assert.NoError(t, err) - err = db.UpdateUserQuota("1", map[time.Time]int{time.Now(): 0}, 0) + err = db.UpdateUserQuota("1", 0) assert.NoError(t, err) }) } diff --git a/server/models/quota.go b/server/models/quota.go index 1f5f3e6a..63e7eaa1 100644 --- a/server/models/quota.go +++ b/server/models/quota.go @@ -1,11 +1,26 @@ // Package models for database models package models -import "github.com/google/uuid" +import ( + "github.com/google/uuid" + "gorm.io/gorm" +) // Quota struct holds available vms for each user type Quota struct { ID uuid.UUID `gorm:"primary_key; unique; type:uuid; column:id"` UserID string `json:"user_id"` PublicIPs int `json:"public_ips"` + VMs []QuotaVM `json:"vms"` +} + +// BeforeCreate generates a new uuid +func (quota *Quota) BeforeCreate(tx *gorm.DB) (err error) { + id, err := uuid.NewUUID() + if err != nil { + return err + } + + quota.ID = id + return } diff --git a/server/models/quota_vms.go b/server/models/quota_vms.go index 5470b27f..9c7ce8ad 100644 --- a/server/models/quota_vms.go +++ b/server/models/quota_vms.go @@ -1,11 +1,9 @@ // Package models for database models package models -import "time" - // QuotaVM struct holds available vms and their expiration date for each user type QuotaVM struct { - QoutaID string `json:"qouta_id"` - Vms int `json:"vms"` - ExpirationDate time.Time `json:"expiration_date"` + QuotaID string `json:"qouta_id"` + Vms int `json:"vms"` + Duration int `json:"duration"` } diff --git a/server/models/voucher.go b/server/models/voucher.go index e3b3208c..46fdca65 100644 --- a/server/models/voucher.go +++ b/server/models/voucher.go @@ -5,16 +5,16 @@ import "time" // Voucher struct holds data of vouchers type Voucher struct { - ID int `json:"id" gorm:"primaryKey"` - UserID string `json:"user_id" binding:"required"` - Voucher string `json:"voucher" gorm:"unique"` - VMs int `json:"vms" binding:"required"` - PublicIPs int `json:"public_ips" binding:"required"` - Reason string `json:"reason" binding:"required"` - Used bool `json:"used" binding:"required"` - Approved bool `json:"approved" binding:"required"` - Rejected bool `json:"rejected" binding:"required"` - VoucherDuration int `json:"voucher_duration" binding:"required"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID int `json:"id" gorm:"primaryKey"` + UserID string `json:"user_id" binding:"required"` + Voucher string `json:"voucher" gorm:"unique"` + VMs int `json:"vms" binding:"required"` + PublicIPs int `json:"public_ips" binding:"required"` + Reason string `json:"reason" binding:"required"` + Used bool `json:"used" binding:"required"` + Approved bool `json:"approved" binding:"required"` + Rejected bool `json:"rejected" binding:"required"` + Duration int `json:"duration" binding:"required"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } From 67fd8ffc7938ccf90b2de280097288ad7de156c1 Mon Sep 17 00:00:00 2001 From: Eslam-Nawara Date: Mon, 11 Dec 2023 13:21:36 +0200 Subject: [PATCH 5/7] delete all expired node on exeeding expiration date --- server/app/app.go | 2 ++ server/deployer/deployer.go | 64 +++++++++++++++++++++++++++++++++ server/deployer/k8s_deployer.go | 2 ++ server/models/database.go | 2 +- server/models/k8s.go | 15 ++++---- 5 files changed, 78 insertions(+), 7 deletions(-) diff --git a/server/app/app.go b/server/app/app.go index 1a6a310e..554ac2b3 100644 --- a/server/app/app.go +++ b/server/app/app.go @@ -88,6 +88,8 @@ func (a *App) startBackgroundWorkers(ctx context.Context) { go a.deployer.PeriodicDeploy(ctx, substrateBlockDiffInSeconds) // remove expired vms and k8s + go a.deployer.CleanExpiredVMs(ctx) + go a.deployer.CleanExpiredK8S(ctx) // check pending deployments a.deployer.ConsumeVMRequest(ctx, true) diff --git a/server/deployer/deployer.go b/server/deployer/deployer.go index 01b20bd2..f32eeac3 100644 --- a/server/deployer/deployer.go +++ b/server/deployer/deployer.go @@ -166,6 +166,70 @@ func (d *Deployer) CancelDeployment(contractID uint64, netContractID uint64, dlT return nil } +func (d *Deployer) CleanExpiredVMs(ctx context.Context) { + ticker := time.NewTicker(24 * time.Hour) + for range ticker.C { + users, err := d.db.ListAllUsers() + if err != nil { + log.Error().Err(err).Msg("failed to get all users") + return + } + + for _, user := range users { + vms, err := d.db.GetAllVms(user.UserID) + if err != nil { + log.Error().Err(err).Msg("failed to get all user vms") + continue + } + + for _, vm := range vms { + if vm.ExpirationDate.Before(time.Now()) { + err = d.CancelDeployment(vm.ContractID, vm.NetworkContractID, "vm", vm.Name) + if err != nil { + log.Error().Err(err).Msg("failed to cancel contract of expired vm") + } + err := d.db.DeleteVMByID(vm.ID) + if err != nil { + log.Error().Err(err).Msg("failed to delete expired vm") + } + } + } + } + } +} + +func (d *Deployer) CleanExpiredK8S(ctx context.Context) { + ticker := time.NewTicker(24 * time.Hour) + for range ticker.C { + users, err := d.db.ListAllUsers() + if err != nil { + log.Error().Err(err).Msg("failed to get all users") + return + } + + for _, user := range users { + k8s, err := d.db.GetAllK8s(user.UserID) + if err != nil { + log.Error().Err(err).Msg("failed to get all user k8s clusters") + continue + } + + for _, k := range k8s { + if k.ExpirationDate.Before(time.Now()) { + err = d.CancelDeployment(uint64(k.ClusterContract), uint64(k.NetworkContract), "k8s", k.Master.Name) + if err != nil { + log.Error().Err(err).Msg("failed to cancel contract of expired k8s cluster") + } + err := d.db.DeleteVMByID(k.ID) + if err != nil { + log.Error().Err(err).Msg("failed to delete expired k8s cluster") + } + } + } + } + } +} + func buildNetwork(node uint32, name string) workloads.ZNet { return workloads.ZNet{ Name: name, diff --git a/server/deployer/k8s_deployer.go b/server/deployer/k8s_deployer.go index bb059ac3..ac448d42 100644 --- a/server/deployer/k8s_deployer.go +++ b/server/deployer/k8s_deployer.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "net/http" + "time" "github.com/codescalers/cloud4students/middlewares" "github.com/codescalers/cloud4students/models" @@ -154,6 +155,7 @@ func (d *Deployer) loadK8s(k8sDeployInput models.K8sDeployInput, userID string, ClusterContract: int(k8sContractID), Master: master, Workers: workers, + ExpirationDate: time.Now().Add(time.Duration(k8sDeployInput.Duration) * 30 * 24 * time.Hour).Truncate(24 * time.Hour), } return k8sCluster, nil diff --git a/server/models/database.go b/server/models/database.go index fc67d65a..003a9c61 100644 --- a/server/models/database.go +++ b/server/models/database.go @@ -73,7 +73,7 @@ func (d *DB) ListAllUsers() ([]UserUsedQuota, error) { Joins("left join quota_vms on quota.id = quota_vms.quota_id"). Joins("left join vouchers on vouchers.used = true and vouchers.user_id = users.id"). Where("verified = true"). - Group("users.id, quota.id"). + Group("users.id"). Scan(&res) return res, query.Error } diff --git a/server/models/k8s.go b/server/models/k8s.go index c6347623..d1b4a321 100644 --- a/server/models/k8s.go +++ b/server/models/k8s.go @@ -1,14 +1,17 @@ // Package models for database models package models +import "time" + // K8sCluster holds all cluster data type K8sCluster struct { - ID int `json:"id" gorm:"primaryKey"` - UserID string `json:"userID"` - NetworkContract int `json:"network_contract_id"` - ClusterContract int `json:"contract_id"` - Master Master `json:"master" gorm:"foreignKey:ClusterID"` - Workers []Worker `json:"workers" gorm:"foreignKey:ClusterID"` + ID int `json:"id" gorm:"primaryKey"` + UserID string `json:"userID"` + NetworkContract int `json:"network_contract_id"` + ClusterContract int `json:"contract_id"` + Master Master `json:"master" gorm:"foreignKey:ClusterID"` + Workers []Worker `json:"workers" gorm:"foreignKey:ClusterID"` + ExpirationDate time.Time `json:"expiration_date"` } // Master struct for kubernetes master data From 2fdfd33526557b713fa72f5bac01bb6936666ca3 Mon Sep 17 00:00:00 2001 From: Eslam-Nawara Date: Tue, 12 Dec 2023 12:30:22 +0200 Subject: [PATCH 6/7] send notfication to user one day before expiration date --- server/app/app.go | 4 ++ server/app/user_handler.go | 30 +++++++------- server/app/voucher_handler.go | 20 ++++----- server/deployer/deployer.go | 72 ++++++++++++++++++++++++++++++++- server/deployer/k8s_deployer.go | 2 +- server/deployer/vms_deployer.go | 2 +- server/models/k8s.go | 2 +- server/models/vm.go | 2 +- server/models/voucher.go | 24 +++++------ 9 files changed, 115 insertions(+), 43 deletions(-) diff --git a/server/app/app.go b/server/app/app.go index 554ac2b3..1b535680 100644 --- a/server/app/app.go +++ b/server/app/app.go @@ -87,6 +87,10 @@ func (a *App) startBackgroundWorkers(ctx context.Context) { go a.deployer.PeriodicRequests(ctx, substrateBlockDiffInSeconds) go a.deployer.PeriodicDeploy(ctx, substrateBlockDiffInSeconds) + // send notification about vms and k8s expiration + go a.deployer.WarnUsersWithExpiredVMs(ctx) + go a.deployer.WarnUsersWithExpiredK8s(ctx) + // remove expired vms and k8s go a.deployer.CleanExpiredVMs(ctx) go a.deployer.CleanExpiredK8S(ctx) diff --git a/server/app/user_handler.go b/server/app/user_handler.go index fbc70250..595cff81 100644 --- a/server/app/user_handler.go +++ b/server/app/user_handler.go @@ -66,16 +66,16 @@ type EmailInput struct { // ApplyForVoucherInput struct for user to apply for voucher type ApplyForVoucherInput struct { - VMs int `json:"vms" binding:"required" validate:"min=0"` - PublicIPs int `json:"public_ips" binding:"required" validate:"min=0"` - Reason string `json:"reason" binding:"required" validate:"nonzero"` - VoucherDuration int `json:"voucher_duration" binding:"required"` + VMs int `json:"vms" binding:"required" validate:"min=0"` + PublicIPs int `json:"public_ips" binding:"required" validate:"min=0"` + Reason string `json:"reason" binding:"required" validate:"nonzero"` + VoucherDurationInMonth int `json:"voucher_duration_in_month" binding:"required"` } // AddVoucherInput struct for voucher applied by user type AddVoucherInput struct { - Voucher string `json:"voucher" binding:"required"` - RequestedDuration int `json:"requestedDuration" binding:"required"` + Voucher string `json:"voucher" binding:"required"` + VoucherDurationInMonth int `json:"voucher_duration_in_month" binding:"required"` } // SignUpHandler creates account for user @@ -574,19 +574,19 @@ func (a *App) ApplyForVoucherHandler(req *http.Request) (interface{}, Response) } // make sure the requested duration is less that the maximum allowed duration - if input.VoucherDuration > a.config.VouchersMaxDuration { + if input.VoucherDurationInMonth > a.config.VouchersMaxDuration { return nil, BadRequest(fmt.Errorf("invalid voucher duration, max duration is %d", a.config.VouchersMaxDuration)) } // generate voucher for user but can't use it until admin approves it v := internal.GenerateRandomVoucher(5) voucher := models.Voucher{ - Voucher: v, - UserID: userID, - VMs: input.VMs, - Reason: input.Reason, - PublicIPs: input.PublicIPs, - Duration: input.VoucherDuration, + Voucher: v, + UserID: userID, + VMs: input.VMs, + Reason: input.Reason, + PublicIPs: input.PublicIPs, + VoucherDurationInMonth: input.VoucherDurationInMonth, } err = a.db.CreateVoucher(&voucher) @@ -631,7 +631,7 @@ func (a *App) ActivateVoucherHandler(req *http.Request) (interface{}, Response) return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - userQuotaVMs, err := a.db.GetUserQuotaVMs(quota.ID.String(), voucherQuota.Duration) + userQuotaVMs, err := a.db.GetUserQuotaVMs(quota.ID.String(), voucherQuota.VoucherDurationInMonth) if err == gorm.ErrRecordNotFound { return nil, NotFound(errors.New("user quota vms is not found")) } @@ -664,7 +664,7 @@ func (a *App) ActivateVoucherHandler(req *http.Request) (interface{}, Response) return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - err = a.db.UpdateUserQuotaVMs(quota.ID.String(), voucherQuota.Duration, userQuotaVMs.Vms+voucherQuota.VMs) + err = a.db.UpdateUserQuotaVMs(quota.ID.String(), voucherQuota.VoucherDurationInMonth, userQuotaVMs.Vms+voucherQuota.VMs) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) diff --git a/server/app/voucher_handler.go b/server/app/voucher_handler.go index 11fdd2ec..c6a48422 100644 --- a/server/app/voucher_handler.go +++ b/server/app/voucher_handler.go @@ -18,10 +18,10 @@ import ( // GenerateVoucherInput struct for data needed when user generate vouchers type GenerateVoucherInput struct { - Length int `json:"length" binding:"required" validate:"min=3,max=20"` - VMs int `json:"vms" binding:"required"` - PublicIPs int `json:"public_ips" binding:"required"` - VoucherDuration int `json:"voucher_duration" binding:"required"` + Length int `json:"length" binding:"required" validate:"min=3,max=20"` + VMs int `json:"vms" binding:"required"` + PublicIPs int `json:"public_ips" binding:"required"` + VoucherDurationInMonth int `json:"voucher_duration_in_month" binding:"required"` } // UpdateVoucherInput struct for data needed when user update voucher @@ -45,16 +45,16 @@ func (a *App) GenerateVoucherHandler(req *http.Request) (interface{}, Response) } voucher := internal.GenerateRandomVoucher(input.Length) - if input.VoucherDuration > a.config.VouchersMaxDuration { + if input.VoucherDurationInMonth > a.config.VouchersMaxDuration { return nil, BadRequest(fmt.Errorf("invalid voucher duration, max duration is %d", a.config.VouchersMaxDuration)) } v := models.Voucher{ - Voucher: voucher, - VMs: input.VMs, - PublicIPs: input.PublicIPs, - Approved: true, - Duration: input.VoucherDuration, + Voucher: voucher, + VMs: input.VMs, + PublicIPs: input.PublicIPs, + Approved: true, + VoucherDurationInMonth: input.VoucherDurationInMonth, } err = a.db.CreateVoucher(&v) diff --git a/server/deployer/deployer.go b/server/deployer/deployer.go index f32eeac3..4fd0feef 100644 --- a/server/deployer/deployer.go +++ b/server/deployer/deployer.go @@ -166,6 +166,74 @@ func (d *Deployer) CancelDeployment(contractID uint64, netContractID uint64, dlT return nil } +func (d *Deployer) WarnUsersWithExpiredVMs(ctx context.Context) { + ticker := time.NewTicker(24 * time.Hour) + for range ticker.C { + users, err := d.db.ListAllUsers() + if err != nil { + log.Error().Err(err).Msg("failed to get all users") + return + } + + for _, user := range users { + vms, err := d.db.GetAllVms(user.UserID) + if err != nil { + log.Error().Err(err).Msg("failed to get all user vms") + continue + } + + for _, vm := range vms { + if time.Now().Before(vm.ExpiresAt) && time.Until(vm.ExpiresAt) < time.Hour*24 { + notification := models.Notification{ + UserID: user.UserID, + Msg: fmt.Sprintf("Warning: vm with id %d expires in one day", vm.ID), + Type: models.VMsType, + } + + err = d.db.CreateNotification(¬ification) + if err != nil { + log.Error().Err(err).Msgf("failed to create notification: %+v", notification) + } + } + } + } + } +} + +func (d *Deployer) WarnUsersWithExpiredK8s(ctx context.Context) { + ticker := time.NewTicker(24 * time.Hour) + for range ticker.C { + users, err := d.db.ListAllUsers() + if err != nil { + log.Error().Err(err).Msg("failed to get all users") + return + } + + for _, user := range users { + k8s, err := d.db.GetAllK8s(user.UserID) + if err != nil { + log.Error().Err(err).Msg("failed to get all user k8s clusters") + continue + } + + for _, k := range k8s { + if time.Now().Before(k.ExpiresAt) && time.Until(k.ExpiresAt) < time.Hour*24 { + notification := models.Notification{ + UserID: user.UserID, + Msg: fmt.Sprintf("Warning: k8s cluster with id %d expires in one day", k.ID), + Type: models.K8sType, + } + + err = d.db.CreateNotification(¬ification) + if err != nil { + log.Error().Err(err).Msgf("failed to create notification: %+v", notification) + } + } + } + } + } +} + func (d *Deployer) CleanExpiredVMs(ctx context.Context) { ticker := time.NewTicker(24 * time.Hour) for range ticker.C { @@ -183,7 +251,7 @@ func (d *Deployer) CleanExpiredVMs(ctx context.Context) { } for _, vm := range vms { - if vm.ExpirationDate.Before(time.Now()) { + if vm.ExpiresAt.Before(time.Now()) { err = d.CancelDeployment(vm.ContractID, vm.NetworkContractID, "vm", vm.Name) if err != nil { log.Error().Err(err).Msg("failed to cancel contract of expired vm") @@ -215,7 +283,7 @@ func (d *Deployer) CleanExpiredK8S(ctx context.Context) { } for _, k := range k8s { - if k.ExpirationDate.Before(time.Now()) { + if k.ExpiresAt.Before(time.Now()) { err = d.CancelDeployment(uint64(k.ClusterContract), uint64(k.NetworkContract), "k8s", k.Master.Name) if err != nil { log.Error().Err(err).Msg("failed to cancel contract of expired k8s cluster") diff --git a/server/deployer/k8s_deployer.go b/server/deployer/k8s_deployer.go index ac448d42..829674a4 100644 --- a/server/deployer/k8s_deployer.go +++ b/server/deployer/k8s_deployer.go @@ -155,7 +155,7 @@ func (d *Deployer) loadK8s(k8sDeployInput models.K8sDeployInput, userID string, ClusterContract: int(k8sContractID), Master: master, Workers: workers, - ExpirationDate: time.Now().Add(time.Duration(k8sDeployInput.Duration) * 30 * 24 * time.Hour).Truncate(24 * time.Hour), + ExpiresAt: time.Now().Add(time.Duration(k8sDeployInput.Duration) * 30 * 24 * time.Hour).Truncate(24 * time.Hour), } return k8sCluster, nil diff --git a/server/deployer/vms_deployer.go b/server/deployer/vms_deployer.go index c4f7567b..37219ac1 100644 --- a/server/deployer/vms_deployer.go +++ b/server/deployer/vms_deployer.go @@ -160,7 +160,7 @@ func (d *Deployer) deployVMRequest(ctx context.Context, user models.User, input MRU: uint64(vm.Memory), ContractID: contractID, NetworkContractID: networkContractID, - ExpirationDate: time.Now().Add(time.Duration(quotaVMs.Duration) * 30 * 24 * time.Hour).Truncate(24 * time.Hour), + ExpiresAt: time.Now().Add(time.Duration(quotaVMs.Duration) * 30 * 24 * time.Hour).Truncate(24 * time.Hour), } err = d.db.CreateVM(&userVM) diff --git a/server/models/k8s.go b/server/models/k8s.go index d1b4a321..c1a84f6d 100644 --- a/server/models/k8s.go +++ b/server/models/k8s.go @@ -11,7 +11,7 @@ type K8sCluster struct { ClusterContract int `json:"contract_id"` Master Master `json:"master" gorm:"foreignKey:ClusterID"` Workers []Worker `json:"workers" gorm:"foreignKey:ClusterID"` - ExpirationDate time.Time `json:"expiration_date"` + ExpiresAt time.Time `json:"expires_at"` } // Master struct for kubernetes master data diff --git a/server/models/vm.go b/server/models/vm.go index a459b1d7..cdd0751f 100644 --- a/server/models/vm.go +++ b/server/models/vm.go @@ -17,7 +17,7 @@ type VM struct { MRU uint64 `json:"mru"` ContractID uint64 `json:"contractID"` NetworkContractID uint64 `json:"networkContractID"` - ExpirationDate time.Time `json:"expirationDate" binding:"required"` + ExpiresAt time.Time `json:"expires_at" binding:"required"` } // DeploymentsCount has the vms and ips reserved in the grid diff --git a/server/models/voucher.go b/server/models/voucher.go index 46fdca65..700da55a 100644 --- a/server/models/voucher.go +++ b/server/models/voucher.go @@ -5,16 +5,16 @@ import "time" // Voucher struct holds data of vouchers type Voucher struct { - ID int `json:"id" gorm:"primaryKey"` - UserID string `json:"user_id" binding:"required"` - Voucher string `json:"voucher" gorm:"unique"` - VMs int `json:"vms" binding:"required"` - PublicIPs int `json:"public_ips" binding:"required"` - Reason string `json:"reason" binding:"required"` - Used bool `json:"used" binding:"required"` - Approved bool `json:"approved" binding:"required"` - Rejected bool `json:"rejected" binding:"required"` - Duration int `json:"duration" binding:"required"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID int `json:"id" gorm:"primaryKey"` + UserID string `json:"user_id" binding:"required"` + Voucher string `json:"voucher" gorm:"unique"` + VMs int `json:"vms" binding:"required"` + PublicIPs int `json:"public_ips" binding:"required"` + Reason string `json:"reason" binding:"required"` + Used bool `json:"used" binding:"required"` + Approved bool `json:"approved" binding:"required"` + Rejected bool `json:"rejected" binding:"required"` + VoucherDurationInMonth int `json:"voucher_duration_in_month" binding:"required"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } From 9e3a546cfdfbef9fe5bfdd3312f22f158e83d6f7 Mon Sep 17 00:00:00 2001 From: Eslam-Nawara Date: Tue, 12 Dec 2023 17:13:36 +0200 Subject: [PATCH 7/7] use foreign key constraint in QuotaVM relation --- server/app/k8s_handler.go | 11 +-------- server/app/quota_handler.go | 18 +-------------- server/app/user_handler.go | 21 ++++++++--------- server/app/vm_handler.go | 11 +-------- server/deployer/k8s_deployer.go | 27 +++++----------------- server/deployer/vms_deployer.go | 40 +++++++++++++-------------------- server/models/database.go | 28 ++++------------------- server/models/database_test.go | 10 ++++----- server/models/quota.go | 6 ++--- server/models/quota_vms.go | 4 ++-- 10 files changed, 49 insertions(+), 127 deletions(-) diff --git a/server/app/k8s_handler.go b/server/app/k8s_handler.go index 7d555f26..3378bf72 100644 --- a/server/app/k8s_handler.go +++ b/server/app/k8s_handler.go @@ -54,16 +54,7 @@ func (a *App) K8sDeployHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - allQuotaVMs, err := a.db.ListUserQuotaVMs(quota.ID.String()) - if err == gorm.ErrRecordNotFound { - return nil, NotFound(errors.New("user quota vms are not found")) - } - if err != nil { - log.Error().Err(err).Send() - return nil, InternalServerError(errors.New(internalServerErrorMsg)) - } - - _, _, err = deployer.ValidateK8sQuota(k8sDeployInput, allQuotaVMs, quota.PublicIPs) + _, _, err = deployer.ValidateK8sQuota(k8sDeployInput, quota.QuotaVMs, quota.PublicIPs) if err != nil { log.Error().Err(err).Send() return nil, BadRequest(errors.New(err.Error())) diff --git a/server/app/quota_handler.go b/server/app/quota_handler.go index 1e497fc0..193de4e1 100644 --- a/server/app/quota_handler.go +++ b/server/app/quota_handler.go @@ -6,17 +6,10 @@ import ( "net/http" "github.com/codescalers/cloud4students/middlewares" - "github.com/codescalers/cloud4students/models" "github.com/rs/zerolog/log" "gorm.io/gorm" ) -// QuotaData represents the structure for Quota and QuotaVMs data. -type QuotaData struct { - Quota models.Quota - QuotaVMs []models.QuotaVM -} - // GetQuotaHandler gets quota func (a *App) GetQuotaHandler(req *http.Request) (interface{}, Response) { userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) @@ -30,17 +23,8 @@ func (a *App) GetQuotaHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - quotaVMs, err := a.db.ListUserQuotaVMs(quota.ID.String()) - if err == gorm.ErrRecordNotFound { - return nil, NotFound(errors.New("user quota vms is not found")) - } - if err != nil { - log.Error().Err(err).Send() - return nil, InternalServerError(errors.New(internalServerErrorMsg)) - } - return ResponseMsg{ Message: "Quota is found", - Data: QuotaData{Quota: quota, QuotaVMs: quotaVMs}, + Data: quota, }, Ok() } diff --git a/server/app/user_handler.go b/server/app/user_handler.go index 595cff81..f6646ab3 100644 --- a/server/app/user_handler.go +++ b/server/app/user_handler.go @@ -631,15 +631,6 @@ func (a *App) ActivateVoucherHandler(req *http.Request) (interface{}, Response) return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - userQuotaVMs, err := a.db.GetUserQuotaVMs(quota.ID.String(), voucherQuota.VoucherDurationInMonth) - if err == gorm.ErrRecordNotFound { - return nil, NotFound(errors.New("user quota vms is not found")) - } - if err != nil { - log.Error().Err(err).Send() - return nil, InternalServerError(errors.New(internalServerErrorMsg)) - } - if voucherQuota.Rejected { return nil, BadRequest(errors.New("voucher is rejected")) } @@ -664,7 +655,8 @@ func (a *App) ActivateVoucherHandler(req *http.Request) (interface{}, Response) return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - err = a.db.UpdateUserQuotaVMs(quota.ID.String(), voucherQuota.VoucherDurationInMonth, userQuotaVMs.Vms+voucherQuota.VMs) + vms := getDurationVMs(quota, voucherQuota.VoucherDurationInMonth) + err = a.db.UpdateUserQuotaVMs(quota.ID, voucherQuota.VoucherDurationInMonth, vms+voucherQuota.VMs) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) @@ -677,3 +669,12 @@ func (a *App) ActivateVoucherHandler(req *http.Request) (interface{}, Response) Data: nil, }, Ok() } + +func getDurationVMs(quota models.Quota, duration int) int { + for _, q := range quota.QuotaVMs { + if duration == q.Duration { + return q.VMs + } + } + return 0 +} diff --git a/server/app/vm_handler.go b/server/app/vm_handler.go index b18af815..b40384cf 100644 --- a/server/app/vm_handler.go +++ b/server/app/vm_handler.go @@ -54,16 +54,7 @@ func (a *App) DeployVMHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - quotaVMs, err := a.db.ListUserQuotaVMs(quota.ID.String()) - if err == gorm.ErrRecordNotFound { - return nil, NotFound(errors.New("user quota vms is not found")) - } - if err != nil { - log.Error().Err(err).Send() - return nil, InternalServerError(errors.New(internalServerErrorMsg)) - } - - _, _, err = deployer.ValidateVMQuota(input, quotaVMs, quota.PublicIPs) + _, _, err = deployer.ValidateVMQuota(input, quota.QuotaVMs, quota.PublicIPs) if err != nil { return nil, BadRequest(errors.New(err.Error())) } diff --git a/server/deployer/k8s_deployer.go b/server/deployer/k8s_deployer.go index 829674a4..b85b5994 100644 --- a/server/deployer/k8s_deployer.go +++ b/server/deployer/k8s_deployer.go @@ -220,7 +220,7 @@ func ValidateK8sQuota(k models.K8sDeployInput, availableResourcesQuota []models. } for _, quotaVMs := range availableResourcesQuota { - if quotaVMs.Duration >= k.Duration && quotaVMs.Vms >= neededQuota { + if quotaVMs.Duration >= k.Duration && quotaVMs.VMs >= neededQuota { return quotaVMs.Duration, neededQuota, nil } } @@ -240,30 +240,12 @@ func (d *Deployer) deployK8sRequest(ctx context.Context, user models.User, k8sDe return http.StatusInternalServerError, errors.New(internalServerErrorMsg) } - allQuotaVMs, err := d.db.ListUserQuotaVMs(quota.ID.String()) - if err == gorm.ErrRecordNotFound { - return http.StatusNotFound, errors.New("user quota vms are not found") - } - if err != nil { - log.Error().Err(err).Send() - return http.StatusInternalServerError, errors.New(internalServerErrorMsg) - } - - neededQuotaDuration, neededQuota, err := ValidateK8sQuota(k8sDeployInput, allQuotaVMs, quota.PublicIPs) + neededQuotaDuration, neededQuota, err := ValidateK8sQuota(k8sDeployInput, quota.QuotaVMs, quota.PublicIPs) if err != nil { log.Error().Err(err).Send() return http.StatusBadRequest, err } - quotaVMs, err := d.db.GetUserQuotaVMs(quota.ID.String(), neededQuotaDuration) - if err == gorm.ErrRecordNotFound { - return http.StatusNotFound, errors.New("user quota vm is not found") - } - if err != nil { - log.Error().Err(err).Send() - return http.StatusInternalServerError, errors.New(internalServerErrorMsg) - } - // deploy network and cluster node, networkContractID, k8sContractID, err := d.deployK8sClusterWithNetwork(ctx, k8sDeployInput, user.SSHKey, adminSSHKey) if err != nil { @@ -292,9 +274,10 @@ func (d *Deployer) deployK8sRequest(ctx context.Context, user models.User, k8sDe return http.StatusInternalServerError, errors.New(internalServerErrorMsg) } - err = d.db.UpdateUserQuotaVMs(quota.ID.String(), neededQuotaDuration, quotaVMs.Vms-neededQuota) + vms := getDurationVMs(quota, neededQuotaDuration) + err = d.db.UpdateUserQuotaVMs(quota.ID, neededQuotaDuration, vms-neededQuota) if err == gorm.ErrRecordNotFound { - return http.StatusNotFound, errors.New("User quota vms is not found") + return http.StatusNotFound, errors.New("user quota vms are not found") } if err != nil { log.Error().Err(err).Send() diff --git a/server/deployer/vms_deployer.go b/server/deployer/vms_deployer.go index 37219ac1..8168f9e0 100644 --- a/server/deployer/vms_deployer.go +++ b/server/deployer/vms_deployer.go @@ -99,7 +99,7 @@ func ValidateVMQuota(vm models.DeployVMInput, availableResourcesQuota []models.Q } for _, quotaVMs := range availableResourcesQuota { - if quotaVMs.Duration >= vm.Duration && quotaVMs.Vms >= neededQuota { + if quotaVMs.Duration >= vm.Duration && quotaVMs.VMs >= neededQuota { return quotaVMs.Duration, neededQuota, nil } } @@ -118,36 +118,17 @@ func (d *Deployer) deployVMRequest(ctx context.Context, user models.User, input return http.StatusInternalServerError, errors.New(internalServerErrorMsg) } - allQuotaVMs, err := d.db.ListUserQuotaVMs(quota.ID.String()) - if err == gorm.ErrRecordNotFound { - return http.StatusNotFound, errors.New("user quota vm is not found") - } - if err != nil { - log.Error().Err(err).Send() - return http.StatusInternalServerError, errors.New(internalServerErrorMsg) - } - - neededQuotaDuration, neededQuota, err := ValidateVMQuota(input, allQuotaVMs, quota.PublicIPs) + neededQuotaDuration, neededQuota, err := ValidateVMQuota(input, quota.QuotaVMs, quota.PublicIPs) if err != nil { return http.StatusBadRequest, err } - quotaVMs, err := d.db.GetUserQuotaVMs(quota.ID.String(), neededQuotaDuration) - if err == gorm.ErrRecordNotFound { - return http.StatusNotFound, errors.New("user quota vm is not found") - } - if err != nil { - log.Error().Err(err).Send() - return http.StatusInternalServerError, errors.New(internalServerErrorMsg) - } - // deploy network and vm vm, contractID, networkContractID, diskSize, err := d.deployVM(ctx, input, user.SSHKey, adminSSHKey) if err != nil { log.Error().Err(err).Send() return http.StatusInternalServerError, errors.New(internalServerErrorMsg) } - userVM := models.VM{ UserID: user.ID.String(), Name: vm.Name, @@ -160,7 +141,7 @@ func (d *Deployer) deployVMRequest(ctx context.Context, user models.User, input MRU: uint64(vm.Memory), ContractID: contractID, NetworkContractID: networkContractID, - ExpiresAt: time.Now().Add(time.Duration(quotaVMs.Duration) * 30 * 24 * time.Hour).Truncate(24 * time.Hour), + ExpiresAt: time.Now().Add(time.Duration(neededQuotaDuration) * 30 * 24 * time.Hour).Truncate(24 * time.Hour), } err = d.db.CreateVM(&userVM) @@ -173,6 +154,7 @@ func (d *Deployer) deployVMRequest(ctx context.Context, user models.User, input if input.Public { publicIPsQuota -= publicQuota } + // update quota of user err = d.db.UpdateUserQuota(user.ID.String(), publicIPsQuota) if err == gorm.ErrRecordNotFound { @@ -183,9 +165,10 @@ func (d *Deployer) deployVMRequest(ctx context.Context, user models.User, input return http.StatusInternalServerError, errors.New(internalServerErrorMsg) } - err = d.db.UpdateUserQuotaVMs(quota.ID.String(), neededQuotaDuration, quotaVMs.Vms-neededQuota) + vms := getDurationVMs(quota, neededQuotaDuration) + err = d.db.UpdateUserQuotaVMs(quota.ID, neededQuotaDuration, vms-neededQuota) if err == gorm.ErrRecordNotFound { - return http.StatusNotFound, errors.New("User quota vms is not found") + return http.StatusNotFound, errors.New("User quota is not found") } if err != nil { log.Error().Err(err).Send() @@ -195,3 +178,12 @@ func (d *Deployer) deployVMRequest(ctx context.Context, user models.User, input middlewares.Deployments.WithLabelValues(user.ID.String(), input.Resources, "vm").Inc() return 0, nil } + +func getDurationVMs(quota models.Quota, duration int) int { + for _, q := range quota.QuotaVMs { + if duration == q.Duration { + return q.VMs + } + } + return 0 +} diff --git a/server/models/database.go b/server/models/database.go index 003a9c61..cea2e32e 100644 --- a/server/models/database.go +++ b/server/models/database.go @@ -216,25 +216,19 @@ func (d *DB) CreateQuota(q *Quota) error { return result.Error } -// CreateQuota creates a new quota vm -func (d *DB) CreateQuotaVM(q *QuotaVM) error { - result := d.db.Create(&q) - return result.Error -} - // UpdateUserQuota updates quota func (d *DB) UpdateUserQuota(userID string, publicIPs int) error { return d.db.Model(&Quota{}).Where("user_id = ?", userID).Update("public_ips", publicIPs).Error } // UpdateUserQuotaVMs updates quota vms -func (d *DB) UpdateUserQuotaVMs(QuotaID string, duration int, vms int) error { +func (d *DB) UpdateUserQuotaVMs(quotaID string, duration int, vms int) error { query := d.db.Model(&QuotaVM{}). - Where(&QuotaVM{QuotaID: QuotaID, Duration: duration}). + Where(&QuotaVM{QuotaID: quotaID, Duration: duration}). Update("vms", vms) if query.RowsAffected == 0 { - return d.CreateQuotaVM(&QuotaVM{QuotaID: QuotaID, Duration: duration, Vms: vms}) + return d.db.Create(&QuotaVM{QuotaID: quotaID, Duration: duration, VMs: vms}).Error } return query.Error } @@ -242,21 +236,7 @@ func (d *DB) UpdateUserQuotaVMs(QuotaID string, duration int, vms int) error { // GetUserQuota gets user quota available publicIPs func (d *DB) GetUserQuota(userID string) (Quota, error) { var res Quota - query := d.db.First(&res, "user_id = ?", userID) - return res, query.Error -} - -// GetUserQuotaVMs gets user quota available vms (vms will be used for both vms and k8s clusters) -func (d *DB) GetUserQuotaVMs(quotaID string, duration int) (QuotaVM, error) { - var res QuotaVM - query := d.db.FirstOrCreate(&res, &QuotaVM{QuotaID: quotaID, Duration: duration}) - return res, query.Error -} - -// ListUserQuotaVMs gets user quota available vms (vms will be used for both vms and k8s clusters) -func (d *DB) ListUserQuotaVMs(quotaID string) ([]QuotaVM, error) { - var res []QuotaVM - query := d.db.Find(&res, "quota_id = ?", quotaID) + query := d.db.Preload("QuotaVMs").First(&res, "user_id = ?", userID) return res, query.Error } diff --git a/server/models/database_test.go b/server/models/database_test.go index f13fb6a1..6b0a6ef5 100644 --- a/server/models/database_test.go +++ b/server/models/database_test.go @@ -501,13 +501,13 @@ func TestUpdateUserQuota(t *testing.T) { assert.NoError(t, err) assert.Equal(t, q.PublicIPs, 10) - err = db.UpdateUserQuotaVMs(q.ID.String(), 1, 5) + err = db.UpdateUserQuotaVMs(q.ID, 1, 5) assert.NoError(t, err) - var qvm QuotaVM - err = db.db.First(&qvm, "quota_id = ? AND duration = ?", q.ID.String(), 1).Error + err = db.db.Preload("QuotaVMs").First(&q, "user_id = 'user'").Error assert.NoError(t, err) - assert.Equal(t, qvm.Vms, 5) + assert.Equal(t, q.PublicIPs, 10) + assert.Equal(t, q.QuotaVMs[0].VMs, 5) q = Quota{} err = db.db.First(&q, "user_id = 'new-user'").Error @@ -531,7 +531,7 @@ func TestGetUserQuota(t *testing.T) { assert.Equal(t, err, gorm.ErrRecordNotFound) }) t.Run("quota found", func(t *testing.T) { - quota1 := Quota{UserID: "user"} + quota1 := Quota{UserID: "user", QuotaVMs: []QuotaVM{}} quota2 := Quota{UserID: "new-user"} err := db.CreateQuota("a1) diff --git a/server/models/quota.go b/server/models/quota.go index 63e7eaa1..46f44828 100644 --- a/server/models/quota.go +++ b/server/models/quota.go @@ -8,10 +8,10 @@ import ( // Quota struct holds available vms for each user type Quota struct { - ID uuid.UUID `gorm:"primary_key; unique; type:uuid; column:id"` + ID string `gorm:"primary_key; unique; column:id"` UserID string `json:"user_id"` PublicIPs int `json:"public_ips"` - VMs []QuotaVM `json:"vms"` + QuotaVMs []QuotaVM `json:"vms" gorm:"foreignKey:quota_id"` } // BeforeCreate generates a new uuid @@ -21,6 +21,6 @@ func (quota *Quota) BeforeCreate(tx *gorm.DB) (err error) { return err } - quota.ID = id + quota.ID = id.String() return } diff --git a/server/models/quota_vms.go b/server/models/quota_vms.go index 9c7ce8ad..a319f4ab 100644 --- a/server/models/quota_vms.go +++ b/server/models/quota_vms.go @@ -4,6 +4,6 @@ package models // QuotaVM struct holds available vms and their expiration date for each user type QuotaVM struct { QuotaID string `json:"qouta_id"` - Vms int `json:"vms"` - Duration int `json:"duration"` + VMs int `json:"vms"` + Duration int `json:"duration" gorm:"unique"` }