diff --git a/server/app/app.go b/server/app/app.go index cc6dc3bf..1b535680 100644 --- a/server/app/app.go +++ b/server/app/app.go @@ -87,6 +87,14 @@ 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) + // 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..3378bf72 100644 --- a/server/app/k8s_handler.go +++ b/server/app/k8s_handler.go @@ -54,7 +54,7 @@ func (a *App) K8sDeployHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - _, err = deployer.ValidateK8sQuota(k8sDeployInput, quota.Vms, 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_test.go b/server/app/quota_handler_test.go index 97838e35..f31aea0a 100644 --- a/server/app/quota_handler_test.go +++ b/server/app/quota_handler_test.go @@ -44,7 +44,6 @@ func TestQuotaRouter(t *testing.T) { err = app.db.CreateQuota( &models.Quota{ UserID: user.ID.String(), - Vms: 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 18efa7d8..f6646ab3 100644 --- a/server/app/user_handler.go +++ b/server/app/user_handler.go @@ -66,21 +66,22 @@ 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"` + 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"` + Voucher string `json:"voucher" binding:"required"` + VoucherDurationInMonth int `json:"voucher_duration_in_month" binding:"required"` } // SignUpHandler creates account for user 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")) @@ -163,7 +164,6 @@ func (a *App) SignUpHandler(req *http.Request) (interface{}, Response) { // create empty quota quota := models.Quota{ UserID: u.ID.String(), - Vms: 0, } err = a.db.CreateQuota("a) if err != nil { @@ -573,14 +573,20 @@ 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.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, + Voucher: v, + UserID: userID, + VMs: input.VMs, + Reason: input.Reason, + PublicIPs: input.PublicIPs, + VoucherDurationInMonth: input.VoucherDurationInMonth, } err = a.db.CreateVoucher(&voucher) @@ -607,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) + quota, err := a.db.GetUserQuota(userID) if err == gorm.ErrRecordNotFound { return nil, NotFound(errors.New("user quota is not found")) } @@ -643,11 +649,19 @@ 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) + err = a.db.UpdateUserQuota(userID, quota.PublicIPs+voucherQuota.PublicIPs) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + + 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)) + } + middlewares.VoucherActivated.WithLabelValues(userID, voucherQuota.Voucher, fmt.Sprint(voucherQuota.VMs), fmt.Sprint(voucherQuota.PublicIPs)).Inc() return ResponseMsg{ @@ -655,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 17bfface..b40384cf 100644 --- a/server/app/vm_handler.go +++ b/server/app/vm_handler.go @@ -54,7 +54,7 @@ func (a *App) DeployVMHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } - _, err = deployer.ValidateVMQuota(input, quota.Vms, quota.PublicIPs) + _, _, err = deployer.ValidateVMQuota(input, quota.QuotaVMs, quota.PublicIPs) if err != nil { return nil, BadRequest(errors.New(err.Error())) } diff --git a/server/app/voucher_handler.go b/server/app/voucher_handler.go index 90a24aca..c6a48422 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"` + VoucherDurationInMonth int `json:"voucher_duration_in_month" 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.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, + 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 01b20bd2..4fd0feef 100644 --- a/server/deployer/deployer.go +++ b/server/deployer/deployer.go @@ -166,6 +166,138 @@ 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 { + 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.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") + } + 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.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") + } + 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 17f5f28e..b85b5994 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, + ExpiresAt: time.Now().Add(time.Duration(k8sDeployInput.Duration) * 30 * 24 * time.Hour).Truncate(24 * time.Hour), } return k8sCluster, nil @@ -199,28 +201,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,7 +240,7 @@ 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) + neededQuotaDuration, neededQuota, err := ValidateK8sQuota(k8sDeployInput, quota.QuotaVMs, quota.PublicIPs) if err != nil { log.Error().Err(err).Send() return http.StatusBadRequest, err @@ -253,12 +258,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 +274,16 @@ func (d *Deployer) deployK8sRequest(ctx context.Context, user models.User, k8sDe return http.StatusInternalServerError, errors.New(internalServerErrorMsg) } + 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 are 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 cf045b46..8168f9e0 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,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, availablePublicIPsQuota int) (int, error) { +func ValidateVMQuota(vm models.DeployVMInput, availableResourcesQuota []models.QuotaVM, availablePublicIPsQuota int) (int, int, error) { neededQuota, err := calcNeededQuota(vm.Resources) if err != nil { - return 0, err + return 0, 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 0, 0, fmt.Errorf("no available quota %d for public ips", availablePublicIPsQuota) } - return neededQuota, nil + for _, quotaVMs := range availableResourcesQuota { + if quotaVMs.Duration >= vm.Duration && quotaVMs.VMs >= neededQuota { + return quotaVMs.Duration, neededQuota, nil + } + } + + 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) { @@ -114,17 +118,17 @@ 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) + neededQuotaDuration, neededQuota, err := ValidateVMQuota(input, quota.QuotaVMs, quota.PublicIPs) if err != nil { return http.StatusBadRequest, err } + // 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, @@ -137,6 +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(neededQuotaDuration) * 30 * 24 * time.Hour).Truncate(24 * time.Hour), } err = d.db.CreateVM(&userVM) @@ -149,8 +154,19 @@ 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(), 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") + } + if err != nil { + log.Error().Err(err).Send() + return http.StatusInternalServerError, errors.New(internalServerErrorMsg) + } + + vms := getDurationVMs(quota, neededQuotaDuration) + err = d.db.UpdateUserQuotaVMs(quota.ID, neededQuotaDuration, vms-neededQuota) if err == gorm.ErrRecordNotFound { return http.StatusNotFound, errors.New("User quota is not found") } @@ -162,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/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/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 7b11f7f2..cea2e32e 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,8 +68,9 @@ 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"). @@ -162,7 +163,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 @@ -217,14 +217,26 @@ func (d *DB) CreateQuota(q *Quota) error { } // UpdateUserQuota updates quota -func (d *DB) UpdateUserQuota(userID string, vms 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, 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.db.Create(&QuotaVM{QuotaID: quotaID, Duration: duration, VMs: vms}).Error + } + return query.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) + 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 c831abc0..6b0a6ef5 100644 --- a/server/models/database_test.go +++ b/server/models/database_test.go @@ -116,7 +116,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 +141,6 @@ func TestGetCodeByEmail(t *testing.T) { assert.NoError(t, err) assert.Equal(t, code, user.Code) }) - } func TestUpdatePassword(t *testing.T) { @@ -232,6 +230,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 +346,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 +374,6 @@ func TestGetAllVMs(t *testing.T) { assert.Equal(t, vms, []VM{vm3}) assert.NoError(t, err) }) - } func TestAvailableVMName(t *testing.T) { @@ -393,7 +392,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 +402,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 +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", 5, 0) + err := db.UpdateUserQuota("user", 0) assert.NoError(t, err) }) t.Run("quota found", func(t *testing.T) { @@ -496,28 +493,37 @@ func TestUpdateUserQuota(t *testing.T) { err = db.CreateQuota("a2) assert.NoError(t, err) - err = db.UpdateUserQuota("user", 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.db.First(&q, "user_id = 'new-user'").Error + err = db.UpdateUserQuotaVMs(q.ID, 1, 5) assert.NoError(t, err) - assert.Equal(t, q.Vms, 0) + err = db.db.Preload("QuotaVMs").First(&q, "user_id = 'user'").Error + assert.NoError(t, err) + 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 + assert.NoError(t, err) + 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", 0, 0) + err = db.UpdateUserQuota("1", 0) assert.NoError(t, err) }) } + func TestGetUserQuota(t *testing.T) { db := setupDB(t) t.Run("quota not found", func(t *testing.T) { @@ -525,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) @@ -570,6 +576,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 +700,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 +734,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 +780,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 +822,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 +901,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 +917,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/k8s.go b/server/models/k8s.go index c6347623..c1a84f6d 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"` + ExpiresAt time.Time `json:"expires_at"` } // Master struct for kubernetes master data diff --git a/server/models/quota.go b/server/models/quota.go index 35a1c934..46f44828 100644 --- a/server/models/quota.go +++ b/server/models/quota.go @@ -1,9 +1,26 @@ // Package models for database models package models +import ( + "github.com/google/uuid" + "gorm.io/gorm" +) + // 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"` + ID string `gorm:"primary_key; unique; column:id"` + UserID string `json:"user_id"` + PublicIPs int `json:"public_ips"` + QuotaVMs []QuotaVM `json:"vms" gorm:"foreignKey:quota_id"` +} + +// 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.String() + return } diff --git a/server/models/quota_vms.go b/server/models/quota_vms.go new file mode 100644 index 00000000..a319f4ab --- /dev/null +++ b/server/models/quota_vms.go @@ -0,0 +1,9 @@ +// Package models for database models +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" gorm:"unique"` +} diff --git a/server/models/vm.go b/server/models/vm.go index 762e7bda..cdd0751f 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"` + 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 8c3c8f56..700da55a 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"` + VoucherDurationInMonth int `json:"voucher_duration_in_month" binding:"required"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` }