focalboard/server/app/cloud.go
Miguel de la Cruz 4b0fb92fba
Multi product architecture (#3381)
- provides support for compiling Boards directly into the Mattermost suite server
- a ServicesAPI interface replaces the PluginAPI to allow for implementations coming from pluginAPI and suite server.
- a new product package provides a place to register Boards as a suite product and handles life-cycle events
- a new boards package replaces much of the mattermost-plugin logic, allowing this to be shared between plugin and product
- Boards now uses module workspaces; run make setup-go-work
2022-07-18 13:21:57 -04:00

319 lines
7.6 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"fmt"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
mmModel "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/utils"
)
var ErrNilPluginAPI = errors.New("server not running in plugin mode")
// GetBoardsCloudLimits returns the limits of the server, and an empty
// limits struct if there are no limits set.
func (a *App) GetBoardsCloudLimits() (*model.BoardsCloudLimits, error) {
if !a.IsCloud() {
return &model.BoardsCloudLimits{}, nil
}
productLimits, err := a.store.GetCloudLimits()
if err != nil {
return nil, err
}
usedCards, err := a.store.GetUsedCardsCount()
if err != nil {
return nil, err
}
cardLimitTimestamp, err := a.store.GetCardLimitTimestamp()
if err != nil {
return nil, err
}
boardsCloudLimits := &model.BoardsCloudLimits{
UsedCards: usedCards,
CardLimitTimestamp: cardLimitTimestamp,
}
if productLimits != nil && productLimits.Boards != nil {
if productLimits.Boards.Cards != nil {
boardsCloudLimits.Cards = *productLimits.Boards.Cards
}
if productLimits.Boards.Views != nil {
boardsCloudLimits.Views = *productLimits.Boards.Views
}
}
return boardsCloudLimits, nil
}
// IsCloud returns true if the server is running as a plugin in a
// cloud licensed server.
func (a *App) IsCloud() bool {
return utils.IsCloudLicense(a.store.GetLicense())
}
// IsCloudLimited returns true if the server is running in cloud mode
// and the card limit has been set.
func (a *App) IsCloudLimited() bool {
return a.CardLimit() != 0 && a.IsCloud()
}
// SetCloudLimits sets the limits of the server.
func (a *App) SetCloudLimits(limits *mmModel.ProductLimits) error {
oldCardLimit := a.CardLimit()
// if the limit object doesn't come complete, we assume limits are
// being disabled
cardLimit := 0
if limits != nil && limits.Boards != nil && limits.Boards.Cards != nil {
cardLimit = *limits.Boards.Cards
}
if oldCardLimit != cardLimit {
a.logger.Info(
"setting new cloud limits",
mlog.Int("oldCardLimit", oldCardLimit),
mlog.Int("cardLimit", cardLimit),
)
a.SetCardLimit(cardLimit)
return a.doUpdateCardLimitTimestamp()
}
a.logger.Info(
"setting new cloud limits, equivalent to the existing ones",
mlog.Int("cardLimit", cardLimit),
)
return nil
}
// doUpdateCardLimitTimestamp performs the update without running any
// checks.
func (a *App) doUpdateCardLimitTimestamp() error {
cardLimitTimestamp, err := a.store.UpdateCardLimitTimestamp(a.CardLimit())
if err != nil {
return err
}
a.wsAdapter.BroadcastCardLimitTimestampChange(cardLimitTimestamp)
return nil
}
// UpdateCardLimitTimestamp checks if the server is a cloud instance
// with limits applied, and if that's true, recalculates the card
// limit timestamp and propagates the new one to the connected
// clients.
func (a *App) UpdateCardLimitTimestamp() error {
if !a.IsCloudLimited() {
return nil
}
return a.doUpdateCardLimitTimestamp()
}
// getTemplateMapForBlocks gets all board ids for the blocks, and
// builds a map with the board IDs as the key and their isTemplate
// field as the value.
func (a *App) getTemplateMapForBlocks(blocks []model.Block) (map[string]bool, error) {
boardMap := map[string]*model.Board{}
for _, block := range blocks {
if _, ok := boardMap[block.BoardID]; !ok {
board, err := a.store.GetBoard(block.BoardID)
if err != nil {
return nil, err
}
boardMap[block.BoardID] = board
}
}
templateMap := map[string]bool{}
for boardID, board := range boardMap {
templateMap[boardID] = board.IsTemplate
}
return templateMap, nil
}
// ApplyCloudLimits takes a set of blocks and, if the server is cloud
// limited, limits those that are outside of the card limit and don't
// belong to a template.
func (a *App) ApplyCloudLimits(blocks []model.Block) ([]model.Block, error) {
// if there is no limit currently being applied, return
if !a.IsCloudLimited() {
return blocks, nil
}
cardLimitTimestamp, err := a.store.GetCardLimitTimestamp()
if err != nil {
return nil, err
}
templateMap, err := a.getTemplateMapForBlocks(blocks)
if err != nil {
return nil, err
}
limitedBlocks := make([]model.Block, len(blocks))
for i, block := range blocks {
// if the block belongs to a template, it will never be
// limited
if isTemplate, ok := templateMap[block.BoardID]; ok && isTemplate {
limitedBlocks[i] = block
continue
}
if block.ShouldBeLimited(cardLimitTimestamp) {
limitedBlocks[i] = block.GetLimited()
} else {
limitedBlocks[i] = block
}
}
return limitedBlocks, nil
}
// ContainsLimitedBlocks checks if a list of blocks contain any block
// that references a limited card.
func (a *App) ContainsLimitedBlocks(blocks []model.Block) (bool, error) {
cardLimitTimestamp, err := a.store.GetCardLimitTimestamp()
if err != nil {
return false, err
}
if cardLimitTimestamp == 0 {
return false, nil
}
cards := []model.Block{}
cardIDMap := map[string]bool{}
for _, block := range blocks {
switch block.Type {
case model.TypeCard:
cards = append(cards, block)
default:
cardIDMap[block.ParentID] = true
}
}
cardIDs := []string{}
// if the card is already present on the set, we don't need to
// fetch it from the database
for cardID := range cardIDMap {
alreadyPresent := false
for _, card := range cards {
if card.ID == cardID {
alreadyPresent = true
break
}
}
if !alreadyPresent {
cardIDs = append(cardIDs, cardID)
}
}
if len(cardIDs) > 0 {
fetchedCards, fErr := a.store.GetBlocksByIDs(cardIDs)
if fErr != nil {
return false, fErr
}
cards = append(cards, fetchedCards...)
}
templateMap, err := a.getTemplateMapForBlocks(cards)
if err != nil {
return false, err
}
for _, card := range cards {
isTemplate, ok := templateMap[card.BoardID]
if !ok {
return false, newErrBoardNotFoundInTemplateMap(card.BoardID)
}
// if the block belongs to a template, it will never be
// limited
if isTemplate {
continue
}
if card.ShouldBeLimited(cardLimitTimestamp) {
return true, nil
}
}
return false, nil
}
type errBoardNotFoundInTemplateMap struct {
id string
}
func newErrBoardNotFoundInTemplateMap(id string) *errBoardNotFoundInTemplateMap {
return &errBoardNotFoundInTemplateMap{id}
}
func (eb *errBoardNotFoundInTemplateMap) Error() string {
return fmt.Sprintf("board %q not found in template map", eb.id)
}
func (a *App) NotifyPortalAdminsUpgradeRequest(teamID string) error {
if a.servicesAPI == nil {
return ErrNilPluginAPI
}
team, err := a.store.GetTeam(teamID)
if err != nil {
return err
}
var ofWhat string
if team == nil {
ofWhat = "your organization"
} else {
ofWhat = team.Title
}
message := fmt.Sprintf("A member of %s has notified you to upgrade this workspace before the trial ends.", ofWhat)
page := 0
getUsersOptions := &mmModel.UserGetOptions{
Active: true,
Role: mmModel.SystemAdminRoleId,
PerPage: 50,
Page: page,
}
for ; true; page++ {
getUsersOptions.Page = page
systemAdmins, appErr := a.servicesAPI.GetUsersFromProfiles(getUsersOptions)
if appErr != nil {
a.logger.Error("failed to fetch system admins", mlog.Int("page_size", getUsersOptions.PerPage), mlog.Int("page", page), mlog.Err(appErr))
return appErr
}
if len(systemAdmins) == 0 {
break
}
receiptUserIDs := []string{}
for _, systemAdmin := range systemAdmins {
receiptUserIDs = append(receiptUserIDs, systemAdmin.Id)
}
if err := a.store.SendMessage(message, "custom_cloud_upgrade_nudge", receiptUserIDs); err != nil {
return err
}
}
return nil
}