diff --git a/Dockerfile.fix-username b/Dockerfile.fix-username deleted file mode 100644 index a7b26e2..0000000 --- a/Dockerfile.fix-username +++ /dev/null @@ -1,17 +0,0 @@ -FROM golang:1.18 - -RUN go install github.com/jstemmer/go-junit-report@latest - -ENV CGO_ENABLED=0 - -WORKDIR /go/src/github.com/cyverse-de/QMS -COPY . . -RUN go build --trimpath -o cmd/fix-usernames ./cmd/fix-usernames - -FROM debian:stable-slim - -COPY --from=0 /go/src/github.com/cyverse-de/QMS/cmd/fix-usernames/fix-usernames /bin - -ENTRYPOINT ["fix-usernames"] - -EXPOSE 8080 diff --git a/cmd/fix-usernames/.gitignore b/cmd/fix-usernames/.gitignore deleted file mode 100644 index ac2c1de..0000000 --- a/cmd/fix-usernames/.gitignore +++ /dev/null @@ -1 +0,0 @@ -fix-usernames diff --git a/cmd/fix-usernames/main.go b/cmd/fix-usernames/main.go deleted file mode 100644 index 3e257e3..0000000 --- a/cmd/fix-usernames/main.go +++ /dev/null @@ -1,351 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - "strings" - - "github.com/cyverse/QMS/internal/db" - "github.com/cyverse/QMS/internal/model" - "github.com/knadh/koanf" - "github.com/knadh/koanf/providers/env" - "github.com/pkg/errors" - "gorm.io/gorm" - "gorm.io/gorm/clause" -) - -type Config struct { - DatabaseURI string - UsernameSuffix string -} - -// loadConfig loads configuration settings from the environment. We're using koanf directly here so that the -// configuration files don't have to be present to run the configuration utility. -func loadConfig() (*Config, error) { - k := koanf.New(".") - - // Load the configuration settings from the environment. - err := k.Load( - env.Provider("QMS_", ".", - func(s string) string { - return strings.Replace(strings.ToLower(strings.TrimPrefix(s, "QMS_")), "_", ".", -1) - }, - ), - nil, - ) - if err != nil { - return nil, err - } - - // Verify that the database URI is specified. - databaseURI := k.String("database.uri") - if databaseURI == "" { - return nil, fmt.Errorf("QMS_DATABASE_URI must be defined") - } - - // Verify that the username suffix is specified. - usernameSuffix := k.String("username.suffix") - if usernameSuffix == "" { - return nil, fmt.Errorf("QMS_USERNAME_SUFFIX must be specified") - } - - return &Config{DatabaseURI: databaseURI, UsernameSuffix: usernameSuffix}, nil -} - -// listUsernames lists all distinct usernames in the system, excluding the suffix if it's present. -func listUsernames(ctx context.Context, tx *gorm.DB) ([]string, error) { - var usernames []string - err := tx.WithContext(ctx). - Table("users"). - Distinct("regexp_replace(users.username, '@.*', '') as username"). - Order("username"). - Find(&usernames). - Error - return usernames, err -} - -// loadCurrentSubscription loads the current subscription for a single user. It does not create a new subscription if -// the user doesn't currently have one. -func loadCurrentSubscription(ctx context.Context, tx *gorm.DB, user *model.User) (*model.Subscription, error) { - var subscriptions []model.Subscription - - // Look up the plan. - err := tx.WithContext(ctx). - Preload("User"). - Preload("Plan"). - Preload("Plan.PlanQuotaDefaults"). - Preload("Plan.PlanQuotaDefaults.ResourceType"). - Preload("Quotas"). - Preload("Quotas.ResourceType"). - Preload("Usages"). - Preload("Usages.ResourceType"). - Where("user_id = ?", user.ID). - Where( - tx.Where("CURRENT_TIMESTAMP BETWEEN subscriptions.effective_start_date AND subscriptions.effective_end_date"). - Or("CURRENT_TIMESTAMP > subscriptions.effective_start_date AND subscriptions.effective_end_date IS NULL"), - ). - Order("subscriptions.effective_start_date desc"). - Limit(1). - Find(&subscriptions). - Error - - var plan *model.Subscription - if len(subscriptions) > 0 { - plan = &subscriptions[0] - } - return plan, err -} - -// LoadSubscription loads the subscription details for the given subscription ID. -func loadSubscription(ctx context.Context, tx *gorm.DB, subscriptionID string) (*model.Subscription, error) { - var subscription *model.Subscription - - err := tx.WithContext(ctx). - Preload("User"). - Preload("Plan"). - Preload("Plan.PlanQuotaDefaults"). - Preload("Plan.PlanQuotaDefaults.ResourceType"). - Preload("Quotas"). - Preload("Quotas.ResourceType"). - Preload("Usages"). - Preload("Usages.ResourceType"). - Where("id = ?", subscriptionID). - First(&subscription). - Error - - return subscription, err -} - -// loadMostRecentDataUsage loads lastest data usage record for a user using both the username with and without the -// username suffix. -func loadMostRecentDataUsage(ctx context.Context, tx *gorm.DB, oldUsername, newUsername string) (*model.Usage, error) { - var usages []model.Usage - - // Look up the usages. - err := tx.WithContext(ctx). - Joins("JOIN subscriptions ON usages.subscription_id = subscriptions.id"). - Joins("JOIN users ON subscriptions.user_id = users.id"). - Joins("JOIN resource_types ON usages.resource_type_id = resource_types.id"). - Where("users.username IN ?", []string{oldUsername, newUsername}). - Where("resource_types.name = ?", "data.size"). - Order("usages.last_modified_at DESC"). - Limit(1). - Find(&usages). - Error - - var usage *model.Usage - if len(usages) > 0 { - usage = &usages[0] - } - return usage, err -} - -// findQuotaValue finds the quota value for a specific resource type in a list of quotas. -func findQuotaValue(quotas []model.Quota, resourceTypeName string) float64 { - var quotaValue float64 - for _, quota := range quotas { - if quota.ResourceType.Name == resourceTypeName && quota.Quota > quotaValue { - quotaValue = quota.Quota - } - } - return quotaValue -} - -// setQuota either adds a quota to a subscription or updates a quota in a subscription. -func setQuota(ctx context.Context, tx *gorm.DB, subscriptionID, resourceTypeID *string, quotaValue float64) error { - quota := model.Quota{ - SubscriptionID: subscriptionID, - ResourceTypeID: resourceTypeID, - Quota: quotaValue, - } - err := tx.WithContext(ctx). - Clauses(clause.OnConflict{ - Columns: []clause.Column{ - { - Name: "subscription_id", - }, - { - Name: "resource_type_id", - }, - }, - DoUpdates: clause.AssignmentColumns([]string{"quota"}), - }). - Create("a).Error - return err -} - -// restorePreviousQuotas ensures that the resource usage limits for the new subscription are at least as large as the -// resource usage limits for the old subscription. -func restorePreviousQuotas(ctx context.Context, tx *gorm.DB, oldSubscription, newSubscription *model.Subscription) error { - for _, quota := range oldSubscription.Quotas { - newQuotaValue := findQuotaValue(newSubscription.Quotas, quota.ResourceType.Name) - if newQuotaValue < quota.Quota { - err := setQuota(ctx, tx, newSubscription.ID, quota.ResourceType.ID, quota.Quota) - if err != nil { - return err - } - } - } - return nil -} - -// addUsageToSubscription adds a usage record to the new subscription. This function is only intended to be used to -// add usage records to a brand new subscription, so it assumes that there aren't any usages associated with the -// subscription yet. -func addUsageToSubscription(ctx context.Context, tx *gorm.DB, subscription *model.Subscription, usage *model.Usage) error { - newUsage := &model.Usage{ - ResourceTypeID: usage.ResourceTypeID, - SubscriptionID: subscription.ID, - Usage: usage.Usage, - } - return tx.WithContext(ctx).Create(newUsage).Error -} - -// loadUser loads user information from the database, without creating a new record for the user if one doesn't exist -// already. -func loadUser(ctx context.Context, tx *gorm.DB, username string) (*model.User, error) { - var users []model.User - - // Look up the user. - err := tx.WithContext(ctx). - Where("username = ?", username). - Limit(1). - Find(&users). - Error - if err != nil { - return nil, err - } - - if len(users) == 0 { - return nil, nil - } - return &users[0], nil -} - -// fixUsername fixes a username for a single user. -func fixUsername(ctx context.Context, tx *gorm.DB, newUsername string, usernameSuffix string) error { - fmt.Printf("fixing the subscriptions for %s...\n", newUsername) - - // Get the information for the incorrect username. - oldUsername := fmt.Sprintf("%s%s", newUsername, usernameSuffix) - oldUser, err := loadUser(ctx, tx, oldUsername) - if err != nil { - return errors.Wrapf(err, "unable get the user details for %s", oldUsername) - } - - // Get the information for the correct username. This user record will be created if it doesn't exist already. - newUser, err := db.GetUser(ctx, tx, newUsername) - if err != nil { - return errors.Wrapf(err, "unable to get the user details for %s", newUsername) - } - - // Load the current subscription for the username without the suffix. This will serve as the source for the user's - // new subscription. We use the username without the suffix for this because subscription purchases never used the - // username suffix. - oldSubscription, err := loadCurrentSubscription(ctx, tx, newUser) - if err != nil { - return errors.Wrapf(err, "unable to load the current plan for %s", newUser.Username) - } - - // Load the most recent data usage record for the user. - usage, err := loadMostRecentDataUsage(ctx, tx, oldUsername, newUsername) - if err != nil { - return errors.Wrapf( - err, - "unable to load the most recent data usage for %s and %s", - oldUsername, - newUsername, - ) - } - - // Deactivate all plans for both the old username and the new username. - if oldUser != nil { - err = db.DeactivateSubscriptions(ctx, tx, *oldUser.ID) - if err != nil { - return errors.Wrapf(err, "unable to deactivate existing plans for %s", oldUser.Username) - } - } - err = db.DeactivateSubscriptions(ctx, tx, *newUser.ID) - if err != nil { - return errors.Wrapf(err, "unable to deactivate existing plans for %s", newUser.Username) - } - - // Create the new subscription. - var newSubscription *model.Subscription - if oldSubscription == nil { - newSubscription, err = db.SubscribeUserToDefaultPlan(ctx, tx, newUser.Username) - if err != nil { - return errors.Wrapf(err, "unable to subscribe %s to the default plan", newUser.Username) - } - } else { - plan := oldSubscription.Plan - newSubscription, err = db.SubscribeUserToPlan(ctx, tx, newUser, plan, true) - if err != nil { - return errors.Wrapf(err, "unable to subscribe %s to the %s plan", newUser.Username, plan.Name) - } - } - - // Get all of the details for the new subscription. - newSubscription, err = loadSubscription(ctx, tx, *newSubscription.ID) - if err != nil { - return errors.Wrapf(err, "unable to load the new subscription details for %s", newUser.Username) - } - - // Ensure that the new quotas are greater than or equal to the old quotas if applicable. - if oldSubscription != nil { - err = restorePreviousQuotas(ctx, tx, oldSubscription, newSubscription) - if err != nil { - return errors.Wrapf(err, "unable to restore previous quotas for %s", newUser.Username) - } - } - - // Associate the usage with the current subscription. - if usage != nil { - err = addUsageToSubscription(ctx, tx, newSubscription, usage) - if err != nil { - return errors.Wrapf(err, "unable to add data usage to the new subscription for %s", newUser.Username) - } - } - - return nil -} - -func main() { - - // Load the configuration. - cfg, err := loadConfig() - if err != nil { - log.Fatalf("unable to load the configuration: %s", err) - } - - // Establish the database connection. - _, gormdb, err := db.Init("postgres", cfg.DatabaseURI) - if err != nil { - log.Fatalf("unable to connect to the database: %s", err) - } - - // Run the actual updates in a transaction. - err = gormdb.Transaction(func(tx *gorm.DB) error { - ctx := context.Background() - - // Get the list of usernames with suffixes. - usernames, err := listUsernames(ctx, tx) - if err != nil { - return errors.Wrap(err, "unable to list usernames with suffixes") - } - - // Fix the usernames. - for _, username := range usernames { - err = fixUsername(ctx, tx, username, cfg.UsernameSuffix) - if err != nil { - return errors.Wrapf(err, "unable to fix the username for %s", username) - } - } - - return nil - }) - if err != nil { - log.Fatal(err) - } -} diff --git a/internal/controllers/subscriptions.go b/internal/controllers/subscriptions.go index 31f3e97..6fa7b5c 100644 --- a/internal/controllers/subscriptions.go +++ b/internal/controllers/subscriptions.go @@ -53,13 +53,20 @@ func (sa *SubscriptionAdder) subscriptionError(username string, f string, args . } // AddSubscription subscribes a user to a subscription plan. -func (sa *SubscriptionAdder) AddSubscription(tx *gorm.DB, username, planName *string, paid bool) *model.SubscriptionResponse { +func (sa *SubscriptionAdder) AddSubscription(tx *gorm.DB, req model.SubscriptionRequest) *model.SubscriptionResponse { + username := req.Username + planName := req.PlanName + paid := req.Paid + if username == nil || *username == "" { return sa.subscriptionError("", "no username provided in request") } if planName == nil || *planName == "" { return sa.subscriptionError(*username, "no plan name provided in request") } + if paid == nil { + return sa.subscriptionError(*username, "no paid indicator provided in request") + } // Look up the plan information. plan, ok := sa.plansByName[*planName] @@ -72,7 +79,7 @@ func (sa *SubscriptionAdder) AddSubscription(tx *gorm.DB, username, planName *st logrus.Fields{ "username": *username, "planName": *planName, - "paid": paid, + "paid": *paid, }, ) @@ -107,7 +114,7 @@ func (sa *SubscriptionAdder) AddSubscription(tx *gorm.DB, username, planName *st } // Add the subscription. - sub, err := db.SubscribeUserToPlan(sa.cfg.Ctx, tx, user, plan, paid) + sub, err := db.SubscribeUserToPlan(sa.cfg.Ctx, tx, user, plan, *paid) if err != nil { log.Error(err) return sa.subscriptionError(*username, err.Error()) @@ -177,9 +184,7 @@ func (s Server) AddSubscriptions(ctx echo.Context) error { _ = s.GORMDB.Transaction(func(tx *gorm.DB) error { response[i] = subscriptionAdder.AddSubscription( tx, - subscriptionRequest.Username, - subscriptionRequest.PlanName, - subscriptionRequest.Paid, + subscriptionRequest, ) return nil }) diff --git a/internal/controllers/user_nats.go b/internal/controllers/user_nats.go deleted file mode 100644 index 2d32936..0000000 --- a/internal/controllers/user_nats.go +++ /dev/null @@ -1 +0,0 @@ -package controllers diff --git a/internal/controllers/users.go b/internal/controllers/users.go index 5c3259c..3a5e104 100644 --- a/internal/controllers/users.go +++ b/internal/controllers/users.go @@ -12,6 +12,7 @@ import ( "github.com/cyverse/QMS/internal/db" "github.com/cyverse/QMS/internal/httpmodel" "github.com/cyverse/QMS/internal/model" + "github.com/cyverse/QMS/internal/query" "github.com/labstack/echo/v4" "github.com/sirupsen/logrus" ) @@ -322,6 +323,11 @@ func (s Server) UpdateSubscription(ctx echo.Context) error { return model.Error(ctx, "invalid plan name", http.StatusBadRequest) } + paid, err := query.ValidateBooleanQueryParam(ctx, "paid", nil) + if err != nil { + return model.Error(ctx, err.Error(), http.StatusBadRequest) + } + log.Debugf("plan name from request is %s", planName) username := strings.TrimSuffix(ctx.Param("username"), s.UsernameSuffix) @@ -369,7 +375,7 @@ func (s Server) UpdateSubscription(ctx echo.Context) error { log.Debug("deactivated all active plans for the user") // Subscribe the user to the plan. - _, err = db.SubscribeUserToPlan(context, tx, user, plan, true) + _, err = db.SubscribeUserToPlan(context, tx, user, plan, paid) if err != nil { return model.Error(ctx, err.Error(), http.StatusInternalServerError) } diff --git a/internal/db/subscriptions.go b/internal/db/subscriptions.go index a97b2c3..58fdc0a 100644 --- a/internal/db/subscriptions.go +++ b/internal/db/subscriptions.go @@ -26,7 +26,9 @@ func QuotasFromPlan(plan *model.Plan) []model.Quota { } // SubscribeUserToPlan subscribes the given user to the given plan. -func SubscribeUserToPlan(ctx context.Context, db *gorm.DB, user *model.User, plan *model.Plan, paid bool) (*model.Subscription, error) { +func SubscribeUserToPlan( + ctx context.Context, db *gorm.DB, user *model.User, plan *model.Plan, paid bool, +) (*model.Subscription, error) { wrapMsg := "unable to add user plan" var err error @@ -67,7 +69,7 @@ func SubscribeUserToDefaultPlan(ctx context.Context, db *gorm.DB, username strin } // Subscribe the user to the plan. - return SubscribeUserToPlan(ctx, db, user, plan, true) + return SubscribeUserToPlan(ctx, db, user, plan, false) } // GetActiveSubscription retrieves the user plan record that is currently active for the user. The effective start diff --git a/internal/model/plan.go b/internal/model/plan.go index 40205af..415ff42 100644 --- a/internal/model/plan.go +++ b/internal/model/plan.go @@ -94,7 +94,7 @@ type Subscription struct { // The recorded usage amounts associated with the subscription Usages []Usage `json:"usages"` - // Whether or not user's need to pay for the subscription. + // True if the user paid for the subscription. Paid bool `json:"paid"` } diff --git a/internal/model/subscriptions.go b/internal/model/subscriptions.go index 38d92fa..ee17ecb 100644 --- a/internal/model/subscriptions.go +++ b/internal/model/subscriptions.go @@ -14,10 +14,8 @@ type SubscriptionRequest struct { // required: true PlanName *string `json:"plan_name"` - // Whether the subscription needs to be paid for. - // - // required: false - Paid bool `json:"paid"` + // True if the user paid for the subscription. + Paid *bool `json:"paid"` } // SubscriptionRequests represents a list of subscription requests. diff --git a/migrations/000011_cpu_hours_increase.down.sql b/migrations/000011_cpu_hours_increase.down.sql new file mode 100644 index 0000000..dab13b3 --- /dev/null +++ b/migrations/000011_cpu_hours_increase.down.sql @@ -0,0 +1,25 @@ +-- +-- Decreases the number of CPU hours available to users by a factor of 10. +-- + +BEGIN; + +SET search_path = public, pg_catalog; + +-- Divide the number of CPU hours for each subscription plan by 10. +UPDATE plan_quota_defaults +SET quota_value = CAST(quota_value / 10 AS bigint) +WHERE resource_type_id = ( + SELECT id FROM resource_types + WHERE name = 'cpu.hours' +); + +-- Divide the number of CPU hours for each subscription by 10. +UPDATE quotas +SET quota = CAST(quota / 10 AS bigint) +WHERE resource_type_id = ( + SELECT id FROM resource_types + WHERE name = 'cpu.hours' +); + +COMMIT; diff --git a/migrations/000011_cpu_hours_increase.up.sql b/migrations/000011_cpu_hours_increase.up.sql new file mode 100644 index 0000000..79b3172 --- /dev/null +++ b/migrations/000011_cpu_hours_increase.up.sql @@ -0,0 +1,25 @@ +-- +-- Increases the number of CPU hours available to users by a factor of 10. +-- + +BEGIN; + +SET search_path = public, pg_catalog; + +-- Multiply the number of CPU hours for each subscription plan by 10. +UPDATE plan_quota_defaults +SET quota_value = quota_value * 10 +WHERE resource_type_id = ( + SELECT id FROM resource_types + WHERE name = 'cpu.hours' +); + +-- Multiply the number of CPU hours for each subscription by 10. +UPDATE quotas +SET quota = quota * 10 +WHERE resource_type_id = ( + SELECT id FROM resource_types + WHERE name = 'cpu.hours' +); + +COMMIT;