Calculate total rows in migration, centralise quota enabled logic and tidy to use env vars only

This commit is contained in:
Rory Powell 2022-01-17 12:44:53 +00:00
parent a12a30c8ad
commit 8fc60af820
10 changed files with 175 additions and 46 deletions

View File

@ -99,7 +99,9 @@ spec:
- name: PLATFORM_URL - name: PLATFORM_URL
value: {{ .Values.globals.platformUrl | quote }} value: {{ .Values.globals.platformUrl | quote }}
- name: USE_QUOTAS - name: USE_QUOTAS
value: "1" value: {{ .Values.globals.useQuotas | quote }}
- name: EXCLUDE_QUOTAS_TENANTS
value: {{ .Values.globals.excludeQuotasTenants | quote }}
- name: ACCOUNT_PORTAL_URL - name: ACCOUNT_PORTAL_URL
value: {{ .Values.globals.accountPortalUrl | quote }} value: {{ .Values.globals.accountPortalUrl | quote }}
- name: ACCOUNT_PORTAL_API_KEY - name: ACCOUNT_PORTAL_API_KEY

View File

@ -93,6 +93,8 @@ globals:
logLevel: info logLevel: info
selfHosted: "1" # set to 0 for budibase cloud environment, set to 1 for self-hosted setup selfHosted: "1" # set to 0 for budibase cloud environment, set to 1 for self-hosted setup
multiTenancy: "0" # set to 0 to disable multiple orgs, set to 1 to enable multiple orgs multiTenancy: "0" # set to 0 to disable multiple orgs, set to 1 to enable multiple orgs
useQuotas: "0"
excludeQuotasTenants: "" # comma seperated list of tenants to exclude from quotas
accountPortalUrl: "" accountPortalUrl: ""
accountPortalApiKey: "" accountPortalApiKey: ""
cookieDomain: "" cookieDomain: ""
@ -239,7 +241,8 @@ couchdb:
hosts: hosts:
- chart-example.local - chart-example.local
path: / path: /
annotations: [] annotations:
[]
# kubernetes.io/ingress.class: nginx # kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true" # kubernetes.io/tls-acme: "true"
tls: tls:

View File

