diff --git a/src/backend/src/CoreModule.js b/src/backend/src/CoreModule.js index 52d5cd01..ae8f738c 100644 --- a/src/backend/src/CoreModule.js +++ b/src/backend/src/CoreModule.js @@ -331,6 +331,9 @@ const install = async ({ services, app, useapi, modapi }) => { const { KernelInfoService } = require('./services/KernelInfoService'); services.registerService('kernel-info', KernelInfoService); + + const { DriverUsagePolicyService } = require('./services/drivers/DriverUsagePolicyService'); + services.registerService('driver-usage-policy', DriverUsagePolicyService); } const install_legacy = async ({ services }) => { diff --git a/src/backend/src/definitions/Driver.js b/src/backend/src/definitions/Driver.js index ea1e4425..4bf79af5 100644 --- a/src/backend/src/definitions/Driver.js +++ b/src/backend/src/definitions/Driver.js @@ -25,6 +25,9 @@ const { CodeUtil } = require("../codex/CodeUtil"); /** * Base class for all driver implementations. + * + * @deprecated - we use traits on services now. This class is kept for compatibility + * with EntityStoreImplementation and DBKVStore which still use this. */ class Driver extends AdvancedBase { constructor (...a) { @@ -169,6 +172,7 @@ class Driver extends AdvancedBase { 'driver.interface': this.constructor.INTERFACE, 'driver.implementation': this.constructor.ID, 'driver.method': method, + ...(this.get_usage_extra ? this.get_usage_extra() : {}), }; await svc_monthlyUsage.increment(actor, method_key, extra); } diff --git a/src/backend/src/drivers/EntityStoreImplementation.js b/src/backend/src/drivers/EntityStoreImplementation.js index 8b1c7432..a84ae8bd 100644 --- a/src/backend/src/drivers/EntityStoreImplementation.js +++ b/src/backend/src/drivers/EntityStoreImplementation.js @@ -90,6 +90,12 @@ class EntityStoreImplementation extends Driver { super(); this.service = service; } + get_usage_extra () { + return { + ['driver.interface']: 'puter-es', + ['driver.implementation']: 'puter-es:' + this.service, + }; + } static METHODS = { create: async function ({ object, options }) { const svc_es = this.services.get(this.service); diff --git a/src/backend/src/routers/drivers/usage.js b/src/backend/src/routers/drivers/usage.js index 5d53f7bb..dd44c4cf 100644 --- a/src/backend/src/routers/drivers/usage.js +++ b/src/backend/src/routers/drivers/usage.js @@ -62,29 +62,35 @@ module.exports = eggspress('/drivers/usage', { const app = await get_app({ id: row.app_id }); const identifying_fields = { - service: row.extra, + service: JSON.parse(row.extra), year: row.year, month: row.month, }; + + // EntityStorage identifiers weren't tracked properly. We don't realy need + // to track or show them, so this isn't a huge deal, but we need to make + // sure they don't populate garbage data into the usage report. + if ( ! identifying_fields.service['driver.implementation'] ) { + continue; + } + + const svc_driverUsage = req.services.get('driver-usage-policy'); + const policy = await svc_driverUsage.get_effective_policy({ + actor, + service_name: identifying_fields.service['driver.implementation'], + trait_name: identifying_fields.service['driver.interface'], + }); + + // console.log(`POLICY FOR ${identifying_fields.service['driver.implementation']} ${identifying_fields.service['driver.interface']}`, policy); + const user_usage_key = hash_serializable_object(identifying_fields); if ( ! usages.user[user_usage_key] ) { usages.user[user_usage_key] = { ...identifying_fields, + policy, }; - - const method_key = row.extra['driver.implementation'] + - ':' + row.extra['driver.method']; - const sla_key = `driver:impl:${method_key}`; - - const svc_sla = x.get('services').get('sla'); - const sla = await svc_sla.get( - user_is_verified ? 'user_verified' : 'user_unverified', - sla_key - ); - - usages.user[user_usage_key].monthly_limit = - sla?.monthly_limit || null; + usages.user[user_usage_key].monthly_limit = policy?.['monthy-limit'] ?? null; } usages.user[user_usage_key].monthly_usage = @@ -109,7 +115,8 @@ module.exports = eggspress('/drivers/usage', { const app_usage_key = hash_serializable_object(id_plus_app); - if ( ! app_usages[app_usage_key] ) { + // DISABLED FOR NOW: need to rework this for the new policy system + if ( false ) if ( ! app_usages[app_usage_key] ) { app_usages[app_usage_key] = { ...identifying_fields, }; diff --git a/src/backend/src/services/ConfigurableCountingService.js b/src/backend/src/services/ConfigurableCountingService.js index 18907bb1..bd4966b7 100644 --- a/src/backend/src/services/ConfigurableCountingService.js +++ b/src/backend/src/services/ConfigurableCountingService.js @@ -132,6 +132,20 @@ class ConfigurableCountingService extends BaseService { pricing_category: JSON.stringify(pricing_category), }; + const duplicate_update_part = + `count = count + 1${ + custom_col_names.length > 0 ? ', ' : '' + } ${ + custom_col_names.map((name) => `${name} = ${name} + ?`).join(', ') + }`; + + const identifying_keys = [ + `year`, `month`, + `service_type`, `service_name`, + `actor_key`, + `pricing_category_hash` + ] + const sql = `INSERT INTO monthly_usage_counts (${ Object.keys(required_data).join(', ') @@ -141,11 +155,13 @@ class ConfigurableCountingService extends BaseService { `VALUES (${ Object.keys(required_data).map(() => '?').join(', ') }, 1, ${custom_col_values.map(() => '?').join(', ')}) ` + - `ON DUPLICATE KEY UPDATE count = count + 1${ - custom_col_names.length > 0 ? ', ' : '' - } ${ - custom_col_names.map((name) => `${name} = ${name} + ?`).join(', ') - }`; + this.db.case({ + mysql: 'ON DUIPLICATE KEY UPDATE ' + duplicate_update_part, + sqlite: `ON CONFLICT(${ + identifying_keys.map(v => `\`${v}\``).join(', ') + }) DO UPDATE SET ${duplicate_update_part}`, + }) + ; const value_array = [ ...Object.values(required_data), diff --git a/src/backend/src/services/drivers/DriverUsagePolicyService.js b/src/backend/src/services/drivers/DriverUsagePolicyService.js new file mode 100644 index 00000000..31a9b796 --- /dev/null +++ b/src/backend/src/services/drivers/DriverUsagePolicyService.js @@ -0,0 +1,88 @@ +const APIError = require("../../api/APIError"); +const { PermissionUtil } = require("../auth/PermissionService"); +const BaseService = require("../BaseService"); + +// DO WE HAVE enough information to get the policy for the newer drivers? +// - looks like it: service:: + +class DriverUsagePolicyService extends BaseService { + async get_policies_for_option_ (option) { + // NOT FINAL: before implementing cascading monthly usage, + // this return will be removed and the code below it will + // be uncommented + return option.path; + /* + const svc_systemData = this.services.get('system-data'); + const svc_su = this.services.get('su'); + + const policies = await Promise.all(option.path.map(async path_node => { + const policy = await svc_su.sudo(async () => { + return await svc_systemData.interpret(option.data); + }); + return { + ...path_node, + policy, + }; + })); + return policies; + */ + } + + async select_best_option_ (options) { + return options[0]; + } + + // TODO: DRY: This is identical to the method of the same name in + // DriverService, except after the line with a comment containing + // the string "[DEVIATION]". + async get_effective_policy ({ actor, service_name, trait_name }) { + const svc_permission = this.services.get('permission'); + const reading = await svc_permission.scan( + actor, + PermissionUtil.join('service', service_name, 'ii', trait_name), + ); + console.log({ + perm: PermissionUtil.join('service', service_name, 'ii', trait_name), + reading: require('util').inspect(reading, { depth: null }), + }); + const options = PermissionUtil.reading_to_options(reading); + console.log('OPTIONS', JSON.stringify(options, undefined, ' ')); + if ( options.length <= 0 ) { + return undefined; + } + const option = await this.select_best_option_(options); + const policies = await this.get_policies_for_option_(option); + console.log('SLA', JSON.stringify(policies, undefined, ' ')); + + // NOT FINAL: For now we apply monthly usage logic + // to the first holder of the permission. Later this + // will be changed so monthly usage can cascade across + // multiple actors. I decided not to implement this + // immediately because it's a hefty time sink and it's + // going to be some time before we can offer this feature + // to the end-user either way. + + let effective_policy = null; + for ( const policy of policies ) { + if ( policy.holder ) { + effective_policy = policy; + break; + } + } + + // === [DEVIATION] In DriverService, this is part of call_new_ === + const svc_systemData = this.services.get('system-data'); + const svc_su = this.services.get('su'); + effective_policy = await svc_su.sudo(async () => { + return await svc_systemData.interpret(effective_policy.data); + }); + + effective_policy = effective_policy.policy; + + return effective_policy; + } +} + +module.exports = { + DriverUsagePolicyService, +}; diff --git a/src/gui/src/UI/Settings/UITabUsage.js b/src/gui/src/UI/Settings/UITabUsage.js index b7773f5c..761312b3 100644 --- a/src/gui/src/UI/Settings/UITabUsage.js +++ b/src/gui/src/UI/Settings/UITabUsage.js @@ -62,12 +62,17 @@ export default { const { monthly_limit, monthly_usage } = service; let usageDisplay = ``; + const first_identifier = false || + service.service['driver.implementation'] || + service.service['driver.interface'] || + ''; + if (monthly_limit !== null) { let usage_percentage = (monthly_usage / monthly_limit * 100).toFixed(0); usage_percentage = usage_percentage > 100 ? 100 : usage_percentage; // Cap at 100% usageDisplay = `
-

${html_encode(service.service['driver.interface'])} (${html_encode(service.service['driver.method'])}):

+

${html_encode(first_identifier)} (${html_encode(service.service['driver.method'])}):

${monthly_usage} used of ${monthly_limit}
${usage_percentage}%
@@ -78,7 +83,7 @@ export default { else { usageDisplay = `
-

${html_encode(service.service['driver.interface'])} (${html_encode(service.service['driver.method'])}):

+

${html_encode(first_identifier)} (${html_encode(service.service['driver.method'])}):

${i18n('usage')}: ${monthly_usage} (${i18n('unlimited')})
`;