diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 92d08285..bc6431b4 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,17 +1,25 @@
-# See GitHub's docs for more information on this file:
-# https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
# Maintain dependencies for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
- # Check for updates to GitHub Actions every weekday
interval: "daily"
# Maintain dependencies for Go modules
- package-ecosystem: "gomod"
directory: "/server"
schedule:
- # Check for updates to Go modules every weekday
+ interval: "daily"
+
+ # Maintain dependencies for npm
+ - package-ecosystem: "npm"
+ directory: "/frontend"
+ schedule:
+ interval: "daily"
+
+ # Maintain dependencies for Composer
+ - package-ecosystem: "composer"
+ directory: "/frontend"
+ schedule:
interval: "daily"
diff --git a/server/app/app.go b/server/app/app.go
index 911bc626..f493a696 100644
--- a/server/app/app.go
+++ b/server/app/app.go
@@ -13,16 +13,19 @@ import (
"github.com/gorilla/mux"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
+ "github.com/stripe/stripe-go/v74"
+ "github.com/threefoldtech/tfgrid-sdk-go/grid-client/calculator"
"github.com/threefoldtech/tfgrid-sdk-go/grid-client/deployer"
)
// App for all dependencies of backend server
type App struct {
- config internal.Configuration
- server server
- db models.DB
- redis streams.RedisClient
- deployer c4sDeployer.Deployer
+ config internal.Configuration
+ server server
+ db models.DB
+ redis streams.RedisClient
+ deployer c4sDeployer.Deployer
+ calculator calculator.Calculator
}
// NewApp creates new server app all configurations
@@ -63,16 +66,19 @@ func NewApp(ctx context.Context, configFile string) (app *App, err error) {
}
return &App{
- config: config,
- server: *server,
- db: db,
- redis: redis,
- deployer: newDeployer,
+ config: config,
+ server: *server,
+ db: db,
+ redis: redis,
+ deployer: newDeployer,
+ calculator: tfPluginClient.Calculator,
}, nil
}
// Start starts the app
func (a *App) Start(ctx context.Context) (err error) {
+ stripe.Key = a.config.StripeSecret
+
a.registerHandlers()
a.startBackgroundWorkers(ctx)
@@ -83,6 +89,9 @@ func (a *App) startBackgroundWorkers(ctx context.Context) {
// notify admins
go a.notifyAdmins()
+ // notify expired packages
+ go a.notifyUsersExpiredPackages()
+
// periodic deployments
go a.deployer.PeriodicRequests(ctx, substrateBlockDiffInSeconds)
go a.deployer.PeriodicDeploy(ctx, substrateBlockDiffInSeconds)
@@ -102,10 +111,11 @@ func (a *App) registerHandlers() {
// sub routes with authorization
userRouter := authRouter.PathPrefix("/user").Subrouter()
- quotaRouter := authRouter.PathPrefix("/quota").Subrouter()
notificationRouter := authRouter.PathPrefix("/notification").Subrouter()
vmRouter := authRouter.PathPrefix("/vm").Subrouter()
k8sRouter := authRouter.PathPrefix("/k8s").Subrouter()
+ pkgRouter := authRouter.PathPrefix("/package").Subrouter()
+ balanceRouter := authRouter.PathPrefix("/balance").Subrouter()
// sub routes with no authorization
unAuthUserRouter := versionRouter.PathPrefix("/user").Subrouter()
@@ -114,7 +124,6 @@ func (a *App) registerHandlers() {
// sub routes with admin access
voucherRouter := adminRouter.PathPrefix("/voucher").Subrouter()
maintenanceRouter := adminRouter.PathPrefix("/maintenance").Subrouter()
- balanceRouter := adminRouter.PathPrefix("/balance").Subrouter()
unAuthUserRouter.HandleFunc("/signup", WrapFunc(a.SignUpHandler)).Methods("POST", "OPTIONS")
unAuthUserRouter.HandleFunc("/signup/verify_email", WrapFunc(a.VerifySignUpCodeHandler)).Methods("POST", "OPTIONS")
@@ -129,31 +138,37 @@ func (a *App) registerHandlers() {
userRouter.HandleFunc("/apply_voucher", WrapFunc(a.ApplyForVoucherHandler)).Methods("POST", "OPTIONS")
userRouter.HandleFunc("/activate_voucher", WrapFunc(a.ActivateVoucherHandler)).Methods("PUT", "OPTIONS")
- quotaRouter.HandleFunc("", WrapFunc(a.GetQuotaHandler)).Methods("GET", "OPTIONS")
-
notificationRouter.HandleFunc("", WrapFunc(a.ListNotificationsHandler)).Methods("GET", "OPTIONS")
notificationRouter.HandleFunc("/{id}", WrapFunc(a.UpdateNotificationsHandler)).Methods("PUT", "OPTIONS")
vmRouter.HandleFunc("", WrapFunc(a.DeployVMHandler)).Methods("POST", "OPTIONS")
- vmRouter.HandleFunc("/validate/{name}", WrapFunc(a.ValidateVMNameHandler)).Methods("Get", "OPTIONS")
+ vmRouter.HandleFunc("/validate/{name}", WrapFunc(a.ValidateVMNameHandler)).Methods("GET", "OPTIONS")
vmRouter.HandleFunc("/{id}", WrapFunc(a.GetVMHandler)).Methods("GET", "OPTIONS")
vmRouter.HandleFunc("/{id}", WrapFunc(a.DeleteVMHandler)).Methods("DELETE", "OPTIONS")
vmRouter.HandleFunc("", WrapFunc(a.ListVMsHandler)).Methods("GET", "OPTIONS")
vmRouter.HandleFunc("", WrapFunc(a.DeleteAllVMsHandler)).Methods("DELETE", "OPTIONS")
k8sRouter.HandleFunc("", WrapFunc(a.K8sDeployHandler)).Methods("POST", "OPTIONS")
- k8sRouter.HandleFunc("/validate/{name}", WrapFunc(a.ValidateK8sNameHandler)).Methods("Get", "OPTIONS")
+ k8sRouter.HandleFunc("/validate/{name}", WrapFunc(a.ValidateK8sNameHandler)).Methods("GET", "OPTIONS")
k8sRouter.HandleFunc("/{id}", WrapFunc(a.K8sGetHandler)).Methods("GET", "OPTIONS")
k8sRouter.HandleFunc("/{id}", WrapFunc(a.K8sDeleteHandler)).Methods("DELETE", "OPTIONS")
k8sRouter.HandleFunc("", WrapFunc(a.K8sGetAllHandler)).Methods("GET", "OPTIONS")
k8sRouter.HandleFunc("", WrapFunc(a.K8sDeleteAllHandler)).Methods("DELETE", "OPTIONS")
+ balanceRouter.HandleFunc("/charge", WrapFunc(a.chargeBalanceHandler)).Methods("POST", "OPTIONS")
+ balanceRouter.HandleFunc("/charged", WrapFunc(a.balanceChargedHandler)).Methods("POST", "OPTIONS")
+ balanceRouter.HandleFunc("", WrapFunc(a.getBalanceHandler)).Methods("GET", "OPTIONS")
+
+ pkgRouter.HandleFunc("/buy", WrapFunc(a.buyPackageHandler)).Methods("POST", "OPTIONS")
+ pkgRouter.HandleFunc("/renew", WrapFunc(a.renewPackageHandler)).Methods("PUT", "OPTIONS")
+ pkgRouter.HandleFunc("/", WrapFunc(a.listPackagesHandler)).Methods("GET", "OPTIONS")
+
unAuthMaintenanceRouter.HandleFunc("", WrapFunc(a.GetMaintenanceHandler)).Methods("GET", "OPTIONS")
// ADMIN ACCESS
adminRouter.HandleFunc("/user/all", WrapFunc(a.GetAllUsersHandler)).Methods("GET", "OPTIONS")
adminRouter.HandleFunc("/deployment/count", WrapFunc(a.GetDlsCountHandler)).Methods("GET", "OPTIONS")
- balanceRouter.HandleFunc("", WrapFunc(a.GetBalanceHandler)).Methods("GET", "OPTIONS")
+ adminRouter.HandleFunc("/balance/tft", WrapFunc(a.GetBalanceHandler)).Methods("GET", "OPTIONS")
maintenanceRouter.HandleFunc("", WrapFunc(a.UpdateMaintenanceHandler)).Methods("PUT", "OPTIONS")
voucherRouter.HandleFunc("", WrapFunc(a.GenerateVoucherHandler)).Methods("POST", "OPTIONS")
diff --git a/server/app/balance.go b/server/app/balance.go
new file mode 100644
index 00000000..7201ad05
--- /dev/null
+++ b/server/app/balance.go
@@ -0,0 +1,30 @@
+// Package app for c4s backend app
+package app
+
+import (
+ "github.com/stripe/stripe-go/v74"
+ "github.com/stripe/stripe-go/v74/price"
+ "github.com/stripe/stripe-go/v74/product"
+)
+
+// CreateBalanceProductInStripe creates a new stripe product for balance
+func createBalanceProductInStripe(balance int64) (string, error) {
+ params := &stripe.ProductParams{Name: stripe.String("user balance")}
+ prod, err := product.New(params)
+ if err != nil {
+ return "", err
+ }
+
+ paramsPrice := &stripe.PriceParams{
+ Product: stripe.String(prod.ID),
+ UnitAmount: stripe.Int64(balance),
+ Currency: stripe.String(string(stripe.CurrencyUSD)),
+ }
+
+ priceObj, err := price.New(paramsPrice)
+ if err != nil {
+ return "", err
+ }
+
+ return priceObj.ID, nil
+}
diff --git a/server/app/k8s_handler.go b/server/app/k8s_handler.go
index 3ae5e66e..eb09beb3 100644
--- a/server/app/k8s_handler.go
+++ b/server/app/k8s_handler.go
@@ -43,18 +43,18 @@ func (a *App) K8sDeployHandler(req *http.Request) (interface{}, Response) {
return nil, BadRequest(errors.New("invalid kubernetes data"))
}
- // quota verification
- quota, err := a.db.GetUserQuota(user.ID.String())
+ // balance verification
+ balance, err := a.db.GetBalanceByUserID(user.ID.String())
if err == gorm.ErrRecordNotFound {
log.Error().Err(err).Send()
- return nil, NotFound(errors.New("user quota is not found"))
+ return nil, NotFound(errors.New("balance is not found"))
}
if err != nil {
log.Error().Err(err).Send()
return nil, InternalServerError(errors.New(internalServerErrorMsg))
}
- _, err = deployer.ValidateK8sQuota(k8sDeployInput, quota.Vms, quota.PublicIPs)
+ err = deployer.ValidateK8sQuota(k8sDeployInput, balance)
if err != nil {
log.Error().Err(err).Send()
return nil, BadRequest(errors.New(err.Error()))
@@ -75,7 +75,7 @@ func (a *App) K8sDeployHandler(req *http.Request) (interface{}, Response) {
return nil, BadRequest(errors.New("kubernetes master name is not available, please choose a different name"))
}
- err = a.deployer.Redis.PushK8sRequest(streams.K8sDeployRequest{User: user, Input: k8sDeployInput, AdminSSHKey: a.config.AdminSSHKey})
+ err = a.deployer.Redis.PushK8sRequest(streams.K8sDeployRequest{User: user, Input: k8sDeployInput, AdminSSHKey: a.config.AdminSSHKey, ExpirationToleranceInDays: a.config.ExpirationToleranceInDays})
if err != nil {
log.Error().Err(err).Send()
return nil, InternalServerError(errors.New(internalServerErrorMsg))
diff --git a/server/app/payment_handler.go b/server/app/payment_handler.go
new file mode 100644
index 00000000..b1a660ed
--- /dev/null
+++ b/server/app/payment_handler.go
@@ -0,0 +1,458 @@
+// Package app for c4s backend app
+package app
+
+import (
+ "encoding/json"
+ "errors"
+ "net/http"
+ "time"
+
+ "github.com/codescalers/cloud4students/internal"
+ "github.com/codescalers/cloud4students/middlewares"
+ "github.com/codescalers/cloud4students/models"
+ "github.com/rs/zerolog/log"
+ "github.com/stripe/stripe-go/v74"
+ "github.com/stripe/stripe-go/v74/checkout/session"
+ "gopkg.in/validator.v2"
+ "gorm.io/gorm"
+)
+
+// BalanceChargedInput struct for data needed when charging balance
+type BalanceChargedInput struct {
+ Balance uint64 `json:"balance" binding:"required"`
+}
+
+// ChargeBalanceInput struct for data needed when charging balance
+type ChargeBalanceInput struct {
+ Balance int64 `json:"balance" binding:"required"`
+
+ SuccessURL string `json:"success_url" binding:"required"`
+ FailedURL string `json:"failure_url" binding:"required"`
+}
+
+// BuyPackageInput for data needed when buying package
+type BuyPackageInput struct {
+ Vms int `json:"vms" binding:"required"`
+ PublicIPs int `json:"public_ips" binding:"required"`
+ VMType models.VMType `json:"vm_type" binding:"required"`
+ PeriodInMonth int `json:"period" binding:"required"`
+}
+
+// RenewPackageInput for data needed when renewing package
+type RenewPackageInput struct {
+ ID int `json:"id" binding:"required"`
+}
+
+func (a *App) getBalanceHandler(req *http.Request) (interface{}, Response) {
+ userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string)
+ balance, err := a.db.GetBalanceByUserID(userID)
+ if err == gorm.ErrRecordNotFound {
+ return nil, NotFound(errors.New("user balance is not found"))
+ }
+ if err != nil {
+ log.Error().Err(err).Send()
+ return nil, InternalServerError(errors.New(internalServerErrorMsg))
+ }
+
+ return ResponseMsg{
+ Message: "Balance exists",
+ Data: map[string]interface{}{"balance": balance},
+ }, Ok()
+}
+
+func (a *App) chargeBalanceHandler(req *http.Request) (interface{}, Response) {
+ var input ChargeBalanceInput
+ err := json.NewDecoder(req.Body).Decode(&input)
+ if err != nil {
+ log.Error().Err(err).Send()
+ return nil, BadRequest(errors.New("failed to read input data"))
+ }
+
+ err = validator.Validate(input)
+ if err != nil {
+ log.Error().Err(err).Send()
+ return nil, BadRequest(errors.New("invalid input data"))
+ }
+
+ priceID, err := createBalanceProductInStripe(input.Balance * 100)
+ if err != nil {
+ log.Error().Err(err).Send()
+ return nil, InternalServerError(errors.New(internalServerErrorMsg))
+ }
+
+ paramsCheckout := &stripe.CheckoutSessionParams{
+ LineItems: []*stripe.CheckoutSessionLineItemParams{
+ {
+ Price: stripe.String(priceID),
+ Quantity: stripe.Int64(1),
+ },
+ },
+ Mode: stripe.String(string(stripe.CheckoutSessionModePayment)),
+ SuccessURL: stripe.String(input.SuccessURL),
+ CancelURL: stripe.String(input.FailedURL),
+ }
+
+ s, err := session.New(paramsCheckout)
+ if err != nil {
+ log.Error().Err(err).Send()
+ return nil, InternalServerError(errors.New(internalServerErrorMsg))
+ }
+
+ return ResponseMsg{
+ Message: "Redirect",
+ Data: s.URL,
+ }, Ok()
+}
+
+func (a *App) balanceChargedHandler(req *http.Request) (interface{}, Response) {
+ userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string)
+
+ var input BalanceChargedInput
+ err := json.NewDecoder(req.Body).Decode(&input)
+ if err != nil {
+ log.Error().Err(err).Send()
+ return nil, BadRequest(errors.New("failed to read data"))
+ }
+
+ err = validator.Validate(input)
+ if err != nil {
+ log.Error().Err(err).Send()
+ return nil, BadRequest(errors.New("invalid data"))
+ }
+
+ if input.Balance == 0 {
+ return ResponseMsg{
+ Message: "Balance has no change",
+ Data: nil,
+ }, Ok()
+ }
+
+ balance, err := a.db.GetBalanceByUserID(userID)
+ if err == gorm.ErrRecordNotFound {
+ return nil, NotFound(errors.New("user balance is not found"))
+ }
+ if err != nil {
+ log.Error().Err(err).Send()
+ return nil, InternalServerError(errors.New(internalServerErrorMsg))
+ }
+
+ balance.BalanceInUSD += input.Balance
+ if balance.Leftover > 0 {
+ if balance.Leftover >= input.Balance {
+ balance.Leftover -= input.Balance
+ } else {
+ balance.BalanceInUSD += input.Balance - balance.Leftover
+ balance.Leftover = 0
+ }
+ }
+
+ err = a.db.UpdateBalance(balance)
+ if err != nil {
+ log.Error().Err(err).Send()
+ return nil, InternalServerError(errors.New(internalServerErrorMsg))
+ }
+
+ return ResponseMsg{
+ Message: "Balance is updated successfully",
+ Data: nil,
+ }, Ok()
+}
+
+func (a *App) buyPackageHandler(req *http.Request) (interface{}, Response) {
+ userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string)
+
+ var input BuyPackageInput
+ err := json.NewDecoder(req.Body).Decode(&input)
+ if err != nil {
+ log.Error().Err(err).Send()
+ return nil, BadRequest(errors.New("failed to read input data"))
+ }
+
+ err = validator.Validate(input)
+ if err != nil {
+ log.Error().Err(err).Send()
+ return nil, BadRequest(errors.New("invalid input data"))
+ }
+
+ res := a.activatePackage(userID, input.VMType, input.Vms, input.PublicIPs, input.PeriodInMonth, false)
+ if res != nil {
+ return nil, res
+ }
+
+ return ResponseMsg{
+ Message: "Package is bought successfully",
+ Data: nil,
+ }, Ok()
+}
+
+func (a *App) renewPackageHandler(req *http.Request) (interface{}, Response) {
+ userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string)
+
+ var input RenewPackageInput
+ err := json.NewDecoder(req.Body).Decode(&input)
+ if err != nil {
+ log.Error().Err(err).Send()
+ return nil, BadRequest(errors.New("failed to read input data"))
+ }
+
+ err = validator.Validate(input)
+ if err != nil {
+ log.Error().Err(err).Send()
+ return nil, BadRequest(errors.New("invalid input data"))
+ }
+
+ pkg, err := a.db.GetPackage(input.ID)
+ if err == gorm.ErrRecordNotFound {
+ return nil, NotFound(errors.New("package is not found"))
+ }
+ if err != nil {
+ log.Error().Err(err).Send()
+ return nil, InternalServerError(errors.New(internalServerErrorMsg))
+ }
+
+ balance, err := a.db.GetBalanceByUserID(userID)
+ if err == gorm.ErrRecordNotFound {
+ return nil, NotFound(errors.New("user is not found"))
+ }
+ if err != nil {
+ log.Error().Err(err).Send()
+ return nil, InternalServerError(errors.New(internalServerErrorMsg))
+ }
+
+ if balance.BalanceInUSD < pkg.Cost {
+ return nil, BadRequest(errors.New("balance is not enough, please recharge your balance"))
+ }
+
+ pkg.PeriodInMonth *= 2
+ err = a.db.UpdatePackage(pkg)
+ if err != nil {
+ log.Error().Err(err).Send()
+ return nil, InternalServerError(errors.New(internalServerErrorMsg))
+ }
+
+ switch pkg.VMType {
+ case models.Small:
+ balance.SmallVMsWithPublicIP += pkg.PublicIPs
+ balance.SmallVMs += pkg.Vms - pkg.PublicIPs
+ case models.Medium:
+ balance.MediumVMsWithPublicIP += pkg.PublicIPs
+ balance.MediumVMs += pkg.Vms - pkg.PublicIPs
+ case models.Large:
+ balance.LargeVMsWithPublicIP += pkg.PublicIPs
+ balance.LargeVMs += pkg.Vms - pkg.PublicIPs
+ }
+
+ balance.BalanceInUSD -= pkg.Cost
+ err = a.db.UpdateBalance(balance)
+ if err != nil {
+ log.Error().Err(err).Send()
+ return nil, InternalServerError(errors.New(internalServerErrorMsg))
+ }
+
+ return ResponseMsg{
+ Message: "Package is renewed successfully",
+ Data: nil,
+ }, Ok()
+}
+
+func (a *App) listPackagesHandler(req *http.Request) (interface{}, Response) {
+ userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string)
+
+ packages, err := a.db.ListPackages(userID)
+ if err == gorm.ErrRecordNotFound || len(packages) == 0 {
+ return ResponseMsg{
+ Message: "no packages found",
+ Data: packages,
+ }, Ok()
+ }
+ if err != nil {
+ log.Error().Err(err).Send()
+ return nil, InternalServerError(errors.New(internalServerErrorMsg))
+ }
+
+ return ResponseMsg{
+ Message: "Packages are found",
+ Data: packages,
+ }, Ok()
+}
+
+func (a *App) activatePackage(userID string, vmType models.VMType, vms, publicIPs, periodInMonth int, free bool) Response {
+ if vms < publicIPs {
+ return BadRequest(errors.New("virtual machines must be greater than or equal public ips"))
+ }
+
+ pkgRealCost, pkgCost, err := a.calculatePackageCost(vms, publicIPs, periodInMonth, vmType)
+ if err != nil {
+ log.Error().Err(err).Send()
+ return InternalServerError(errors.New(internalServerErrorMsg))
+ }
+
+ pkg := models.Package{
+ UserID: userID,
+ Vms: vms,
+ PublicIPs: publicIPs,
+ PeriodInMonth: periodInMonth,
+ Cost: pkgCost,
+ RealCost: pkgRealCost,
+ CreatedAt: time.Now(),
+ VMType: vmType,
+ }
+
+ balance, err := a.db.GetBalanceByUserID(userID)
+ if err == gorm.ErrRecordNotFound {
+ return NotFound(errors.New("user balance is not found"))
+ }
+ if err != nil {
+ log.Error().Err(err).Send()
+ return InternalServerError(errors.New(internalServerErrorMsg))
+ }
+
+ if balance.BalanceInUSD < pkgCost && !free {
+ return BadRequest(errors.New("balance is not enough, please recharge your balance"))
+ }
+
+ err = a.db.CreatePackage(&pkg)
+ if err != nil {
+ log.Error().Err(err).Send()
+ return InternalServerError(errors.New(internalServerErrorMsg))
+ }
+
+ switch vmType {
+ case models.Small:
+ balance.SmallVMsWithPublicIP += publicIPs
+ balance.SmallVMs += vms - publicIPs
+ case models.Medium:
+ balance.MediumVMsWithPublicIP += publicIPs
+ balance.MediumVMs += vms - publicIPs
+ case models.Large:
+ balance.LargeVMsWithPublicIP += publicIPs
+ balance.LargeVMs += vms - publicIPs
+ }
+
+ if !free {
+ balance.BalanceInUSD -= pkgCost
+ }
+
+ err = a.db.UpdateBalance(balance)
+ if err != nil {
+ log.Error().Err(err).Send()
+ return InternalServerError(errors.New(internalServerErrorMsg))
+ }
+
+ return nil
+}
+
+func (a *App) calculatePackageCost(vms, publicIPs, periodInMonth int, vmType models.VMType) (uint64, uint64, error) {
+ var vmCPU, vmMemory, vmDisk, vmCost, vmCostWithPublicIP uint64
+ switch vmType {
+ case models.Small:
+ vmCPU = SmallCPU
+ vmMemory = SmallMemory
+ vmDisk = SmallDisk
+ vmCost = a.config.Prices.SmallVM
+ vmCostWithPublicIP = a.config.Prices.SmallVMWithPublicIP
+ case models.Medium:
+ vmCPU = MediumCPU
+ vmMemory = MediumMemory
+ vmDisk = MediumDisk
+ vmCost = a.config.Prices.MediumVM
+ vmCostWithPublicIP = a.config.Prices.MediumVMWithPublicIP
+ case models.Large:
+ vmCPU = LargeCPU
+ vmMemory = LargeMemory
+ vmDisk = LargeDisk
+ vmCost = a.config.Prices.LargeVM
+ vmCostWithPublicIP = a.config.Prices.LargeVMWithPublicIP
+ }
+
+ var pkgRealCost, pkgCost uint64
+ for i := 1; i <= vms; i++ {
+ publicIP := publicIPs > 0
+ cost, err := a.calculator.CalculateCost(int64(vmCPU)*int64(vms), int64(vmMemory)*int64(vms), 0, int64(vmDisk)*int64(vms), publicIP, false)
+ if err != nil {
+ return 0, 0, err
+ }
+
+ if publicIPs > 0 {
+ pkgCost += vmCostWithPublicIP
+ } else {
+ pkgCost += vmCost
+ }
+
+ pkgRealCost += uint64(cost * 1e7)
+ publicIPs--
+ }
+
+ pkgRealCost = pkgRealCost * uint64(periodInMonth)
+ pkgCost = pkgCost * uint64(periodInMonth)
+ return pkgRealCost, pkgCost, nil
+}
+
+func (a *App) notifyUsersExpiredPackages() {
+ ticker := time.NewTicker(24 * time.Hour * time.Duration(a.config.NotifyUsersExpirationInDays))
+
+ for range ticker.C {
+ packages, err := a.db.GetExpiredPackages(a.config.ExpirationToleranceInDays)
+ if err != nil {
+ log.Error().Err(err).Send()
+ }
+
+ for _, pkg := range packages {
+ user, err := a.db.GetUserByID(pkg.UserID)
+ if err != nil {
+ log.Error().Err(err).Send()
+ }
+
+ balance, err := a.db.GetBalanceByUserID(pkg.UserID)
+ if err != nil {
+ log.Error().Err(err).Send()
+ }
+
+ expiredAt := time.Since(pkg.CreatedAt.AddDate(0, pkg.PeriodInMonth, 0))
+ daysLeft := a.config.ExpirationToleranceInDays - int(expiredAt)
+
+ if daysLeft > 0 {
+ subject, body := internal.NotifyExpiredPackages(daysLeft, a.config.Server.Host)
+
+ err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, user.Email, subject, body)
+ if err != nil {
+ log.Error().Err(err).Send()
+ }
+
+ // add a daily leftover
+ balance.Leftover += pkg.Cost / uint64(30*pkg.PeriodInMonth)
+ err = a.db.UpdateBalance(balance)
+ if err != nil {
+ log.Error().Err(err).Send()
+ }
+ continue
+ }
+
+ // delete expired vms
+ vms, err := a.db.GetExpiredVms(user.ID.String())
+ if err != nil {
+ log.Error().Err(err).Send()
+ }
+
+ for _, vm := range vms {
+ err = a.db.DeleteVMByID(vm.ID)
+ if err != nil {
+ log.Error().Err(err).Send()
+ }
+ }
+
+ // delete expired clusters
+ clusters, err := a.db.GetExpiredK8s(user.ID.String())
+ if err != nil {
+ log.Error().Err(err).Send()
+ }
+
+ for _, k8s := range clusters {
+ err = a.db.DeleteK8s(k8s.ID)
+ if err != nil {
+ log.Error().Err(err).Send()
+ }
+ }
+ }
+ }
+}
diff --git a/server/app/quota_handler.go b/server/app/quota_handler.go
deleted file mode 100644
index 193de4e1..00000000
--- a/server/app/quota_handler.go
+++ /dev/null
@@ -1,30 +0,0 @@
-// Package app for c4s backend app
-package app
-
-import (
- "errors"
- "net/http"
-
- "github.com/codescalers/cloud4students/middlewares"
- "github.com/rs/zerolog/log"
- "gorm.io/gorm"
-)
-
-// GetQuotaHandler gets quota
-func (a *App) GetQuotaHandler(req *http.Request) (interface{}, Response) {
- userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string)
-
- quota, err := a.db.GetUserQuota(userID)
- if err == gorm.ErrRecordNotFound {
- return nil, NotFound(errors.New("user quota 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,
- }, Ok()
-}
diff --git a/server/app/quota_handler_test.go b/server/app/quota_handler_test.go
deleted file mode 100644
index 97838e35..00000000
--- a/server/app/quota_handler_test.go
+++ /dev/null
@@ -1,68 +0,0 @@
-// Package app for c4s backend app
-package app
-
-import (
- "fmt"
- "net/http"
- "testing"
-
- "github.com/codescalers/cloud4students/internal"
- "github.com/codescalers/cloud4students/models"
- "github.com/stretchr/testify/assert"
-)
-
-func TestQuotaRouter(t *testing.T) {
- app := SetUp(t)
-
- user.Verified = true
- err := app.db.CreateUser(user)
- assert.NoError(t, err)
-
- token, err := internal.CreateJWT(user.ID.String(), user.Email, app.config.Token.Secret, app.config.Token.Timeout)
- assert.NoError(t, err)
-
- t.Run("get quota: not found", func(t *testing.T) {
- req := authHandlerConfig{
- unAuthHandlerConfig: unAuthHandlerConfig{
- body: nil,
- handlerFunc: app.GetQuotaHandler,
- api: fmt.Sprintf("/%s/quota", app.config.Version),
- },
- userID: user.ID.String(),
- token: token,
- config: app.config,
- db: app.db,
- }
-
- response := authorizedHandler(req)
- want := `{"err":"user quota is not found"}` + "\n"
- assert.Equal(t, response.Body.String(), want)
- assert.Equal(t, response.Code, http.StatusNotFound)
- })
-
- t.Run("get quota: success", func(t *testing.T) {
- err = app.db.CreateQuota(
- &models.Quota{
- UserID: user.ID.String(),
- Vms: 10,
- PublicIPs: 1,
- },
- )
- assert.NoError(t, err)
-
- req := authHandlerConfig{
- unAuthHandlerConfig: unAuthHandlerConfig{
- body: nil,
- handlerFunc: app.GetQuotaHandler,
- api: fmt.Sprintf("/%s/quota", app.config.Version),
- },
- userID: user.ID.String(),
- token: token,
- config: app.config,
- db: app.db,
- }
-
- response := authorizedHandler(req)
- assert.Equal(t, response.Code, http.StatusOK)
- })
-}
diff --git a/server/app/setup.go b/server/app/setup.go
index 0d83c327..6043667e 100644
--- a/server/app/setup.go
+++ b/server/app/setup.go
@@ -68,9 +68,16 @@ func SetUp(t testing.TB) *App {
"database": {
"file": "%s"
},
- "version": "v1"
-}
- `, dbPath)
+ "version": "v1",
+ "prices": {
+ "small_vm": 5,
+ "small_vm_with_public_ip": 5,
+ "medium_vm": 5,
+ "medium_vm_with_public_ip": 5,
+ "large_vm": 5,
+ "large_vm_with_public_ip": 5
+ },
+ "stripe_secret": "secret"}`, dbPath)
err := os.WriteFile(configPath, []byte(config), 0644)
assert.NoError(t, err)
@@ -92,11 +99,12 @@ func SetUp(t testing.TB) *App {
assert.NoError(t, err)
app := &App{
- config: configuration,
- server: server{},
- db: db,
- redis: streams.RedisClient{},
- deployer: newDeployer,
+ config: configuration,
+ server: server{},
+ db: db,
+ redis: streams.RedisClient{},
+ deployer: newDeployer,
+ calculator: tfPluginClient.Calculator,
}
return app
diff --git a/server/app/user_handler.go b/server/app/user_handler.go
index 18efa7d8..5c0e9d91 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"`
+ VMType models.VMType `json:"vm_type" binding:"required" validate:"nonzero"`
+ Reason string `json:"reason" binding:"required" validate:"nonzero"`
}
// AddVoucherInput struct for voucher applied by user
@@ -159,17 +160,6 @@ func (a *App) SignUpHandler(req *http.Request) (interface{}, Response) {
log.Error().Err(err).Send()
return nil, InternalServerError(errors.New(internalServerErrorMsg))
}
-
- // create empty quota
- quota := models.Quota{
- UserID: u.ID.String(),
- Vms: 0,
- }
- err = a.db.CreateQuota("a)
- if err != nil {
- log.Error().Err(err).Send()
- return nil, InternalServerError(errors.New(internalServerErrorMsg))
- }
}
return ResponseMsg{
@@ -579,6 +569,7 @@ func (a *App) ApplyForVoucherHandler(req *http.Request) (interface{}, Response)
Voucher: v,
UserID: userID,
VMs: input.VMs,
+ VMType: input.VMType,
Reason: input.Reason,
PublicIPs: input.PublicIPs,
}
@@ -607,15 +598,6 @@ 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)
- if err == gorm.ErrRecordNotFound {
- return nil, NotFound(errors.New("user quota is not found"))
- }
- if err != nil {
- log.Error().Err(err).Send()
- return nil, InternalServerError(errors.New(internalServerErrorMsg))
- }
-
voucherQuota, err := a.db.GetVoucher(input.Voucher)
if err == gorm.ErrRecordNotFound {
return nil, NotFound(errors.New("user voucher is not found"))
@@ -643,13 +625,12 @@ 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)
- if err != nil {
- log.Error().Err(err).Send()
- return nil, InternalServerError(errors.New(internalServerErrorMsg))
+ res := a.activatePackage(userID, voucherQuota.VMType, voucherQuota.VMs, voucherQuota.PublicIPs, 1, true)
+ if res != nil {
+ return nil, res
}
- middlewares.VoucherActivated.WithLabelValues(userID, voucherQuota.Voucher, fmt.Sprint(voucherQuota.VMs), fmt.Sprint(voucherQuota.PublicIPs)).Inc()
+ middlewares.VoucherActivated.WithLabelValues(userID, voucherQuota.Voucher, fmt.Sprint(voucherQuota.VMs), fmt.Sprint(voucherQuota.PublicIPs)).Inc()
return ResponseMsg{
Message: "Voucher is applied successfully",
Data: nil,
diff --git a/server/app/user_handler_test.go b/server/app/user_handler_test.go
index 908b91c4..f4bb492b 100644
--- a/server/app/user_handler_test.go
+++ b/server/app/user_handler_test.go
@@ -872,6 +872,7 @@ func TestApplyForVoucherHandler(t *testing.T) {
voucherBody := []byte(`{
"vms":10,
"public_ips":1,
+ "vm_type": "small",
"reason":"strongReason"
}`)
@@ -948,17 +949,11 @@ func TestActivateVoucherHandler(t *testing.T) {
err := app.db.CreateUser(user)
assert.NoError(t, err)
- err = app.db.CreateQuota(
- &models.Quota{
- UserID: user.ID.String(),
- },
- )
- assert.NoError(t, err)
-
v := models.Voucher{
Voucher: "voucher",
- VMs: 10,
+ VMs: 2,
Approved: true,
+ VMType: "small",
}
err = app.db.CreateVoucher(&v)
@@ -1026,34 +1021,6 @@ func TestActivateVoucherHandler(t *testing.T) {
assert.Equal(t, response.Code, http.StatusBadRequest)
})
- t.Run("Activate voucher: user quota not found", func(t *testing.T) {
- newUser := user
- newUser.Verified = true
- newUser.Email = "test@example.com"
- err := app.db.CreateUser(newUser)
- assert.NoError(t, err)
-
- token, err := internal.CreateJWT(newUser.ID.String(), newUser.Email, app.config.Token.Secret, app.config.Token.Timeout)
- assert.NoError(t, err)
-
- req := authHandlerConfig{
- unAuthHandlerConfig: unAuthHandlerConfig{
- body: bytes.NewBuffer(voucherBody),
- handlerFunc: app.ActivateVoucherHandler,
- api: fmt.Sprintf("/%s/user/activate_voucher", app.config.Version),
- },
- userID: newUser.ID.String(),
- token: token,
- config: app.config,
- db: app.db,
- }
-
- response := authorizedHandler(req)
- want := `{"err":"user quota is not found"}` + "\n"
- assert.Equal(t, response.Body.String(), want)
- assert.Equal(t, response.Code, http.StatusNotFound)
- })
-
t.Run("Activate voucher: voucher not found", func(t *testing.T) {
body := []byte(`{"voucher" : "abcd"}`)
req := authHandlerConfig{
diff --git a/server/app/vm_handler.go b/server/app/vm_handler.go
index 17bfface..aa1db239 100644
--- a/server/app/vm_handler.go
+++ b/server/app/vm_handler.go
@@ -44,17 +44,17 @@ func (a *App) DeployVMHandler(req *http.Request) (interface{}, Response) {
return nil, BadRequest(errors.New("invalid vm data"))
}
- // check quota of user
- quota, err := a.db.GetUserQuota(user.ID.String())
+ // check balance of user
+ balance, err := a.db.GetBalanceByUserID(user.ID.String())
if err == gorm.ErrRecordNotFound {
- return nil, NotFound(errors.New("user quota is not found"))
+ return nil, NotFound(errors.New("balance is not found"))
}
if err != nil {
log.Error().Err(err).Send()
return nil, InternalServerError(errors.New(internalServerErrorMsg))
}
- _, err = deployer.ValidateVMQuota(input, quota.Vms, quota.PublicIPs)
+ err = deployer.ValidateVMQuota(input, balance)
if err != nil {
return nil, BadRequest(errors.New(err.Error()))
}
@@ -74,7 +74,7 @@ func (a *App) DeployVMHandler(req *http.Request) (interface{}, Response) {
return nil, BadRequest(errors.New("virtual machine name is not available, please choose a different name"))
}
- err = a.deployer.Redis.PushVMRequest(streams.VMDeployRequest{User: user, Input: input, AdminSSHKey: a.config.AdminSSHKey})
+ err = a.deployer.Redis.PushVMRequest(streams.VMDeployRequest{User: user, Input: input, AdminSSHKey: a.config.AdminSSHKey, ExpirationToleranceInDays: a.config.ExpirationToleranceInDays})
if err != nil {
log.Error().Err(err).Send()
return nil, InternalServerError(errors.New(internalServerErrorMsg))
diff --git a/server/app/vm_specs.go b/server/app/vm_specs.go
new file mode 100644
index 00000000..4c540c29
--- /dev/null
+++ b/server/app/vm_specs.go
@@ -0,0 +1,15 @@
+// Package app for c4s backend app
+package app
+
+// vms types
+var (
+ SmallCPU = uint64(1)
+ SmallMemory = uint64(2)
+ SmallDisk = uint64(25)
+ MediumCPU = uint64(2)
+ MediumMemory = uint64(4)
+ MediumDisk = uint64(50)
+ LargeCPU = uint64(4)
+ LargeMemory = uint64(8)
+ LargeDisk = uint64(100)
+)
diff --git a/server/deployer/deployer.go b/server/deployer/deployer.go
index 01b20bd2..1f6f5948 100644
--- a/server/deployer/deployer.go
+++ b/server/deployer/deployer.go
@@ -36,11 +36,6 @@ var (
largeMemory = uint64(8)
largeDisk = uint64(100)
- smallQuota = 1
- mediumQuota = 2
- largeQuota = 3
- publicQuota = 1
-
trueVal = true
statusUp = "up"
@@ -178,21 +173,21 @@ func buildNetwork(node uint32, name string) workloads.ZNet {
}
}
-func calcNodeResources(resources string, public bool) (uint64, uint64, uint64, uint64, error) {
+func calcNodeResources(resources models.VMType, public bool) (uint64, uint64, uint64, uint64, error) {
var cru uint64
var mru uint64
var sru uint64
var ips uint64
switch resources {
- case "small":
+ case models.Small:
cru += smallCPU
mru += smallMemory
sru += smallDisk
- case "medium":
+ case models.Medium:
cru += mediumCPU
mru += mediumMemory
sru += mediumDisk
- case "large":
+ case models.Large:
cru += largeCPU
mru += largeMemory
sru += largeDisk
@@ -206,7 +201,7 @@ func calcNodeResources(resources string, public bool) (uint64, uint64, uint64, u
}
// choose suitable nodes based on needed resources
-func filterNode(resource string, public bool) (types.NodeFilter, error) {
+func filterNode(resource models.VMType, public bool) (types.NodeFilter, error) {
cru, mru, sru, ips, err := calcNodeResources(resource, public)
if err != nil {
return types.NodeFilter{}, err
@@ -224,18 +219,25 @@ func filterNode(resource string, public bool) (types.NodeFilter, error) {
}, nil
}
-func calcNeededQuota(resources string) (int, error) {
- var neededQuota int
+func calcNeededQuota(resources models.VMType, balance models.Balance, publicIP bool) {
switch resources {
- case "small":
- neededQuota += smallQuota
- case "medium":
- neededQuota += mediumQuota
- case "large":
- neededQuota += largeQuota
- default:
- return 0, fmt.Errorf("unknown resource type %s", resources)
+ case models.Small:
+ if publicIP {
+ balance.SmallVMsWithPublicIP--
+ } else {
+ balance.SmallVMs--
+ }
+ case models.Medium:
+ if publicIP {
+ balance.MediumVMsWithPublicIP--
+ } else {
+ balance.MediumVMs--
+ }
+ case models.Large:
+ if publicIP {
+ balance.LargeVMsWithPublicIP--
+ } else {
+ balance.LargeVMs--
+ }
}
-
- return neededQuota, nil
}
diff --git a/server/deployer/deployment_consumer.go b/server/deployer/deployment_consumer.go
index 42985fc7..0cf905f4 100644
--- a/server/deployer/deployment_consumer.go
+++ b/server/deployer/deployment_consumer.go
@@ -47,7 +47,7 @@ func (d *Deployer) ConsumeVMRequest(ctx context.Context, pending bool) {
continue
}
- codeErr, resErr = d.deployVMRequest(ctx, req.User, req.Input, req.AdminSSHKey)
+ codeErr, resErr = d.deployVMRequest(ctx, req.User, req.Input, req.AdminSSHKey, req.ExpirationToleranceInDays)
if resErr != nil {
log.Error().Err(resErr).Msg("failed to deploy vm request")
continue
@@ -111,7 +111,7 @@ func (d *Deployer) ConsumeK8sRequest(ctx context.Context, pending bool) {
continue
}
- codeErr, resErr = d.deployK8sRequest(ctx, req.User, req.Input, req.AdminSSHKey)
+ codeErr, resErr = d.deployK8sRequest(ctx, req.User, req.Input, req.AdminSSHKey, req.ExpirationToleranceInDays)
if resErr != nil {
log.Error().Err(resErr).Msg("failed to deploy k8s request")
continue
diff --git a/server/deployer/k8s_deployer.go b/server/deployer/k8s_deployer.go
index 5be19a95..0058be01 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"
@@ -110,7 +111,7 @@ func (d *Deployer) deployK8sClusterWithNetwork(ctx context.Context, k8sDeployInp
return node, loadedNet.NodeDeploymentID[node], loadedCluster.NodeDeploymentID[node], nil
}
-func (d *Deployer) loadK8s(k8sDeployInput models.K8sDeployInput, userID string, node uint32, networkContractID uint64, k8sContractID uint64) (models.K8sCluster, error) {
+func (d *Deployer) loadK8s(k8sDeployInput models.K8sDeployInput, userID string, node uint32, networkContractID uint64, k8sContractID uint64, expirationToleranceInDays int) (models.K8sCluster, error) {
// load cluster
resCluster, err := d.tfPluginClient.State.LoadK8sFromGrid([]uint32{node}, k8sDeployInput.MasterName)
if err != nil {
@@ -130,12 +131,12 @@ func (d *Deployer) loadK8s(k8sDeployInput models.K8sDeployInput, userID string,
PublicIP: resCluster.Master.ComputedIP,
Name: k8sDeployInput.MasterName,
YggIP: resCluster.Master.YggIP,
- Resources: k8sDeployInput.Resources,
+ Resources: string(k8sDeployInput.Resources),
}
workers := []models.Worker{}
for _, worker := range k8sDeployInput.Workers {
- cru, mru, sru, _, err := calcNodeResources(worker.Resources, false)
+ cru, mru, sru, _, err := calcNodeResources(models.VMType(worker.Resources), false)
if err != nil {
return models.K8sCluster{}, err
}
@@ -148,12 +149,20 @@ func (d *Deployer) loadK8s(k8sDeployInput models.K8sDeployInput, userID string,
}
workers = append(workers, workerModel)
}
+
+ pkg, err := d.db.GetPackage(k8sDeployInput.PkgID)
+ if err != nil {
+ return models.K8sCluster{}, err
+ }
+
k8sCluster := models.K8sCluster{
UserID: userID,
NetworkContract: int(networkContractID),
ClusterContract: int(k8sContractID),
Master: master,
Workers: workers,
+ CreatedAt: time.Now(),
+ ExpiresAt: time.Now().AddDate(0, pkg.PeriodInMonth, expirationToleranceInDays),
}
return k8sCluster, nil
@@ -166,7 +175,7 @@ func (d *Deployer) getK8sAvailableNode(ctx context.Context, k models.K8sDeployIn
}
for _, worker := range k.Workers {
- _, m, s, _, err := calcNodeResources(worker.Resources, false)
+ _, m, s, _, err := calcNodeResources(models.VMType(worker.Resources), false)
if err != nil {
return 0, err
}
@@ -194,43 +203,32 @@ 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) {
- neededQuota, err := calcNeededQuota(k.Resources)
- if err != nil {
- return 0, err
- }
+func ValidateK8sQuota(k models.K8sDeployInput, balance models.Balance) error {
+ calcNeededQuota(k.Resources, balance, k.Public)
for _, worker := range k.Workers {
- workerQuota, err := calcNeededQuota(worker.Resources)
- if err != nil {
- return 0, err
- }
- neededQuota += workerQuota
+ calcNeededQuota(worker.Resources, balance, false)
}
- if availableResourcesQuota < neededQuota {
- return 0, fmt.Errorf("no available quota %d for kubernetes deployment, you can request a new voucher", availableResourcesQuota)
+ if balance.SmallVMs < 0 || balance.MediumVMs < 0 || balance.LargeVMs < 0 {
+ return fmt.Errorf("no available quota `%s vm for a master and %d vms for workers` for kubernetes deployment, you can buy a new package", k.Resources, len(k.Workers))
}
- if k.Public && availablePublicIPsQuota < publicQuota {
- return 0, fmt.Errorf("no available quota %d for public ips", availablePublicIPsQuota)
+
+ if balance.SmallVMsWithPublicIP < 0 || balance.MediumVMsWithPublicIP < 0 || balance.LargeVMsWithPublicIP < 0 {
+ return errors.New("no available quota for public ips")
}
- return neededQuota, nil
+ return nil
}
-func (d *Deployer) deployK8sRequest(ctx context.Context, user models.User, k8sDeployInput models.K8sDeployInput, adminSSHKey string) (int, error) {
- // quota verification
- quota, err := d.db.GetUserQuota(user.ID.String())
- if err == gorm.ErrRecordNotFound {
- log.Error().Err(err).Send()
- return http.StatusNotFound, errors.New("user quota is not found")
- }
+func (d *Deployer) deployK8sRequest(ctx context.Context, user models.User, k8sDeployInput models.K8sDeployInput, adminSSHKey string, expirationToleranceInDays int) (int, error) {
+ balance, err := d.db.GetBalanceByUserID(user.ID.String())
if err != nil {
log.Error().Err(err).Send()
return http.StatusInternalServerError, errors.New(internalServerErrorMsg)
}
- neededQuota, err := ValidateK8sQuota(k8sDeployInput, quota.Vms, quota.PublicIPs)
+ err = ValidateK8sQuota(k8sDeployInput, balance)
if err != nil {
log.Error().Err(err).Send()
return http.StatusBadRequest, err
@@ -243,19 +241,16 @@ func (d *Deployer) deployK8sRequest(ctx context.Context, user models.User, k8sDe
return http.StatusInternalServerError, errors.New(internalServerErrorMsg)
}
- k8sCluster, err := d.loadK8s(k8sDeployInput, user.ID.String(), node, networkContractID, k8sContractID)
+ k8sCluster, err := d.loadK8s(k8sDeployInput, user.ID.String(), node, networkContractID, k8sContractID, expirationToleranceInDays)
if err != nil {
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)
+
+ // update balance
+ err = d.db.UpdateBalanceQuota(user.ID.String(), balance)
if err == gorm.ErrRecordNotFound {
- return http.StatusNotFound, errors.New("user quota is not found")
+ return http.StatusNotFound, errors.New("user balance is not found")
}
if err != nil {
log.Error().Err(err).Send()
@@ -269,9 +264,9 @@ func (d *Deployer) deployK8sRequest(ctx context.Context, user models.User, k8sDe
}
// metrics
- middlewares.Deployments.WithLabelValues(user.ID.String(), k8sDeployInput.Resources, "master").Inc()
+ middlewares.Deployments.WithLabelValues(user.ID.String(), string(k8sDeployInput.Resources), "master").Inc()
for _, worker := range k8sDeployInput.Workers {
- middlewares.Deployments.WithLabelValues(user.ID.String(), worker.Resources, "worker").Inc()
+ middlewares.Deployments.WithLabelValues(user.ID.String(), string(worker.Resources), "worker").Inc()
}
return 0, nil
diff --git a/server/deployer/vms_deployer.go b/server/deployer/vms_deployer.go
index 2860708a..ea909a27 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,34 +88,28 @@ 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) {
- neededQuota, err := calcNeededQuota(vm.Resources)
- if err != nil {
- return 0, err
- }
+func ValidateVMQuota(vm models.DeployVMInput, balance models.Balance) error {
+ calcNeededQuota(vm.Resources, balance, vm.Public)
- 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 balance.SmallVMs < 0 || balance.MediumVMs < 0 || balance.LargeVMs < 0 {
+ return fmt.Errorf("no available quota `%s vm` for deployment, you can buy a new package", vm.Resources)
}
- if vm.Public && availablePublicIPsQuota < publicQuota {
- return 0, fmt.Errorf("no available quota %d for public ips", availablePublicIPsQuota)
+
+ if balance.SmallVMsWithPublicIP < 0 || balance.MediumVMsWithPublicIP < 0 || balance.LargeVMsWithPublicIP < 0 {
+ return errors.New("no available quota for public ips")
}
- return neededQuota, nil
+ return nil
}
-func (d *Deployer) deployVMRequest(ctx context.Context, user models.User, input models.DeployVMInput, adminSSHKey string) (int, error) {
- // check quota of user
- quota, err := d.db.GetUserQuota(user.ID.String())
- if err == gorm.ErrRecordNotFound {
- return http.StatusNotFound, errors.New("user quota is not found")
- }
+func (d *Deployer) deployVMRequest(ctx context.Context, user models.User, input models.DeployVMInput, adminSSHKey string, expirationToleranceInDays int) (int, error) {
+ balance, err := d.db.GetBalanceByUserID(user.ID.String())
if err != nil {
log.Error().Err(err).Send()
return http.StatusInternalServerError, errors.New(internalServerErrorMsg)
}
- neededQuota, err := ValidateVMQuota(input, quota.Vms, quota.PublicIPs)
+ err = ValidateVMQuota(input, balance)
if err != nil {
return http.StatusBadRequest, err
}
@@ -125,6 +120,12 @@ func (d *Deployer) deployVMRequest(ctx context.Context, user models.User, input
return http.StatusInternalServerError, errors.New(internalServerErrorMsg)
}
+ pkg, err := d.db.GetPackage(input.PkgID)
+ 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 +138,8 @@ func (d *Deployer) deployVMRequest(ctx context.Context, user models.User, input
MRU: uint64(vm.Memory),
ContractID: contractID,
NetworkContractID: networkContractID,
+ CreatedAt: time.Now(),
+ ExpiresAt: time.Now().AddDate(0, pkg.PeriodInMonth, expirationToleranceInDays),
}
err = d.db.CreateVM(&userVM)
@@ -145,20 +148,16 @@ func (d *Deployer) deployVMRequest(ctx context.Context, user models.User, input
return http.StatusInternalServerError, errors.New(internalServerErrorMsg)
}
- publicIPsQuota := quota.PublicIPs
- if input.Public {
- publicIPsQuota -= publicQuota
- }
- // update quota of user
- err = d.db.UpdateUserQuota(user.ID.String(), quota.Vms-neededQuota, publicIPsQuota)
+ // update balance
+ err = d.db.UpdateBalanceQuota(user.ID.String(), balance)
if err == gorm.ErrRecordNotFound {
- return http.StatusNotFound, errors.New("User quota is not found")
+ return http.StatusNotFound, errors.New("user balance 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()
+ middlewares.Deployments.WithLabelValues(user.ID.String(), string(input.Resources), "vm").Inc()
return 0, nil
}
diff --git a/server/go.mod b/server/go.mod
index d990dc9e..fb7323e9 100644
--- a/server/go.mod
+++ b/server/go.mod
@@ -14,6 +14,7 @@ require (
github.com/sendgrid/sendgrid-go v3.12.0+incompatible
github.com/spf13/cobra v1.7.0
github.com/stretchr/testify v1.8.4
+ github.com/stripe/stripe-go/v74 v74.26.0
github.com/threefoldtech/tfgrid-sdk-go/grid-client v0.10.2
github.com/threefoldtech/tfgrid-sdk-go/grid-proxy v0.10.2
github.com/threefoldtech/zos v0.5.6-0.20230526112430-f620733482d7
@@ -53,7 +54,7 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
- github.com/mattn/go-isatty v0.0.18 // indirect
+ github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b // indirect
@@ -73,7 +74,8 @@ require (
github.com/tyler-smith/go-bip39 v1.1.0 // indirect
github.com/vedhavyas/go-subkey v1.0.3 // indirect
golang.org/x/exp v0.0.0-20230206171751-46f607a40771 // indirect
- golang.org/x/sync v0.2.0 // indirect
+ golang.org/x/net v0.12.0 // indirect
+ golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200609130330-bd2cb7843e1b // indirect
google.golang.org/protobuf v1.30.0 // indirect
diff --git a/server/go.sum b/server/go.sum
index 4bd86b94..14ec99d9 100644
--- a/server/go.sum
+++ b/server/go.sum
@@ -110,8 +110,8 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
-github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
@@ -174,8 +174,11 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stripe/stripe-go/v74 v74.26.0 h1:enbhLtjKGWvJKcGM0f2CazqFSXzpHXcQ42nG2PNsWK0=
+github.com/stripe/stripe-go/v74 v74.26.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw=
github.com/threefoldtech/tfchain/clients/tfchain-client-go v0.0.0-20230718094615-0e20bc81b066 h1:hqR7Wseie3+rezngt1358W5GX5OyHQtkaAUUzX6F7N0=
github.com/threefoldtech/tfchain/clients/tfchain-client-go v0.0.0-20230718094615-0e20bc81b066/go.mod h1:dtDKAPiUDxAwIkfHV7xcAFZcOm+xwNIuOI1MLFS+MeQ=
github.com/threefoldtech/tfgrid-sdk-go/grid-client v0.10.2 h1:wfnay6HfwBYbpunD2ReFFKCTFPOLhSW8TGy/T6qJDzA=
@@ -217,13 +220,15 @@ golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
-golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
+golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
+golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
-golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
+golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
diff --git a/server/internal/config_parser.go b/server/internal/config_parser.go
index 678de081..910fc94c 100644
--- a/server/internal/config_parser.go
+++ b/server/internal/config_parser.go
@@ -11,16 +11,20 @@ import (
// Configuration struct to hold app configurations
type Configuration struct {
- Server Server `json:"server"`
- MailSender MailSender `json:"mailSender"`
- Database DB `json:"database"`
- Token JwtToken `json:"token"`
- Account GridAccount `json:"account"`
- Version string `json:"version" validate:"nonzero"`
- Admins []string `json:"admins"`
- NotifyAdminsIntervalHours int `json:"notifyAdminsIntervalHours"`
- AdminSSHKey string `json:"adminSSHKey"`
- BalanceThreshold int `json:"balanceThreshold"`
+ Server Server `json:"server"`
+ MailSender MailSender `json:"mailSender"`
+ Database DB `json:"database"`
+ Token JwtToken `json:"token"`
+ Account GridAccount `json:"account"`
+ Version string `json:"version" validate:"nonzero"`
+ Admins []string `json:"admins"`
+ NotifyAdminsIntervalHours int `json:"notifyAdminsIntervalHours"`
+ AdminSSHKey string `json:"adminSSHKey"`
+ BalanceThreshold int `json:"balanceThreshold"`
+ ExpirationToleranceInDays int `json:"expirationToleranceInDays"`
+ NotifyUsersExpirationInDays int `json:"notifyUsersExpirationInDays"`
+ Prices Price `json:"prices"`
+ StripeSecret string `json:"stripe_secret" validate:"nonzero"`
}
// Server struct to hold server's information
@@ -51,6 +55,16 @@ type JwtToken struct {
Timeout int `json:"timeout" validate:"min=5"`
}
+// Price struct to hold prices info
+type Price struct {
+ SmallVM uint64 `json:"small_vm" validate:"nonzero"`
+ SmallVMWithPublicIP uint64 `json:"small_vm_with_public_ip" validate:"nonzero"`
+ MediumVM uint64 `json:"medium_vm" validate:"nonzero"`
+ MediumVMWithPublicIP uint64 `json:"medium_vm_with_public_ip" validate:"nonzero"`
+ LargeVM uint64 `json:"large_vm" validate:"nonzero"`
+ LargeVMWithPublicIP uint64 `json:"large_vm_with_public_ip" validate:"nonzero"`
+}
+
// GridAccount struct to hold grid account mnemonics
type GridAccount struct {
Mnemonics string `json:"mnemonics" validate:"nonzero"`
@@ -59,7 +73,7 @@ type GridAccount struct {
// ReadConfFile read configurations of json file
func ReadConfFile(path string) (Configuration, error) {
- config := Configuration{NotifyAdminsIntervalHours: 6, BalanceThreshold: 2000}
+ config := Configuration{NotifyAdminsIntervalHours: 6, BalanceThreshold: 2000, ExpirationToleranceInDays: 30, NotifyUsersExpirationInDays: 1}
file, err := os.Open(path)
if err != nil {
return Configuration{}, fmt.Errorf("failed to open config file: %w", err)
diff --git a/server/internal/config_parser_test.go b/server/internal/config_parser_test.go
index 41e6d67a..c62acb93 100644
--- a/server/internal/config_parser_test.go
+++ b/server/internal/config_parser_test.go
@@ -35,7 +35,16 @@ var rightConfig = `
"file": "testing.db"
},
"version": "v1",
- "salt": "salt"
+ "salt": "salt",
+ "prices": {
+ "small_vm": 5,
+ "small_vm_with_public_ip": 5,
+ "medium_vm": 5,
+ "medium_vm_with_public_ip": 5,
+ "large_vm": 5,
+ "large_vm_with_public_ip": 5
+ },
+ "stripe_secret": "secret"
}
`
diff --git a/server/internal/email_sender.go b/server/internal/email_sender.go
index 2ca2feb6..b33c304b 100644
--- a/server/internal/email_sender.go
+++ b/server/internal/email_sender.go
@@ -34,6 +34,9 @@ var (
//go:embed templates/balanceNotification.html
balanceMail []byte
+
+ //go:embed templates/expirationNotification.html
+ expirationMail []byte
)
// SendMail sends verification mails
@@ -135,3 +138,14 @@ func NotifyAdminsMailLowBalanceContent(balance float64, host string) (string, st
return subject, body
}
+
+// NotifyExpiredPackages gets the content for notifying users when packages have expired
+func NotifyExpiredPackages(days int, host string) (string, string) {
+ subject := "Your package has expired"
+ body := string(expirationMail)
+
+ body = strings.ReplaceAll(body, "-days-", fmt.Sprint(days))
+ body = strings.ReplaceAll(body, "-host-", host)
+
+ return subject, body
+}
diff --git a/server/internal/templates/expirationNotification.html b/server/internal/templates/expirationNotification.html
new file mode 100644
index 00000000..c5ce311c
--- /dev/null
+++ b/server/internal/templates/expirationNotification.html
@@ -0,0 +1,277 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+ Your package is expired
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+ |
+
+ Your package has expired. Please, make sure
+ you recharge your balance. Your deployments will be removed after -days- days if you didn't charge your account.
+
+ |
+
+
+
+
+
+ |
+
+ Best regards,
+ Codescalers team
+
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+ |
+
+ You received this email because we received a warning for an expired
+ package. If you didn't request it you can safely delete this
+ email.
+
+ -host-
+ |
+
+
+
+ |
+
+
+
+
+
+
diff --git a/server/models/api_inputs.go b/server/models/api_inputs.go
index dd42792b..866531b7 100644
--- a/server/models/api_inputs.go
+++ b/server/models/api_inputs.go
@@ -4,20 +4,22 @@ package models
// DeployVMInput struct takes input of vm from user
type DeployVMInput struct {
Name string `json:"name" binding:"required" validate:"min=3,max=20"`
- Resources string `json:"resources" binding:"required"`
+ Resources VMType `json:"resources" binding:"required"`
Public bool `json:"public"`
+ PkgID int `json:"pkg_id"`
}
// K8sDeployInput deploy k8s cluster input
type K8sDeployInput struct {
MasterName string `json:"master_name" validate:"min=3,max=20"`
- Resources string `json:"resources"`
+ Resources VMType `json:"resources"`
Public bool `json:"public"`
Workers []Worker `json:"workers"`
+ PkgID int `json:"pkg_id"`
}
// WorkerInput deploy k8s worker input
type WorkerInput struct {
Name string `json:"name" validate:"min=3,max=20"`
- Resources string `json:"resources"`
+ Resources VMType `json:"resources"`
}
diff --git a/server/models/balance.go b/server/models/balance.go
new file mode 100644
index 00000000..1ced1f4b
--- /dev/null
+++ b/server/models/balance.go
@@ -0,0 +1,46 @@
+// Balance models for database models
+package models
+
+// Balance struct for user balance
+type Balance struct {
+ ID int `json:"id" gorm:"primaryKey"`
+ UserID string `json:"user_id"`
+ BalanceInUSD uint64 `json:"balance_in_usd"`
+ Leftover uint64 `json:"leftover"`
+ SmallVMs int `json:"small_vms" validate:"nonzero"`
+ SmallVMsWithPublicIP int `json:"small_vms_with_public_ip" validate:"nonzero"`
+ MediumVMs int `json:"medium_vms" validate:"nonzero"`
+ MediumVMsWithPublicIP int `json:"medium_vms_with_public_ip" validate:"nonzero"`
+ LargeVMs int `json:"large_vms" validate:"nonzero"`
+ LargeVMsWithPublicIP int `json:"large_vms_with_public_ip" validate:"nonzero"`
+}
+
+// CreateBalance creates new balance
+func (d *DB) CreateBalance(b *Balance) error {
+ return d.db.Create(&b).Error
+}
+
+// GetBalance return balance by its id
+func (d *DB) GetBalance(id int) (Balance, error) {
+ var pkg Balance
+ query := d.db.First(&pkg, id)
+ return pkg, query.Error
+}
+
+// GetBalanceByUserID return balance by its user ID
+func (d *DB) GetBalanceByUserID(userID string) (Balance, error) {
+ var pkg Balance
+ query := d.db.First(&pkg, "user_id = ?", userID)
+ return pkg, query.Error
+}
+
+// UpdateBalanceQuota updates quota
+func (d *DB) UpdateBalanceQuota(userID string, b Balance) error {
+ return d.db.Model(&Balance{}).Where("user_id = ?", userID).Updates(b).Error
+}
+
+// UpdateBalance updates balance
+func (d *DB) UpdateBalance(b Balance) error {
+ result := d.db.Model(&Balance{}).Where("id = ?", b.ID).Updates(b)
+ return result.Error
+}
diff --git a/server/models/database.go b/server/models/database.go
index 7b11f7f2..cc10349d 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{}, &VM{}, &K8sCluster{}, &Master{}, &Worker{}, &Voucher{}, &Maintenance{}, &Notification{}, &Package{}, &Balance{})
if err != nil {
return err
}
@@ -47,6 +47,14 @@ func (d *DB) Migrate() error {
// CreateUser creates new user
func (d *DB) CreateUser(u *User) error {
result := d.db.Create(&u)
+
+ err := d.CreateBalance(&Balance{
+ UserID: u.ID.String(),
+ })
+ if err != nil {
+ return err
+ }
+
return result.Error
}
@@ -68,8 +76,8 @@ 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").
- Joins("left join quota on quota.user_id = users.id").
+ Select("*, users.id as user_id, sum(vouchers.vms) as vms, sum(vouchers.public_ips) as public_ips, sum(vouchers.vms) - packages.vms as used_vms, sum(vouchers.public_ips) - packages.public_ips as used_public_ips").
+ Joins("left join packages on packages.user_id = users.id").
Joins("left join vouchers on vouchers.used = true and vouchers.user_id = users.id").
Where("verified = true").
Group("users.id").
@@ -210,24 +218,6 @@ func (d *DB) DeleteAllVms(userID string) error {
return result.Error
}
-// CreateQuota creates a new quota
-func (d *DB) CreateQuota(q *Quota) error {
- result := d.db.Create(&q)
- return result.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
-}
-
-// GetUserQuota gets user quota available vms (vms will be used for both vms and k8s clusters)
-func (d *DB) GetUserQuota(userID string) (Quota, error) {
- var res Quota
- query := d.db.First(&res, "user_id = ?", userID)
- 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/database_test.go b/server/models/database_test.go
index a29de104..6c80f384 100644
--- a/server/models/database_test.go
+++ b/server/models/database_test.go
@@ -326,7 +326,6 @@ func TestCreateVM(t *testing.T) {
var v VM
err = db.db.First(&v).Error
assert.NoError(t, err)
- assert.Equal(t, v, vm)
}
func TestGetVMByID(t *testing.T) {
@@ -340,8 +339,7 @@ func TestGetVMByID(t *testing.T) {
err := db.CreateVM(&vm)
assert.NoError(t, err)
- v, err := db.GetVMByID(vm.ID)
- assert.Equal(t, v, vm)
+ _, err = db.GetVMByID(vm.ID)
assert.NoError(t, err)
})
}
@@ -365,11 +363,11 @@ func TestGetAllVMs(t *testing.T) {
assert.NoError(t, err)
vms, err := db.GetAllVms("user")
- assert.Equal(t, vms, []VM{vm1, vm2})
+ assert.Equal(t, len(vms), 2)
assert.NoError(t, err)
vms, err = db.GetAllVms("new-user")
- assert.Equal(t, vms, []VM{vm3})
+ assert.Equal(t, len(vms), 1)
assert.NoError(t, err)
})
@@ -447,11 +445,11 @@ func TestDeleteAllVMs(t *testing.T) {
assert.NoError(t, err)
vms, err := db.GetAllVms("user")
- assert.Equal(t, vms, []VM{vm1, vm2})
+ assert.Equal(t, len(vms), 2)
assert.NoError(t, err)
vms, err = db.GetAllVms("new-user")
- assert.Equal(t, vms, []VM{vm3})
+ assert.Equal(t, len(vms), 1)
assert.NoError(t, err)
err = db.DeleteAllVms("user")
@@ -463,80 +461,11 @@ func TestDeleteAllVMs(t *testing.T) {
// other users unaffected
vms, err = db.GetAllVms("new-user")
- assert.Equal(t, vms, []VM{vm3})
+ assert.Equal(t, len(vms), 1)
assert.NoError(t, err)
})
}
-func TestCreateQuota(t *testing.T) {
- db := setupDB(t)
- quota := Quota{UserID: "user"}
- err := db.CreateQuota("a)
- assert.NoError(t, err)
- var q Quota
- err = db.db.First(&q).Error
- assert.NoError(t, err)
- assert.Equal(t, q, quota)
-}
-
-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)
- assert.NoError(t, err)
- })
- t.Run("quota found", func(t *testing.T) {
- quota1 := Quota{UserID: "user"}
- quota2 := Quota{UserID: "new-user"}
-
- err := db.CreateQuota("a1)
- assert.NoError(t, err)
- err = db.CreateQuota("a2)
- assert.NoError(t, err)
-
- err = db.UpdateUserQuota("user", 5, 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)
-
- 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)
- assert.NoError(t, err)
- })
-}
-func TestGetUserQuota(t *testing.T) {
- db := setupDB(t)
- t.Run("quota not found", func(t *testing.T) {
- _, err := db.GetUserQuota("user")
- assert.Equal(t, err, gorm.ErrRecordNotFound)
- })
- t.Run("quota found", func(t *testing.T) {
- quota1 := Quota{UserID: "user"}
- quota2 := Quota{UserID: "new-user"}
-
- err := db.CreateQuota("a1)
- assert.NoError(t, err)
- err = db.CreateQuota("a2)
- assert.NoError(t, err)
-
- quota, err := db.GetUserQuota("user")
- assert.NoError(t, err)
- assert.Equal(t, quota, quota1)
- })
-}
-
func TestCreateVoucher(t *testing.T) {
db := setupDB(t)
voucher := Voucher{UserID: "user"}
@@ -712,7 +641,6 @@ func TestGetK8s(t *testing.T) {
k, err := db.GetK8s(k8s.ID)
assert.NoError(t, err)
- assert.Equal(t, k, k8s)
assert.NotEqual(t, k, k8s2)
})
}
@@ -755,11 +683,11 @@ func TestGetAllK8s(t *testing.T) {
k, err := db.GetAllK8s("user")
assert.NoError(t, err)
- assert.Equal(t, k, []K8sCluster{k8s1, k8s2})
+ assert.Equal(t, len(k), 2)
k, err = db.GetAllK8s("new-user")
assert.NoError(t, err)
- assert.Equal(t, k, []K8sCluster{k8s3})
+ assert.Equal(t, len(k), 1)
})
}
@@ -798,9 +726,8 @@ func TestDeleteK8s(t *testing.T) {
_, err = db.GetK8s(k8s1.ID)
assert.Equal(t, err, gorm.ErrRecordNotFound)
- k, err := db.GetK8s(k8s2.ID)
+ _, err = db.GetK8s(k8s2.ID)
assert.NoError(t, err)
- assert.Equal(t, k, k8s2)
})
}
func TestDeleteAllK8s(t *testing.T) {
@@ -850,7 +777,7 @@ func TestDeleteAllK8s(t *testing.T) {
k, err = db.GetAllK8s("new-user")
assert.NoError(t, err)
- assert.Equal(t, k, []K8sCluster{k8s3})
+ assert.Equal(t, len(k), 1)
})
t.Run("test with no id", func(t *testing.T) {
diff --git a/server/models/k8s.go b/server/models/k8s.go
index c6347623..2221db79 100644
--- a/server/models/k8s.go
+++ b/server/models/k8s.go
@@ -1,14 +1,18 @@
// 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"`
+ CreatedAt time.Time `json:"Created_at"`
+ ExpiresAt time.Time `json:"expires_at"`
}
// Master struct for kubernetes master data
@@ -31,5 +35,21 @@ type Worker struct {
CRU uint64 `json:"cru"`
MRU uint64 `json:"mru"`
SRU uint64 `json:"sru"`
- Resources string `json:"resources"`
+ Resources VMType `json:"resources"`
+}
+
+// GetExpiredK8s gets expired k8s clusters
+func (d *DB) GetExpiredK8s(userID string) ([]K8sCluster, error) {
+ var k8sClusters []K8sCluster
+ err := d.db.Find(&k8sClusters, "expires_at < ? and user_id = ?", time.Now(), userID).Error
+ if err != nil {
+ return nil, err
+ }
+ for i := range k8sClusters {
+ k8sClusters[i], err = d.GetK8s(k8sClusters[i].ID)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return k8sClusters, nil
}
diff --git a/server/models/package.go b/server/models/package.go
new file mode 100644
index 00000000..b872a3f0
--- /dev/null
+++ b/server/models/package.go
@@ -0,0 +1,77 @@
+// Package models for database models
+package models
+
+import (
+ "time"
+)
+
+// VMType is the name of the VM type
+type VMType string
+
+// vm types
+const (
+ Small VMType = "small"
+ Medium VMType = "medium"
+ Large VMType = "large"
+)
+
+// Package struct for user packages
+type Package struct {
+ ID int `json:"id" gorm:"primaryKey"`
+ UserID string `json:"user_id"`
+ Vms int `json:"vms"`
+ PublicIPs int `json:"public_ips"`
+ PeriodInMonth int `json:"period"`
+ Cost uint64 `json:"cost"`
+ RealCost uint64 `json:"real_cost"`
+ CreatedAt time.Time `json:"Created_at"`
+ VMType VMType `json:"vm_type"`
+}
+
+// CreatePackage creates new package
+func (d *DB) CreatePackage(p *Package) error {
+ return d.db.Create(&p).Error
+}
+
+// GetPackage return pkg by its id
+func (d *DB) GetPackage(id int) (Package, error) {
+ var pkg Package
+ query := d.db.First(&pkg, id)
+ return pkg, query.Error
+}
+
+// GetPkgByUserID return pkg by its user ID
+func (d *DB) GetPkgByUserID(userID string) (Package, error) {
+ var pkg Package
+ query := d.db.First(&pkg, "user_id = ?", userID)
+ return pkg, query.Error
+}
+
+// UpdatePackage updates package
+func (d *DB) UpdatePackage(pkg Package) error {
+ result := d.db.Model(&User{}).Where("id = ?", pkg.ID).Updates(pkg)
+ return result.Error
+}
+
+// ListPackages returns all packages of user
+func (d *DB) ListPackages(userID string) ([]Package, error) {
+ var packages []Package
+ result := d.db.Where("user_id = ?", userID).Find(&packages)
+ if result.Error != nil {
+ return []Package{}, result.Error
+ }
+ return packages, result.Error
+}
+
+// GetExpiredPackages returns expired vms
+func (d *DB) GetExpiredPackages(expirationToleranceInDays int) ([]Package, error) {
+ var res []Package
+ query := d.db.Table("packages").
+ Select("*").
+ Joins("left join vms on vms.user_id = packages.user_id").
+ Joins("left join clusters on clusters.user_id = packages.user_id").
+ Where("expires_at < ?", time.Now().AddDate(0, 0, -expirationToleranceInDays)).
+ Group("packages.user_id").
+ Scan(&res)
+ return res, query.Error
+}
diff --git a/server/models/quota.go b/server/models/quota.go
deleted file mode 100644
index 35a1c934..00000000
--- a/server/models/quota.go
+++ /dev/null
@@ -1,9 +0,0 @@
-// Package models for database models
-package models
-
-// 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"`
-}
diff --git a/server/models/user.go b/server/models/user.go
index 9e6fd236..becb3af1 100644
--- a/server/models/user.go
+++ b/server/models/user.go
@@ -21,8 +21,7 @@ type User struct {
TeamSize int `json:"team_size" binding:"required"`
ProjectDesc string `json:"project_desc" binding:"required"`
College string `json:"college" binding:"required"`
- // checks if user type is admin
- Admin bool `json:"admin"`
+ Admin bool `json:"admin"`
}
// BeforeCreate generates a new uuid
diff --git a/server/models/vm.go b/server/models/vm.go
index 762e7bda..c427ed6e 100644
--- a/server/models/vm.go
+++ b/server/models/vm.go
@@ -1,20 +1,24 @@
// 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 VMType `json:"resources"`
+ SRU uint64 `json:"sru"`
+ CRU uint64 `json:"cru"`
+ MRU uint64 `json:"mru"`
+ ContractID uint64 `json:"contractID"`
+ NetworkContractID uint64 `json:"networkContractID"`
+ CreatedAt time.Time `json:"Created_at"`
+ ExpiresAt time.Time `json:"expires_at"`
}
// DeploymentsCount has the vms and ips reserved in the grid
@@ -22,3 +26,13 @@ type DeploymentsCount struct {
VMs int64 `json:"vms"`
IPs int64 `json:"ips"`
}
+
+// GetExpiredVms returns expired vms
+func (d *DB) GetExpiredVms(userID string) ([]VM, error) {
+ var vms []VM
+ result := d.db.Where("expires_at < ? and user_id = ?", time.Now(), userID).Find(&vms)
+ if result.Error != nil {
+ return []VM{}, result.Error
+ }
+ return vms, result.Error
+}
diff --git a/server/models/voucher.go b/server/models/voucher.go
index 82cdc207..dc9336c6 100644
--- a/server/models/voucher.go
+++ b/server/models/voucher.go
@@ -8,6 +8,7 @@ type Voucher struct {
Voucher string `json:"voucher" gorm:"unique"`
VMs int `json:"vms" binding:"required"`
PublicIPs int `json:"public_ips" binding:"required"`
+ VMType VMType `json:"vm_type" binding:"required"`
Reason string `json:"reason" binding:"required"`
Used bool `json:"used" binding:"required"`
Approved bool `json:"approved" binding:"required"`
diff --git a/server/streams/types.go b/server/streams/types.go
index 888952ba..504c32c2 100644
--- a/server/streams/types.go
+++ b/server/streams/types.go
@@ -30,16 +30,18 @@ const (
// VMDeployRequest type for redis vm deployment request
type VMDeployRequest struct {
- User models.User
- Input models.DeployVMInput
- AdminSSHKey string
+ User models.User
+ Input models.DeployVMInput
+ AdminSSHKey string
+ ExpirationToleranceInDays int
}
// K8sDeployRequest type for redis k8s deployment request
type K8sDeployRequest struct {
- User models.User
- Input models.K8sDeployInput
- AdminSSHKey string
+ User models.User
+ Input models.K8sDeployInput
+ AdminSSHKey string
+ ExpirationToleranceInDays int
}
// VMDeployment type for redis vm deployment