@ -1,6 +1,5 @@
const rowController = require("../../api/controllers/row") const rowController = require("../../api/controllers/row")
const automationUtils = require("../automationUtils") const automationUtils = require("../automationUtils")
const env = require("../../environment")
const usage = require("../../utilities/usageQuota") const usage = require("../../utilities/usageQuota")
const { buildCtx } = require("./utils") const { buildCtx } = require("./utils")
@ -83,9 +82,7 @@ exports.run = async function ({ inputs, appId, emitter }) {
inputs.row.tableId, inputs.row.tableId,
inputs.row inputs.row
) )
if (env.USE_QUOTAS) {
await usage.update(usage.Properties.ROW, 1) await usage.update(usage.Properties.ROW, 1)
}
await rowController.save(ctx) await rowController.save(ctx)
return { return {
row: inputs.row, row: inputs.row,

View File

@ -38,6 +38,7 @@ module.exports = {
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY, MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY, MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
USE_QUOTAS: process.env.USE_QUOTAS, USE_QUOTAS: process.env.USE_QUOTAS,
EXCLUDE_QUOTAS_TENANTS: process.env.EXCLUDE_QUOTAS_TENANTS,
REDIS_URL: process.env.REDIS_URL, REDIS_URL: process.env.REDIS_URL,
REDIS_PASSWORD: process.env.REDIS_PASSWORD, REDIS_PASSWORD: process.env.REDIS_PASSWORD,
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY, INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,

View File

@ -38,7 +38,7 @@ module S3Module {
signatureVersion: { signatureVersion: {
type: "string", type: "string",
required: false, required: false,
default: "v4" default: "v4",
}, },
}, },
query: { query: {

View File

@ -1,11 +1,5 @@
jest.mock("../../db") jest.mock("../../db")
jest.mock("../../utilities/usageQuota") jest.mock("../../utilities/usageQuota")
jest.mock("../../environment", () => ({
isTest: () => true,
isProd: () => false,
isDev: () => true,
_set: () => {},
}))
jest.mock("@budibase/backend-core/tenancy", () => ({ jest.mock("@budibase/backend-core/tenancy", () => ({
getTenantId: () => "testing123" getTenantId: () => "testing123"
})) }))
@ -32,6 +26,7 @@ class TestConfiguration {
url: "/applications" url: "/applications"
} }
} }
usageQuota.useQuotas = () => true
} }
executeMiddleware() { executeMiddleware() {
@ -113,12 +108,10 @@ describe("usageQuota middleware", () => {
it("calculates and persists the correct usage quota for the relevant action", async () => { it("calculates and persists the correct usage quota for the relevant action", async () => {
config.setUrl("/rows") config.setUrl("/rows")
config.setProd(true)
await config.executeMiddleware() await config.executeMiddleware()
// expect(usageQuota.update).toHaveBeenCalledWith("rows", 1) expect(usageQuota.update).toHaveBeenCalledWith("rows", 1)
expect(usageQuota.update).not.toHaveBeenCalledWith("rows", 1)
expect(config.next).toHaveBeenCalled() expect(config.next).toHaveBeenCalled()
}) })

View File

@ -1,18 +1,11 @@
const CouchDB = require("../db") const CouchDB = require("../db")
const usageQuota = require("../utilities/usageQuota") const usageQuota = require("../utilities/usageQuota")
const env = require("../environment")
const { getTenantId } = require("@budibase/backend-core/tenancy")
const { const {
isExternalTable, isExternalTable,
isRowId: isExternalRowId, isRowId: isExternalRowId,
} = require("../integrations/utils") } = require("../integrations/utils")
const quotaMigration = require("../migrations/sync_app_and_reset_rows_quotas") const quotaMigration = require("../migrations/sync_app_and_reset_rows_quotas")
const testing = false
// tenants without limits
const EXCLUDED_TENANTS = ["bb", "default", "bbtest", "bbstaging"]
// currently only counting new writes and deletes // currently only counting new writes and deletes
const METHOD_MAP = { const METHOD_MAP = {
POST: 1, POST: 1,
@ -20,7 +13,7 @@ const METHOD_MAP = {
} }
const DOMAIN_MAP = { const DOMAIN_MAP = {
// rows: usageQuota.Properties.ROW, // works - disabled rows: usageQuota.Properties.ROW,
// upload: usageQuota.Properties.UPLOAD, // doesn't work yet // upload: usageQuota.Properties.UPLOAD, // doesn't work yet
// views: usageQuota.Properties.VIEW, // doesn't work yet // views: usageQuota.Properties.VIEW, // doesn't work yet
// users: usageQuota.Properties.USER, // doesn't work yet // users: usageQuota.Properties.USER, // doesn't work yet
@ -39,13 +32,7 @@ function getProperty(url) {
} }
module.exports = async (ctx, next) => { module.exports = async (ctx, next) => {
const tenantId = getTenantId() if (!usageQuota.useQuotas()) {
// if in development or a self hosted cloud usage quotas should not be executed
if (
(env.isDev() || env.SELF_HOSTED || EXCLUDED_TENANTS.includes(tenantId)) &&
!testing
) {
return next() return next()
} }

View File

@ -6,25 +6,80 @@ const {
const { getGlobalDB } = require("@budibase/backend-core/tenancy") const { getGlobalDB } = require("@budibase/backend-core/tenancy")
const { getAllApps } = require("@budibase/backend-core/db") const { getAllApps } = require("@budibase/backend-core/db")
const CouchDB = require("../db") const CouchDB = require("../db")
const { getUsageQuotaDoc } = require("../utilities/usageQuota") const { getUsageQuotaDoc, useQuotas } = require("../utilities/usageQuota")
const { getRowParams } = require("../db/utils")
/**
* Get all rows in the given app ids.
*
* The returned rows may contan duplicates if there
* is a production and dev app.
*/
const getAllRows = async appIds => {
const allRows = []
let appDb
for (let appId of appIds) {
try {
appDb = new CouchDB(appId)
const response = await appDb.allDocs(
getRowParams(null, null, {
include_docs: false,
})
)
allRows.push(...response.rows.map(r => r.id))
} catch (e) {
// don't error out if we can't count the app rows, just continue
}
}
return allRows
}
/**
* Get all rows in the given app ids.
*
* The returned rows will be unique, duplicated rows across
* production and dev apps will be removed.
*/
const getUniqueRows = async appIds => {
const allRows = await getAllRows(appIds)
return new Set(allRows)
}
const syncRowsQuota = async db => {
// get all rows in all apps
const allApps = await getAllApps(CouchDB, { all: true })
const appIds = allApps ? allApps.map(app => app.appId) : []
const rows = await getUniqueRows(appIds)
// sync row count
const usageDoc = await getUsageQuotaDoc(db)
usageDoc.usageQuota.rows = rows.size
await db.put(usageDoc)
}
const syncAppsQuota = async db => {
// get app count
const devApps = await getAllApps(CouchDB, { dev: true })
const appCount = devApps ? devApps.length : 0
// sync app count
const usageDoc = await getUsageQuotaDoc(db)
usageDoc.usageQuota.apps = appCount
await db.put(usageDoc)
}
exports.runIfRequired = async () => { exports.runIfRequired = async () => {
await migrateIfRequired( await migrateIfRequired(
MIGRATION_DBS.GLOBAL_DB, MIGRATION_DBS.GLOBAL_DB,
MIGRATIONS.SYNC_APP_AND_RESET_ROWS_QUOTAS, MIGRATIONS.SYNC_APP_AND_RESET_ROWS_QUOTAS,
async () => { async () => {
if (!useQuotas()) {
return
}
const db = getGlobalDB() const db = getGlobalDB()
const usageDoc = await getUsageQuotaDoc(db) await syncAppsQuota(db)
await syncRowsQuota(db)
// reset the rows
usageDoc.usageQuota.rows = 0
// sync the apps
const apps = await getAllApps(CouchDB, { dev: true })
const appCount = apps ? apps.length : 0
usageDoc.usageQuota.apps = appCount
await db.put(usageDoc)
} }
) )
} }

View File

@ -0,0 +1,72 @@
const getTenantId = jest.fn()
jest.mock("@budibase/backend-core/tenancy", () => ({
getTenantId
}))
const usageQuota = require("../usageQuota")
const env = require("../../environment")
class TestConfiguration {
constructor() {
this.enableQuotas()
}
enableQuotas = () => {
env.USE_QUOTAS = 1
}
disableQuotas = () => {
env.USE_QUOTAS = null
}
setTenantId = (tenantId) => {
getTenantId.mockReturnValue(tenantId)
}
setExcludedTenants = (tenants) => {
env.EXCLUDE_QUOTAS_TENANTS = tenants
}
reset = () => {
this.disableQuotas()
this.setExcludedTenants(null)
}
}
describe("usageQuota", () => {
let config
beforeEach(() => {
config = new TestConfiguration()
})
afterEach(() => {
config.reset()
})
describe("useQuotas", () => {
it("works when no settings have been provided", () => {
config.reset()
expect(usageQuota.useQuotas()).toBe(false)
})
it("honours USE_QUOTAS setting", () => {
config.disableQuotas()
expect(usageQuota.useQuotas()).toBe(false)
config.enableQuotas()
expect(usageQuota.useQuotas()).toBe(true)
})
it("honours EXCLUDE_QUOTAS_TENANTS setting", () => {
config.setTenantId("test")
// tenantId is in the list
config.setExcludedTenants("test, test2, test2")
expect(usageQuota.useQuotas()).toBe(false)
config.setExcludedTenants("test,test2,test2")
expect(usageQuota.useQuotas()).toBe(false)
// tenantId is not in the list
config.setTenantId("other")
expect(usageQuota.useQuotas()).toBe(true)
})
})
})

View File

@ -1,12 +1,31 @@
const env = require("../environment") const env = require("../environment")
const { getGlobalDB } = require("@budibase/backend-core/tenancy") const { getGlobalDB, getTenantId } = require("@budibase/backend-core/tenancy")
const { const {
StaticDatabases, StaticDatabases,
generateNewUsageQuotaDoc, generateNewUsageQuotaDoc,
} = require("@budibase/backend-core/db") } = require("@budibase/backend-core/db")
exports.useQuotas = () => {
// check if quotas are enabled
if (env.USE_QUOTAS) {
// check if there are any tenants without limits
if (env.EXCLUDE_QUOTAS_TENANTS) {
const excludedTenants = env.EXCLUDE_QUOTAS_TENANTS.replace(
/\s/g,
""
).split(",")
const tenantId = getTenantId()
if (excludedTenants.includes(tenantId)) {
return false
}
}
return true
}
return false
}
exports.Properties = { exports.Properties = {
ROW: "rows", // mostly works - disabled - app / table deletion not yet accounted for ROW: "rows", // mostly works - app / table deletion not yet accounted for
UPLOAD: "storage", // doesn't work yet UPLOAD: "storage", // doesn't work yet
VIEW: "views", // doesn't work yet VIEW: "views", // doesn't work yet
USER: "users", // doesn't work yet USER: "users", // doesn't work yet
@ -37,7 +56,7 @@ exports.getUsageQuotaDoc = async db => {
* also been reset after this call. * also been reset after this call.
*/ */
exports.update = async (property, usage) => { exports.update = async (property, usage) => {
if (!env.USE_QUOTAS) { if (!exports.useQuotas()) {
return return
} }