mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 11:05:39 -04:00
[Core] Implement Kibana Pricing Tiers service (#221281)
## 📓 Summary Closes https://github.com/elastic/observability-dev/issues/4490 We need to introduce a tier concept in Kibana to offer tiered versions of our Observability solution. Some time ago, the security team did something similar to introduce tiered product lines, and this work lays the foundation for a Kibana shared solution that can work with product tiers. This PoC introduces a new core `PricingService` to manage feature availability based on subscription tiers in serverless deployments. The implementation enables: - Tier-aware feature registration. - Dynamic configuration loading based on `serverless.<project-type>.<tier>.yml` files. - Client/server APIs for feature checks based on available tier (`isFeatureAvailable()`) - Backwards compatibility with existing project types (Search, Observability, Security, Chat) ## 🚶 Architecture walkthrough ### Configuration To have a mechanism that allows for performing changes at the configuration level, the `PricingService` loads and validates a strict configuration of product tiers. The available configuration is defined as follows: ```yml # serverless.oblt.yml (default config) pricing.tiers.enabled: true pricing.tiers.products: - name: observability tier: complete # serverless.oblt.complete.yml xpack.infra.enabled: true xpack.slo.enabled: true xpack.features.overrides: // Etc... # serverless.oblt.essentials.yml xpack.infra.enabled: false xpack.slo.enabled: false // Etc... # Optionally, security solution could set their product tiers from their configuration files, as they are already supported by the core pricing service # serverless.security.yml pricing.tiers.enabled: true pricing.tiers.products: - name: security tier: complete - name: endpoint tier: complete - name: cloud tier: complete ``` The Control Plane team will update how the kibana-controller inject the configuration in the `kibana.yml` file to reflect the values set during the project creation. It will also auto-load any optional `serverless.<project-type>.<tier>.yml` matching the config definition when tiers are enabled. The configuration is strictly validated against typed products. It might be very opinionated to store this validation in a core package, but it ensures strong typing across the codebase to register product features and ensure a fast-failing check in case the product tiers do not exist. ### Pricing service As far as the configuration-based approach is great to disable whole plugins and override configs, it doesn't cover all the scenarios where a specific tier wants to limit minor parts of a plugin feature (e.g., for o11y limited onboarding, we cannot disable the whole plugin). To cover these cases, the `PricingService` exposes a mechanism to register features associated with specific tiers. This is unbound from the `KibanaFeatureRegistry` mechanism, and we could consider extending its behaviour in the future as needed. Once product features are registered, the core start contract exposes a simple client to detect when a feature is available. <img width="1453" alt="Screenshot 2025-05-23 at 12 35 11" src="https://github.com/user-attachments/assets/05267c00-afe0-49c6-b518-b1ce8f4a0546" /> ## ✏️ Usage To launch Kibana with a specific product tier (e.g. observability `essentials`, mostly limiting to logs features), update the `pricing.tiers.products` in the serverless project configuration file: ```yml # serverless.oblt.yml pricing.tiers.enabled: true pricing.tiers.products: - name: observability tier: essentials ``` The above will run a Kibana serverless project in this specific tier and will load the configuration defined in `serverless.oblt.essentials.yml`, which currently disables a couple of plugins. Given this context, let's take a limited o11y onboarding when the subscription tier is `essentials`: ```ts // x-pack/solutions/observability/plugins/observability_onboarding/server/plugin.ts export class ObservabilityOnboardingPlugin { public setup(core: CoreSetup) { // ... // Register a feature available only on the complete tier core.pricing.registerProductFeatures([ { id: 'observability-complete-onboarding', products: [{ name: 'observability', tier: 'complete' }], }, ]); // ... } } // x-pack/solutions/observability/plugins/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx const { core } = useKibana(); const isCompleteOnboardingAvailable = core.pricing.tiers.isFeatureAvailable('observability-complete-onboarding'); if (isCompleteOnboardingAvailable) { return <CompleteOnboarding />; } else { return <EssentialsOnboarding />; } ``` The results of the above changes will look as follows once Kibana is running: | Complete tier | Essentials tier | |--------|--------| | <img width="2998" alt="Screenshot 2025-05-23 at 13 51 14" src="https://github.com/user-attachments/assets/bcf7c791-4623-42e4-91ce-0622d981e7e7" /> | <img width="2996" alt="Screenshot 2025-05-23 at 13 53 36" src="https://github.com/user-attachments/assets/429c82eb-761c-4aa1-b13d-81ac95301e60" /> | The tiers client is also available server-side through the `getStartServices()` function, such that ad-hoc activity/registrations can be performed. ## 👣 Next Steps - [x] Implement pending core tests - [x] Document API usage in README --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
ff9f47b22d
commit
657c0aa8b4
99 changed files with 2318 additions and 70 deletions
9
.github/CODEOWNERS
vendored
9
.github/CODEOWNERS
vendored
|
@ -238,6 +238,13 @@ src/core/packages/plugins/server-mocks @elastic/kibana-core
|
|||
src/core/packages/preboot/server @elastic/kibana-core
|
||||
src/core/packages/preboot/server-internal @elastic/kibana-core
|
||||
src/core/packages/preboot/server-mocks @elastic/kibana-core
|
||||
src/core/packages/pricing/browser @elastic/kibana-core
|
||||
src/core/packages/pricing/browser-internal @elastic/kibana-core
|
||||
src/core/packages/pricing/browser-mocks @elastic/kibana-core
|
||||
src/core/packages/pricing/common @elastic/kibana-core
|
||||
src/core/packages/pricing/server @elastic/kibana-core
|
||||
src/core/packages/pricing/server-internal @elastic/kibana-core
|
||||
src/core/packages/pricing/server-mocks @elastic/kibana-core
|
||||
src/core/packages/rendering/browser @elastic/kibana-core
|
||||
src/core/packages/rendering/browser-internal @elastic/kibana-core
|
||||
src/core/packages/rendering/browser-mocks @elastic/kibana-core
|
||||
|
@ -1913,6 +1920,8 @@ src/platform/plugins/shared/discover/public/context_awareness/profile_providers/
|
|||
/config/serverless.chat.yml @elastic/kibana-core @elastic/kibana-security @elastic/search-kibana
|
||||
/config/serverless.es.yml @elastic/kibana-core @elastic/kibana-security @elastic/search-kibana
|
||||
/config/serverless.oblt.yml @elastic/kibana-core @elastic/kibana-security @elastic/observability-ui
|
||||
/config/serverless.oblt.complete.yml @elastic/kibana-core @elastic/observability-ui
|
||||
/config/serverless.oblt.essentials.yml @elastic/kibana-core @elastic/observability-ui
|
||||
/config/serverless.security.yml @elastic/kibana-core @elastic/security-solution @elastic/kibana-security
|
||||
/config/serverless.security.search_ai_lake.yml @elastic/security-solution @elastic/kibana-security
|
||||
/config/serverless.security.essentials.yml @elastic/security-solution @elastic/kibana-security
|
||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -66,6 +66,8 @@ webpackstats.json
|
|||
!/config/serverless.es.yml
|
||||
!/config/serverless.chat.yml
|
||||
!/config/serverless.oblt.yml
|
||||
!/config/serverless.oblt.complete.yml
|
||||
!/config/serverless.oblt.essentials.yml
|
||||
!/config/serverless.security.yml
|
||||
!/config/serverless.security.essentials.yml
|
||||
!/config/serverless.security.complete.yml
|
||||
|
|
63
config/serverless.oblt.complete.yml
Normal file
63
config/serverless.oblt.complete.yml
Normal file
|
@ -0,0 +1,63 @@
|
|||
# Observability Complete tier config
|
||||
|
||||
## Enabled plugins
|
||||
xpack.infra.enabled: true
|
||||
xpack.slo.enabled: true
|
||||
|
||||
xpack.features.overrides:
|
||||
### Applications feature privileges are fine-tuned to grant access to Logs, and Observability apps.
|
||||
apm:
|
||||
### By default, this feature named as `APM and User Experience`, but should be renamed to `Applications`.
|
||||
name: "Applications"
|
||||
privileges:
|
||||
# Infrastructure's `All` feature privilege should implicitly grant `All` access to Logs and Observability apps.
|
||||
all.composedOf:
|
||||
- feature: "logs"
|
||||
privileges: [ "all" ]
|
||||
- feature: "observability"
|
||||
privileges: [ "all" ]
|
||||
# Infrastructure's `Read` feature privilege should implicitly grant `Read` access to Logs and Observability apps.
|
||||
read.composedOf:
|
||||
- feature: "logs"
|
||||
privileges: [ "read" ]
|
||||
- feature: "observability"
|
||||
privileges: [ "read" ]
|
||||
### Fleet feature privileges are fine-tuned to grant access to Logs app.
|
||||
fleetv2:
|
||||
privileges:
|
||||
# Fleet `All` feature privilege should implicitly grant `All` access to Logs app.
|
||||
all.composedOf:
|
||||
- feature: "logs"
|
||||
privileges: [ "all" ]
|
||||
# Fleet `Read` feature privilege should implicitly grant `Read` access to Logs app.
|
||||
read.composedOf:
|
||||
- feature: "logs"
|
||||
privileges: [ "read" ]
|
||||
infrastructure:
|
||||
### By default, this feature named as `Metrics`, but should be renamed to `Infrastructure`.
|
||||
name: "Infrastructure"
|
||||
privileges:
|
||||
# Infrastructure's `All` feature privilege should implicitly grant `All` access to Logs and Observability apps.
|
||||
all.composedOf:
|
||||
- feature: "logs"
|
||||
privileges: [ "all" ]
|
||||
- feature: "observability"
|
||||
privileges: [ "all" ]
|
||||
# Infrastructure's `Read` feature privilege should implicitly grant `Read` access to Logs and Observability apps.
|
||||
read.composedOf:
|
||||
- feature: "logs"
|
||||
privileges: [ "read" ]
|
||||
- feature: "observability"
|
||||
privileges: [ "read" ]
|
||||
### Logs feature is hidden in Role management since it's automatically granted by either Infrastructure, or Applications features.
|
||||
logs.hidden: true
|
||||
slo:
|
||||
privileges:
|
||||
# SLOs `All` feature privilege should implicitly grant `All` access to Observability app.
|
||||
all.composedOf:
|
||||
- feature: "observability"
|
||||
privileges: [ "all" ]
|
||||
# SLOs `Read` feature privilege should implicitly grant `Read` access to Observability app.
|
||||
read.composedOf:
|
||||
- feature: "observability"
|
||||
privileges: [ "read" ]
|
5
config/serverless.oblt.essentials.yml
Normal file
5
config/serverless.oblt.essentials.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Observability Logs Essentials tier config
|
||||
|
||||
## Disable xpack plugins
|
||||
xpack.infra.enabled: false
|
||||
xpack.slo.enabled: false
|
|
@ -1,34 +1,19 @@
|
|||
# Observability Project config
|
||||
|
||||
## Core pricing tier for observability project
|
||||
pricing.tiers.enabled: true
|
||||
pricing.tiers.products:
|
||||
- name: observability
|
||||
tier: complete
|
||||
|
||||
# Make sure the plugins belonging to this project type are loaded
|
||||
plugins.allowlistPluginGroups: ['platform', 'observability']
|
||||
|
||||
## Enabled plugins
|
||||
xpack.infra.enabled: true
|
||||
|
||||
# Disabled Observability plugins
|
||||
xpack.ux.enabled: false
|
||||
xpack.legacy_uptime.enabled: false
|
||||
|
||||
## Fine-tune the observability solution feature privileges. Also, refer to `serverless.yml` for the project-agnostic overrides.
|
||||
xpack.features.overrides:
|
||||
### Applications feature privileges are fine-tuned to grant access to Logs, and Observability apps.
|
||||
apm:
|
||||
### By default, this feature named as `APM and User Experience`, but should be renamed to `Applications`.
|
||||
name: "Applications"
|
||||
privileges:
|
||||
# Infrastructure's `All` feature privilege should implicitly grant `All` access to Logs and Observability apps.
|
||||
all.composedOf:
|
||||
- feature: "logs"
|
||||
privileges: [ "all" ]
|
||||
- feature: "observability"
|
||||
privileges: [ "all" ]
|
||||
# Infrastructure's `Read` feature privilege should implicitly grant `Read` access to Logs and Observability apps.
|
||||
read.composedOf:
|
||||
- feature: "logs"
|
||||
privileges: [ "read" ]
|
||||
- feature: "observability"
|
||||
privileges: [ "read" ]
|
||||
### Dashboards feature should be moved from Analytics category to the Observability one.
|
||||
dashboard_v2.category: "observability"
|
||||
### Discover feature should be moved from Analytics category to the Observability one and its privileges are
|
||||
|
@ -54,36 +39,6 @@ xpack.features.overrides:
|
|||
read.composedOf:
|
||||
- feature: "observability"
|
||||
privileges: [ "read" ]
|
||||
### Fleet feature privileges are fine-tuned to grant access to Logs app.
|
||||
fleetv2:
|
||||
privileges:
|
||||
# Fleet `All` feature privilege should implicitly grant `All` access to Logs app.
|
||||
all.composedOf:
|
||||
- feature: "logs"
|
||||
privileges: [ "all" ]
|
||||
# Fleet `Read` feature privilege should implicitly grant `Read` access to Logs app.
|
||||
read.composedOf:
|
||||
- feature: "logs"
|
||||
privileges: [ "read" ]
|
||||
### Infrastructure feature privileges are fine-tuned to grant access to Logs, and Observability apps.
|
||||
infrastructure:
|
||||
### By default, this feature named as `Metrics`, but should be renamed to `Infrastructure`.
|
||||
name: "Infrastructure"
|
||||
privileges:
|
||||
# Infrastructure's `All` feature privilege should implicitly grant `All` access to Logs and Observability apps.
|
||||
all.composedOf:
|
||||
- feature: "logs"
|
||||
privileges: [ "all" ]
|
||||
- feature: "observability"
|
||||
privileges: [ "all" ]
|
||||
# Infrastructure's `Read` feature privilege should implicitly grant `Read` access to Logs and Observability apps.
|
||||
read.composedOf:
|
||||
- feature: "logs"
|
||||
privileges: [ "read" ]
|
||||
- feature: "observability"
|
||||
privileges: [ "read" ]
|
||||
### Logs feature is hidden in Role management since it's automatically granted by either Infrastructure, or Applications features.
|
||||
logs.hidden: true
|
||||
### Machine Learning feature should be moved from Analytics category to the Observability one and renamed to `AI Ops`.
|
||||
ml:
|
||||
category: "observability"
|
||||
|
@ -91,17 +46,6 @@ xpack.features.overrides:
|
|||
### Observability feature is hidden in Role management since it's automatically granted by either Discover,
|
||||
### Infrastructure, Applications, Synthetics, or SLOs features.
|
||||
observability.hidden: true
|
||||
### SLOs feature privileges are fine-tuned to grant access to Observability app.
|
||||
slo:
|
||||
privileges:
|
||||
# SLOs `All` feature privilege should implicitly grant `All` access to Observability app.
|
||||
all.composedOf:
|
||||
- feature: "observability"
|
||||
privileges: [ "all" ]
|
||||
# SLOs `Read` feature privilege should implicitly grant `Read` access to Observability app.
|
||||
read.composedOf:
|
||||
- feature: "observability"
|
||||
privileges: [ "read" ]
|
||||
### Stack alerts is hidden in Role management since it's not needed.
|
||||
stackAlerts.hidden: true
|
||||
### Synthetics feature privileges are fine-tuned to grant access to Observability app.
|
||||
|
@ -118,8 +62,6 @@ xpack.features.overrides:
|
|||
- feature: "observability"
|
||||
privileges: [ "read" ]
|
||||
|
||||
## Enable the slo plugin
|
||||
xpack.slo.enabled: true
|
||||
|
||||
## Cloud settings
|
||||
xpack.cloud.serverless.project_type: observability
|
||||
|
@ -127,8 +69,6 @@ xpack.cloud.serverless.project_type: observability
|
|||
## Enable the Serverless Observability plugin
|
||||
xpack.serverless.observability.enabled: true
|
||||
|
||||
## Configure plugins
|
||||
|
||||
## Set the home route
|
||||
uiSettings.overrides.defaultRoute: /app/observability/landing
|
||||
|
||||
|
|
|
@ -376,6 +376,11 @@
|
|||
"@kbn/core-plugins-server-internal": "link:src/core/packages/plugins/server-internal",
|
||||
"@kbn/core-preboot-server": "link:src/core/packages/preboot/server",
|
||||
"@kbn/core-preboot-server-internal": "link:src/core/packages/preboot/server-internal",
|
||||
"@kbn/core-pricing-browser": "link:src/core/packages/pricing/browser",
|
||||
"@kbn/core-pricing-browser-internal": "link:src/core/packages/pricing/browser-internal",
|
||||
"@kbn/core-pricing-common": "link:src/core/packages/pricing/common",
|
||||
"@kbn/core-pricing-server": "link:src/core/packages/pricing/server",
|
||||
"@kbn/core-pricing-server-internal": "link:src/core/packages/pricing/server-internal",
|
||||
"@kbn/core-provider-plugin": "link:src/platform/test/plugin_functional/plugins/core_provider_plugin",
|
||||
"@kbn/core-rendering-browser": "link:src/core/packages/rendering/browser",
|
||||
"@kbn/core-rendering-browser-internal": "link:src/core/packages/rendering/browser-internal",
|
||||
|
@ -1470,6 +1475,8 @@
|
|||
"@kbn/core-plugins-browser-mocks": "link:src/core/packages/plugins/browser-mocks",
|
||||
"@kbn/core-plugins-server-mocks": "link:src/core/packages/plugins/server-mocks",
|
||||
"@kbn/core-preboot-server-mocks": "link:src/core/packages/preboot/server-mocks",
|
||||
"@kbn/core-pricing-browser-mocks": "link:src/core/packages/pricing/browser-mocks",
|
||||
"@kbn/core-pricing-server-mocks": "link:src/core/packages/pricing/server-mocks",
|
||||
"@kbn/core-rendering-browser-mocks": "link:src/core/packages/rendering/browser-mocks",
|
||||
"@kbn/core-rendering-server-mocks": "link:src/core/packages/rendering/server-mocks",
|
||||
"@kbn/core-saved-objects-api-server-mocks": "link:src/core/packages/saved-objects/api-server-mocks",
|
||||
|
|
|
@ -73,6 +73,19 @@ export function compileConfigStack({
|
|||
}
|
||||
}
|
||||
|
||||
// Pricing specific tier configs
|
||||
const config = getConfigFromFiles(configs.filter(isNotNull));
|
||||
const isPricingTiersEnabled = _.get(config, 'pricing.tiers.enabled', false);
|
||||
if (isPricingTiersEnabled) {
|
||||
const tier = getServerlessProjectTierFromConfig(config);
|
||||
if (tier) {
|
||||
configs.push(resolveConfig(`serverless.${serverlessMode}.${tier}.yml`));
|
||||
if (dev && devConfig !== false) {
|
||||
configs.push(resolveConfig(`serverless.${serverlessMode}.${tier}.dev.yml`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return configs.filter(isNotNull);
|
||||
}
|
||||
|
||||
|
@ -100,6 +113,25 @@ function getSecurityTierFromCfg(configs) {
|
|||
return productType?.product_tier;
|
||||
}
|
||||
|
||||
/** @typedef {'essentials' | 'complete' | 'search_ai_lake' | 'ai_soc'} ServerlessProjectTier */
|
||||
/**
|
||||
* @param {string[]} config Configuration object from merged configs
|
||||
* @returns {ServerlessProjectTier|undefined} The serverless project tier in the summed configs
|
||||
*/
|
||||
function getServerlessProjectTierFromConfig(config) {
|
||||
const products = _.get(config, 'pricing.tiers.products', []);
|
||||
|
||||
// Constraint tier to be the same for
|
||||
const uniqueTiers = _.uniqBy(products, 'tier');
|
||||
if (uniqueTiers.length > 1) {
|
||||
throw new Error(
|
||||
'Multiple tiers found in pricing.tiers.products, the applied tier should be the same for all the products.'
|
||||
);
|
||||
}
|
||||
|
||||
return uniqueTiers.at(0)?.tier;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} fileName Name of the config within the config directory
|
||||
* @returns {string | null} The resolved path to the config, if it exists, null otherwise
|
||||
|
|
|
@ -185,6 +185,128 @@ describe('compileConfigStack', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('pricing tiers configuration', () => {
|
||||
it('adds pricing tier config to the stack when pricing.tiers.enabled is true', async () => {
|
||||
getConfigFromFiles.mockImplementationOnce(() => {
|
||||
return {
|
||||
pricing: {
|
||||
tiers: {
|
||||
enabled: true,
|
||||
products: [{ name: 'observability', tier: 'essentials' }],
|
||||
},
|
||||
},
|
||||
serverless: 'oblt',
|
||||
};
|
||||
});
|
||||
|
||||
const configList = compileConfigStack({
|
||||
serverless: 'oblt',
|
||||
}).map(toFileNames);
|
||||
|
||||
expect(configList).toEqual([
|
||||
'serverless.yml',
|
||||
'serverless.oblt.yml',
|
||||
'kibana.yml',
|
||||
'serverless.oblt.essentials.yml',
|
||||
]);
|
||||
});
|
||||
|
||||
it('adds pricing tier config with dev mode when pricing.tiers.enabled is true', async () => {
|
||||
getConfigFromFiles.mockImplementationOnce(() => {
|
||||
return {
|
||||
pricing: {
|
||||
tiers: {
|
||||
enabled: true,
|
||||
products: [{ name: 'observability', tier: 'complete' }],
|
||||
},
|
||||
},
|
||||
serverless: 'oblt',
|
||||
};
|
||||
});
|
||||
|
||||
const configList = compileConfigStack({
|
||||
serverless: 'oblt',
|
||||
dev: true,
|
||||
}).map(toFileNames);
|
||||
|
||||
expect(configList).toEqual([
|
||||
'serverless.yml',
|
||||
'serverless.oblt.yml',
|
||||
'kibana.yml',
|
||||
'kibana.dev.yml',
|
||||
'serverless.dev.yml',
|
||||
'serverless.oblt.dev.yml',
|
||||
'serverless.oblt.complete.yml',
|
||||
'serverless.oblt.complete.dev.yml',
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not add pricing tier config when pricing.tiers.enabled is false', async () => {
|
||||
getConfigFromFiles.mockImplementationOnce(() => {
|
||||
return {
|
||||
pricing: {
|
||||
tiers: {
|
||||
enabled: false,
|
||||
products: [{ name: 'observability', tier: 'essentials' }],
|
||||
},
|
||||
},
|
||||
serverless: 'oblt',
|
||||
};
|
||||
});
|
||||
|
||||
const configList = compileConfigStack({
|
||||
serverless: 'oblt',
|
||||
}).map(toFileNames);
|
||||
|
||||
expect(configList).toEqual(['serverless.yml', 'serverless.oblt.yml', 'kibana.yml']);
|
||||
});
|
||||
|
||||
it('does not add pricing tier config when pricing.tiers.enabled is true but no tier is specified', async () => {
|
||||
getConfigFromFiles.mockImplementationOnce(() => {
|
||||
return {
|
||||
pricing: {
|
||||
tiers: {
|
||||
enabled: true,
|
||||
products: [],
|
||||
},
|
||||
},
|
||||
serverless: 'oblt',
|
||||
};
|
||||
});
|
||||
|
||||
const configList = compileConfigStack({
|
||||
serverless: 'oblt',
|
||||
}).map(toFileNames);
|
||||
|
||||
expect(configList).toEqual(['serverless.yml', 'serverless.oblt.yml', 'kibana.yml']);
|
||||
});
|
||||
|
||||
it('throws an error when multiple different tiers are specified', async () => {
|
||||
getConfigFromFiles.mockImplementationOnce(() => {
|
||||
return {
|
||||
pricing: {
|
||||
tiers: {
|
||||
enabled: true,
|
||||
products: [
|
||||
{ name: 'observability', tier: 'complete' },
|
||||
{ name: 'observability', tier: 'essentials' },
|
||||
],
|
||||
},
|
||||
},
|
||||
serverless: 'oblt',
|
||||
};
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
compileConfigStack({
|
||||
serverless: 'oblt',
|
||||
});
|
||||
}).toThrow(
|
||||
'Multiple tiers found in pricing.tiers.products, the applied tier should be the same for all the products.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function toFileNames(path) {
|
||||
return Path.basename(path);
|
||||
}
|
||||
|
|
|
@ -97,6 +97,14 @@ export const createConfigService = ({
|
|||
report_to: [],
|
||||
});
|
||||
}
|
||||
if (path === 'pricing') {
|
||||
return new BehaviorSubject({
|
||||
tiers: {
|
||||
enabled: true,
|
||||
products: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
throw new Error(`Unexpected config path: ${path}`);
|
||||
});
|
||||
return configService;
|
||||
|
|
|
@ -26,6 +26,7 @@ import { securityServiceMock } from '@kbn/core-security-browser-mocks';
|
|||
import { userProfileServiceMock } from '@kbn/core-user-profile-browser-mocks';
|
||||
import { renderingServiceMock } from '@kbn/core-rendering-browser-mocks';
|
||||
import { coreFeatureFlagsMock } from '@kbn/core-feature-flags-browser-mocks';
|
||||
import { pricingServiceMock } from '@kbn/core-pricing-browser-mocks';
|
||||
|
||||
export function createCoreStartMock({ basePath = '' } = {}) {
|
||||
const mock = {
|
||||
|
@ -49,6 +50,7 @@ export function createCoreStartMock({ basePath = '' } = {}) {
|
|||
security: securityServiceMock.createStart(),
|
||||
userProfile: userProfileServiceMock.createStart(),
|
||||
rendering: renderingServiceMock.create(),
|
||||
pricing: pricingServiceMock.createStartContract(),
|
||||
plugins: {
|
||||
onStart: jest.fn(),
|
||||
},
|
||||
|
|
|
@ -30,7 +30,8 @@
|
|||
"@kbn/core-security-browser-mocks",
|
||||
"@kbn/core-user-profile-browser-mocks",
|
||||
"@kbn/core-feature-flags-browser-mocks",
|
||||
"@kbn/core-rendering-browser-mocks"
|
||||
"@kbn/core-rendering-browser-mocks",
|
||||
"@kbn/core-pricing-browser-mocks"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -23,6 +23,7 @@ import type { ApplicationStart } from '@kbn/core-application-browser';
|
|||
import type { ChromeStart } from '@kbn/core-chrome-browser';
|
||||
import type { CustomBrandingStart } from '@kbn/core-custom-branding-browser';
|
||||
import type { PluginsServiceStart } from '@kbn/core-plugins-contracts-browser';
|
||||
import type { PricingServiceStart } from '@kbn/core-pricing-browser';
|
||||
import type { SecurityServiceStart } from '@kbn/core-security-browser';
|
||||
import type { RenderingService } from '@kbn/core-rendering-browser';
|
||||
import type { UserProfileServiceStart } from '@kbn/core-user-profile-browser';
|
||||
|
@ -78,6 +79,8 @@ export interface CoreStart {
|
|||
theme: ThemeServiceStart;
|
||||
/** {@link PluginsServiceStart} */
|
||||
plugins: PluginsServiceStart;
|
||||
/** {@link PricingServiceStart} */
|
||||
pricing: PricingServiceStart;
|
||||
/** {@link SecurityServiceStart} */
|
||||
security: SecurityServiceStart;
|
||||
/** {@link UserProfileServiceStart} */
|
||||
|
|
|
@ -31,7 +31,8 @@
|
|||
"@kbn/core-security-browser",
|
||||
"@kbn/core-user-profile-browser",
|
||||
"@kbn/core-feature-flags-browser",
|
||||
"@kbn/core-rendering-browser"
|
||||
"@kbn/core-rendering-browser",
|
||||
"@kbn/core-pricing-browser"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -30,6 +30,7 @@ import type { InternalUserSettingsServiceSetup } from '@kbn/core-user-settings-s
|
|||
import type { InternalSecurityServiceSetup } from '@kbn/core-security-server-internal';
|
||||
import type { InternalUserProfileServiceSetup } from '@kbn/core-user-profile-server-internal';
|
||||
import type { InternalFeatureFlagsSetup } from '@kbn/core-feature-flags-server-internal';
|
||||
import type { PricingServiceSetup } from '@kbn/core-pricing-server';
|
||||
|
||||
/** @internal */
|
||||
export interface InternalCoreSetup {
|
||||
|
@ -56,4 +57,5 @@ export interface InternalCoreSetup {
|
|||
userSettings: InternalUserSettingsServiceSetup;
|
||||
security: InternalSecurityServiceSetup;
|
||||
userProfile: InternalUserProfileServiceSetup;
|
||||
pricing: PricingServiceSetup;
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import type { CoreUsageDataStart } from '@kbn/core-usage-data-server';
|
|||
import type { CustomBrandingStart } from '@kbn/core-custom-branding-server';
|
||||
import type { InternalSecurityServiceStart } from '@kbn/core-security-server-internal';
|
||||
import type { InternalUserProfileServiceStart } from '@kbn/core-user-profile-server-internal';
|
||||
import type { PricingServiceStart } from '@kbn/core-pricing-server';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
|
@ -42,4 +43,5 @@ export interface InternalCoreStart {
|
|||
customBranding: CustomBrandingStart;
|
||||
security: InternalSecurityServiceStart;
|
||||
userProfile: InternalUserProfileServiceStart;
|
||||
pricing: PricingServiceStart;
|
||||
}
|
||||
|
|
|
@ -37,7 +37,8 @@
|
|||
"@kbn/core-security-server-internal",
|
||||
"@kbn/core-user-profile-server-internal",
|
||||
"@kbn/core-feature-flags-server",
|
||||
"@kbn/core-feature-flags-server-internal"
|
||||
"@kbn/core-feature-flags-server-internal",
|
||||
"@kbn/core-pricing-server"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -31,6 +31,7 @@ import { securityServiceMock } from '@kbn/core-security-server-mocks';
|
|||
import { userProfileServiceMock } from '@kbn/core-user-profile-server-mocks';
|
||||
import { createCoreStartMock } from './core_start.mock';
|
||||
import { coreFeatureFlagsMock } from '@kbn/core-feature-flags-server-mocks';
|
||||
import { pricingServiceMock } from '@kbn/core-pricing-server-mocks';
|
||||
|
||||
type CoreSetupMockType = MockedKeys<CoreSetup> & {
|
||||
elasticsearch: ReturnType<typeof elasticsearchServiceMock.createSetup>;
|
||||
|
@ -83,6 +84,7 @@ export function createCoreSetupMock({
|
|||
onSetup: jest.fn(),
|
||||
onStart: jest.fn(),
|
||||
},
|
||||
pricing: pricingServiceMock.createSetupContract(),
|
||||
getStartServices: jest
|
||||
.fn<Promise<[ReturnType<typeof createCoreStartMock>, object, any]>, []>()
|
||||
.mockResolvedValue([createCoreStartMock(), pluginStartDeps, pluginStartContract]),
|
||||
|
|
|
@ -23,6 +23,7 @@ import { customBrandingServiceMock } from '@kbn/core-custom-branding-server-mock
|
|||
import { securityServiceMock } from '@kbn/core-security-server-mocks';
|
||||
import { userProfileServiceMock } from '@kbn/core-user-profile-server-mocks';
|
||||
import { coreFeatureFlagsMock } from '@kbn/core-feature-flags-server-mocks';
|
||||
import { pricingServiceMock } from '@kbn/core-pricing-server-mocks';
|
||||
|
||||
export function createCoreStartMock() {
|
||||
const mock: MockedKeys<CoreStart> = {
|
||||
|
@ -43,6 +44,7 @@ export function createCoreStartMock() {
|
|||
plugins: {
|
||||
onStart: jest.fn(),
|
||||
},
|
||||
pricing: pricingServiceMock.createStartContract(),
|
||||
};
|
||||
|
||||
return mock;
|
||||
|
|
|
@ -30,6 +30,7 @@ import { userSettingsServiceMock } from '@kbn/core-user-settings-server-mocks';
|
|||
import { securityServiceMock } from '@kbn/core-security-server-mocks';
|
||||
import { userProfileServiceMock } from '@kbn/core-user-profile-server-mocks';
|
||||
import { coreFeatureFlagsMock } from '@kbn/core-feature-flags-server-mocks';
|
||||
import { pricingServiceMock } from '@kbn/core-pricing-server-mocks';
|
||||
|
||||
export function createInternalCoreSetupMock() {
|
||||
const setupDeps = {
|
||||
|
@ -56,6 +57,7 @@ export function createInternalCoreSetupMock() {
|
|||
userSettings: userSettingsServiceMock.createSetupContract(),
|
||||
security: securityServiceMock.createInternalSetup(),
|
||||
userProfile: userProfileServiceMock.createInternalSetup(),
|
||||
pricing: pricingServiceMock.createSetupContract(),
|
||||
};
|
||||
return setupDeps;
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import { customBrandingServiceMock } from '@kbn/core-custom-branding-server-mock
|
|||
import { securityServiceMock } from '@kbn/core-security-server-mocks';
|
||||
import { userProfileServiceMock } from '@kbn/core-user-profile-server-mocks';
|
||||
import { coreFeatureFlagsMock } from '@kbn/core-feature-flags-server-mocks';
|
||||
import { pricingServiceMock } from '@kbn/core-pricing-server-mocks';
|
||||
|
||||
export function createInternalCoreStartMock() {
|
||||
const startDeps = {
|
||||
|
@ -40,6 +41,7 @@ export function createInternalCoreStartMock() {
|
|||
customBranding: customBrandingServiceMock.createStartContract(),
|
||||
security: securityServiceMock.createInternalStart(),
|
||||
userProfile: userProfileServiceMock.createInternalStart(),
|
||||
pricing: pricingServiceMock.createStartContract(),
|
||||
};
|
||||
return startDeps;
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
"@kbn/core-security-server-mocks",
|
||||
"@kbn/core-user-profile-server-mocks",
|
||||
"@kbn/core-feature-flags-server-mocks",
|
||||
"@kbn/core-pricing-server-mocks",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -27,6 +27,7 @@ import type { CoreUsageDataSetup } from '@kbn/core-usage-data-server';
|
|||
import type { CustomBrandingSetup } from '@kbn/core-custom-branding-server';
|
||||
import type { UserSettingsServiceSetup } from '@kbn/core-user-settings-server';
|
||||
import type { PluginsServiceSetup } from '@kbn/core-plugins-contracts-server';
|
||||
import type { PricingServiceSetup } from '@kbn/core-pricing-server';
|
||||
import type { SecurityServiceSetup } from '@kbn/core-security-server';
|
||||
import type { UserProfileServiceSetup } from '@kbn/core-user-profile-server';
|
||||
import type { CoreStart } from './core_start';
|
||||
|
@ -82,6 +83,8 @@ export interface CoreSetup<TPluginsStart extends Record<string, any> = {}, TStar
|
|||
coreUsageData: CoreUsageDataSetup;
|
||||
/** {@link PluginsServiceSetup} */
|
||||
plugins: PluginsServiceSetup;
|
||||
/** {@link PricingServiceSetup} */
|
||||
pricing: PricingServiceSetup;
|
||||
/** {@link SecurityServiceSetup} */
|
||||
security: SecurityServiceSetup;
|
||||
/** {@link UserProfileServiceSetup} */
|
||||
|
|
|
@ -22,6 +22,7 @@ import type { CustomBrandingStart } from '@kbn/core-custom-branding-server';
|
|||
import type { PluginsServiceStart } from '@kbn/core-plugins-contracts-server';
|
||||
import type { SecurityServiceStart } from '@kbn/core-security-server';
|
||||
import type { UserProfileServiceStart } from '@kbn/core-user-profile-server';
|
||||
import type { PricingServiceStart } from '@kbn/core-pricing-server';
|
||||
|
||||
/**
|
||||
* Context passed to the plugins `start` method.
|
||||
|
@ -55,6 +56,8 @@ export interface CoreStart {
|
|||
coreUsageData: CoreUsageDataStart;
|
||||
/** {@link PluginsServiceStart} */
|
||||
plugins: PluginsServiceStart;
|
||||
/** {@link PricingServiceStart} */
|
||||
pricing: PricingServiceStart;
|
||||
/** {@link SecurityServiceStart} */
|
||||
security: SecurityServiceStart;
|
||||
/** {@link UserProfileServiceStart} */
|
||||
|
|
|
@ -33,7 +33,8 @@
|
|||
"@kbn/core-plugins-contracts-server",
|
||||
"@kbn/core-security-server",
|
||||
"@kbn/core-user-profile-server",
|
||||
"@kbn/core-feature-flags-server"
|
||||
"@kbn/core-feature-flags-server",
|
||||
"@kbn/core-pricing-server"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -175,5 +175,6 @@ export function createPluginStartContext<
|
|||
onStart: (...dependencyNames) => runtimeResolver.onStart(plugin.name, dependencyNames),
|
||||
},
|
||||
rendering: deps.rendering,
|
||||
pricing: deps.pricing,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -291,6 +291,9 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>({
|
|||
onSetup: (...dependencyNames) => runtimeResolver.onSetup(plugin.name, dependencyNames),
|
||||
onStart: (...dependencyNames) => runtimeResolver.onStart(plugin.name, dependencyNames),
|
||||
},
|
||||
pricing: {
|
||||
registerProductFeatures: deps.pricing.registerProductFeatures,
|
||||
},
|
||||
security: {
|
||||
registerSecurityDelegate: (api) => deps.security.registerSecurityDelegate(api),
|
||||
fips: deps.security.fips,
|
||||
|
@ -385,6 +388,7 @@ export function createPluginStartContext<TPlugin, TPluginDependencies>({
|
|||
plugins: {
|
||||
onStart: (...dependencyNames) => runtimeResolver.onStart(plugin.name, dependencyNames),
|
||||
},
|
||||
pricing: deps.pricing,
|
||||
security: {
|
||||
authc: deps.security.authc,
|
||||
audit: deps.security.audit,
|
||||
|
|
131
src/core/packages/pricing/browser-internal/README.md
Normal file
131
src/core/packages/pricing/browser-internal/README.md
Normal file
|
@ -0,0 +1,131 @@
|
|||
# @kbn/core-pricing-browser-internal
|
||||
|
||||
## Overview
|
||||
|
||||
The `@kbn/core-pricing-browser-internal` package provides the browser-side implementation of Kibana's pricing service. It allows client-side code to check if features are available based on the current pricing tier configuration.
|
||||
|
||||
This package is part of Kibana's core and is designed to be consumed by other plugins that need to conditionally render UI elements or enable/disable functionality based on the pricing tier.
|
||||
|
||||
## Key Components
|
||||
|
||||
### PricingService
|
||||
|
||||
The main service that handles fetching pricing configuration from the server and providing a client to check feature availability.
|
||||
|
||||
### PricingTiersClient
|
||||
|
||||
A client that allows plugins to check if a specific feature is available based on the current pricing tier configuration.
|
||||
|
||||
## How It Works
|
||||
|
||||
The browser-side pricing service works by:
|
||||
|
||||
1. Fetching the pricing configuration from the server via an API call to `/internal/core/pricing`
|
||||
2. Creating a `PricingTiersClient` instance with the fetched configuration
|
||||
3. Providing the client to plugins through the core start contract
|
||||
|
||||
Plugins can then use the client to check if specific features are available based on the current pricing tier configuration.
|
||||
|
||||
## Usage
|
||||
|
||||
### Consuming the PricingService in a Plugin
|
||||
|
||||
To use the pricing service in your plugin, you can consume it directly from the provided `core` dependencies.
|
||||
|
||||
Here's an example of how to consume the pricing service in a plugin:
|
||||
|
||||
```typescript
|
||||
// my-plugin/public/plugin.ts
|
||||
import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
|
||||
|
||||
export class MyPlugin implements Plugin {
|
||||
public start(core: CoreStart) {
|
||||
// Check if a feature is available based on the current pricing tier
|
||||
const isFeature1Available = core.pricing.isFeatureAvailable('my-plugin:feature1');
|
||||
const isFeature2Available = core.pricing.isFeatureAvailable('my-plugin:feature2');
|
||||
|
||||
// Conditionally enable features based on availability
|
||||
if (isFeature1Available) {
|
||||
// Enable feature1
|
||||
}
|
||||
|
||||
if (isFeature2Available) {
|
||||
// Enable feature2
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using in React Components
|
||||
|
||||
To use the pricing service in React components, you'll typically want to access it through the CoreStart provided to a plugin.
|
||||
|
||||
#### Using with CoreStart
|
||||
|
||||
```tsx
|
||||
// my-plugin/public/application.tsx
|
||||
import React from 'react';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
|
||||
interface MyComponentProps {
|
||||
coreStart: CoreStart;
|
||||
}
|
||||
|
||||
const MyComponent: React.FC<MyComponentProps> = ({ coreStart }) => {
|
||||
const isFeatureAvailable = coreStart.pricing.isFeatureAvailable('my-plugin:feature1');
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isFeatureAvailable ? (
|
||||
<div>This feature is available in your current pricing tier!</div>
|
||||
) : (
|
||||
<div>This feature requires an upgrade to access.</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
When testing components that use the pricing service, you can use the `@kbn/core-pricing-browser-mocks` package to mock the pricing service:
|
||||
|
||||
```typescript
|
||||
import { pricingServiceMock } from '@kbn/core-pricing-browser-mocks';
|
||||
import { render } from '@testing-library/react';
|
||||
import { CoreContext } from '@kbn/core-react';
|
||||
import { MyComponent } from './my_component';
|
||||
|
||||
describe('MyComponent', () => {
|
||||
let pricingStart: ReturnType<typeof pricingServiceMock.createStartContract>;
|
||||
|
||||
beforeEach(() => {
|
||||
pricingStart = pricingServiceMock.createStartContract();
|
||||
|
||||
// Mock feature availability
|
||||
pricingStart.isFeatureAvailable.mockImplementation((featureId) => {
|
||||
if (featureId === 'my-plugin:feature1') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
it('renders feature1 when available', () => {
|
||||
const { getByText } = render(
|
||||
<CoreContext.Provider value={{ services: { pricing: pricingStart } }}>
|
||||
<MyComponent />
|
||||
</CoreContext.Provider>
|
||||
);
|
||||
|
||||
expect(getByText('Use Feature 1')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Related Packages
|
||||
|
||||
- `@kbn/core-pricing-common`: Contains common types and interfaces used by both server and browser packages.
|
||||
- `@kbn/core-pricing-browser`: The public API for the pricing service that plugins can consume.
|
||||
- `@kbn/core-pricing-server`: The server-side counterpart to the pricing service.
|
||||
- `@kbn/core-pricing-browser-mocks`: Mocks for testing plugins that consume the pricing service.
|
10
src/core/packages/pricing/browser-internal/index.ts
Normal file
10
src/core/packages/pricing/browser-internal/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export { PricingService } from './src/pricing_service';
|
14
src/core/packages/pricing/browser-internal/jest.config.js
Normal file
14
src/core/packages/pricing/browser-internal/jest.config.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../../../..',
|
||||
roots: ['<rootDir>/src/core/packages/pricing/browser-internal'],
|
||||
};
|
9
src/core/packages/pricing/browser-internal/kibana.jsonc
Normal file
9
src/core/packages/pricing/browser-internal/kibana.jsonc
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"type": "shared-browser",
|
||||
"id": "@kbn/core-pricing-browser-internal",
|
||||
"owner": [
|
||||
"@elastic/kibana-core"
|
||||
],
|
||||
"group": "platform",
|
||||
"visibility": "private"
|
||||
}
|
7
src/core/packages/pricing/browser-internal/package.json
Normal file
7
src/core/packages/pricing/browser-internal/package.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "@kbn/core-pricing-browser-internal",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"author": "Kibana Core",
|
||||
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
|
||||
import type { GetPricingResponse } from '@kbn/core-pricing-browser';
|
||||
import { PricingService } from './pricing_service';
|
||||
|
||||
describe('PricingService', () => {
|
||||
let service: PricingService;
|
||||
let http: ReturnType<typeof httpServiceMock.createStartContract>;
|
||||
let mockPricingResponse: GetPricingResponse;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new PricingService();
|
||||
http = httpServiceMock.createStartContract();
|
||||
|
||||
mockPricingResponse = {
|
||||
tiers: {
|
||||
enabled: true,
|
||||
products: [
|
||||
{ name: 'observability', tier: 'complete' },
|
||||
{ name: 'security', tier: 'essentials' },
|
||||
],
|
||||
},
|
||||
product_features: {
|
||||
feature1: {
|
||||
id: 'feature1',
|
||||
description: 'A feature for observability products',
|
||||
products: [{ name: 'observability', tier: 'complete' }],
|
||||
},
|
||||
feature2: {
|
||||
id: 'feature2',
|
||||
description: 'A feature for security products',
|
||||
products: [{ name: 'security', tier: 'essentials' }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
http.get.mockResolvedValue(mockPricingResponse);
|
||||
});
|
||||
|
||||
describe('#start()', () => {
|
||||
it('fetches pricing data from the API', async () => {
|
||||
await service.start({ http });
|
||||
|
||||
expect(http.get).toHaveBeenCalledWith('/internal/core/pricing');
|
||||
});
|
||||
|
||||
it('returns a PricingTiersClient with the fetched data', async () => {
|
||||
const startContract = await service.start({ http });
|
||||
|
||||
expect(startContract).toHaveProperty('isFeatureAvailable');
|
||||
});
|
||||
|
||||
it('initializes the client with the correct tiers configuration', async () => {
|
||||
const startContract = await service.start({ http });
|
||||
|
||||
// Since our mock has feature1 with observability product which is enabled in tiers
|
||||
expect(startContract.isFeatureAvailable('feature1')).toBe(true);
|
||||
});
|
||||
|
||||
it('initializes the client with empty data when API returns empty response', async () => {
|
||||
const emptyResponse: GetPricingResponse = {
|
||||
tiers: {
|
||||
enabled: false,
|
||||
products: [],
|
||||
},
|
||||
product_features: {},
|
||||
};
|
||||
http.get.mockResolvedValue(emptyResponse);
|
||||
|
||||
const startContract = await service.start({ http });
|
||||
|
||||
// When tiers are disabled, all features should be available
|
||||
expect(startContract.isFeatureAvailable('any-feature')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { InternalHttpStart } from '@kbn/core-http-browser-internal';
|
||||
import type { GetPricingResponse, PricingServiceStart } from '@kbn/core-pricing-browser';
|
||||
import { PricingTiersClient, ProductFeaturesRegistry } from '@kbn/core-pricing-common';
|
||||
|
||||
interface StartDeps {
|
||||
http: InternalHttpStart;
|
||||
}
|
||||
|
||||
const defaultPricingResponse: GetPricingResponse = {
|
||||
tiers: {
|
||||
enabled: false,
|
||||
products: [],
|
||||
},
|
||||
product_features: {},
|
||||
};
|
||||
|
||||
/**
|
||||
* Service that is responsible for UI Pricing.
|
||||
* @internal
|
||||
*/
|
||||
export class PricingService {
|
||||
public async start({ http }: StartDeps): Promise<PricingServiceStart> {
|
||||
const isAnonymous = http.anonymousPaths.isAnonymous(window.location.pathname);
|
||||
const pricingResponse = isAnonymous
|
||||
? defaultPricingResponse
|
||||
: await http.get<GetPricingResponse>('/internal/core/pricing');
|
||||
|
||||
const tiersClient = new PricingTiersClient(
|
||||
pricingResponse.tiers,
|
||||
new ProductFeaturesRegistry(pricingResponse.product_features)
|
||||
);
|
||||
|
||||
return {
|
||||
isFeatureAvailable: tiersClient.isFeatureAvailable,
|
||||
};
|
||||
}
|
||||
}
|
23
src/core/packages/pricing/browser-internal/tsconfig.json
Normal file
23
src/core/packages/pricing/browser-internal/tsconfig.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"extends": "../../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/core-http-browser-internal",
|
||||
"@kbn/core-pricing-browser",
|
||||
"@kbn/core-pricing-common",
|
||||
"@kbn/core-http-browser-mocks",
|
||||
]
|
||||
}
|
4
src/core/packages/pricing/browser-mocks/README.md
Normal file
4
src/core/packages/pricing/browser-mocks/README.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
# @kbn/core-pricing-browser-mocks
|
||||
|
||||
Contains the mocks for Core's internal `pricing` browser-side service:
|
||||
- `pricingServiceMock`
|
10
src/core/packages/pricing/browser-mocks/index.ts
Normal file
10
src/core/packages/pricing/browser-mocks/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export { pricingServiceMock } from './src/pricing_service.mock';
|
14
src/core/packages/pricing/browser-mocks/jest.config.js
Normal file
14
src/core/packages/pricing/browser-mocks/jest.config.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../../../..',
|
||||
roots: ['<rootDir>/src/core/packages/pricing/browser-mocks'],
|
||||
};
|
10
src/core/packages/pricing/browser-mocks/kibana.jsonc
Normal file
10
src/core/packages/pricing/browser-mocks/kibana.jsonc
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"type": "shared-browser",
|
||||
"id": "@kbn/core-pricing-browser-mocks",
|
||||
"owner": [
|
||||
"@elastic/kibana-core"
|
||||
],
|
||||
"group": "platform",
|
||||
"visibility": "shared",
|
||||
"devOnly": true
|
||||
}
|
7
src/core/packages/pricing/browser-mocks/package.json
Normal file
7
src/core/packages/pricing/browser-mocks/package.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "@kbn/core-pricing-browser-mocks",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"author": "Kibana Core",
|
||||
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import type { PricingServiceStart } from '@kbn/core-pricing-browser';
|
||||
import type { PricingService } from '@kbn/core-pricing-browser-internal';
|
||||
|
||||
const createStartContractMock = (): jest.Mocked<PricingServiceStart> => ({
|
||||
isFeatureAvailable: jest.fn(),
|
||||
});
|
||||
|
||||
const createMock = (): jest.Mocked<PublicMethodsOf<PricingService>> => ({
|
||||
start: jest.fn().mockImplementation(createStartContractMock),
|
||||
});
|
||||
|
||||
export const pricingServiceMock = {
|
||||
create: createMock,
|
||||
createStartContract: createStartContractMock,
|
||||
};
|
22
src/core/packages/pricing/browser-mocks/tsconfig.json
Normal file
22
src/core/packages/pricing/browser-mocks/tsconfig.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"extends": "../../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/utility-types",
|
||||
"@kbn/core-pricing-browser",
|
||||
"@kbn/core-pricing-browser-internal",
|
||||
]
|
||||
}
|
3
src/core/packages/pricing/browser/README.md
Normal file
3
src/core/packages/pricing/browser/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# @kbn/core-pricing-browser
|
||||
|
||||
This package contains the public types for Core's browser-side `pricing` service.
|
11
src/core/packages/pricing/browser/index.ts
Normal file
11
src/core/packages/pricing/browser/index.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export type { PricingServiceStart } from './src/contracts';
|
||||
export type { GetPricingResponse } from './src/api';
|
14
src/core/packages/pricing/browser/jest.config.js
Normal file
14
src/core/packages/pricing/browser/jest.config.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../../../..',
|
||||
roots: ['<rootDir>/src/core/packages/pricing/browser'],
|
||||
};
|
9
src/core/packages/pricing/browser/kibana.jsonc
Normal file
9
src/core/packages/pricing/browser/kibana.jsonc
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"type": "shared-browser",
|
||||
"id": "@kbn/core-pricing-browser",
|
||||
"owner": [
|
||||
"@elastic/kibana-core"
|
||||
],
|
||||
"group": "platform",
|
||||
"visibility": "shared"
|
||||
}
|
6
src/core/packages/pricing/browser/package.json
Normal file
6
src/core/packages/pricing/browser/package.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/core-pricing-browser",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
|
||||
}
|
18
src/core/packages/pricing/browser/src/api.ts
Normal file
18
src/core/packages/pricing/browser/src/api.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { PricingProduct, PricingProductFeature } from '@kbn/core-pricing-common';
|
||||
|
||||
export interface GetPricingResponse {
|
||||
tiers: {
|
||||
enabled: boolean;
|
||||
products: PricingProduct[];
|
||||
};
|
||||
product_features: Record<string, PricingProductFeature>;
|
||||
}
|
18
src/core/packages/pricing/browser/src/contracts.ts
Normal file
18
src/core/packages/pricing/browser/src/contracts.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { IPricingTiersClient } from '@kbn/core-pricing-common';
|
||||
/**
|
||||
* Start contract for Core's pricing service.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface PricingServiceStart {
|
||||
isFeatureAvailable: IPricingTiersClient['isFeatureAvailable'];
|
||||
}
|
21
src/core/packages/pricing/browser/tsconfig.json
Normal file
21
src/core/packages/pricing/browser/tsconfig.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"extends": "../../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node",
|
||||
"react"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/core-pricing-common",
|
||||
]
|
||||
}
|
3
src/core/packages/pricing/common/README.md
Normal file
3
src/core/packages/pricing/common/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# @kbn/core-pricing-common
|
||||
|
||||
Contains public types and constants for Core's browser-side `pricing` service.
|
13
src/core/packages/pricing/common/index.ts
Normal file
13
src/core/packages/pricing/common/index.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export { ProductFeaturesRegistry } from './src/product_features_registry';
|
||||
export type { IPricingTiersClient, PricingProductFeature } from './src/types';
|
||||
export type { PricingProduct, TiersConfig } from './src/pricing_tiers_config';
|
||||
export { PricingTiersClient } from './src/pricing_tiers_client';
|
14
src/core/packages/pricing/common/jest.config.js
Normal file
14
src/core/packages/pricing/common/jest.config.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../../../..',
|
||||
roots: ['<rootDir>/src/core/packages/pricing/common'],
|
||||
};
|
9
src/core/packages/pricing/common/kibana.jsonc
Normal file
9
src/core/packages/pricing/common/kibana.jsonc
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/core-pricing-common",
|
||||
"owner": [
|
||||
"@elastic/kibana-core"
|
||||
],
|
||||
"group": "platform",
|
||||
"visibility": "shared"
|
||||
}
|
7
src/core/packages/pricing/common/package.json
Normal file
7
src/core/packages/pricing/common/package.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "@kbn/core-pricing-common",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"author": "Kibana Core",
|
||||
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { PricingTiersClient } from './pricing_tiers_client';
|
||||
import { ProductFeaturesRegistry } from './product_features_registry';
|
||||
import type { PricingProductFeature } from './types';
|
||||
import type { TiersConfig } from './pricing_tiers_config';
|
||||
|
||||
describe('PricingTiersClient', () => {
|
||||
let productFeaturesRegistry: ProductFeaturesRegistry;
|
||||
let tiersConfig: TiersConfig;
|
||||
let client: PricingTiersClient;
|
||||
|
||||
beforeEach(() => {
|
||||
productFeaturesRegistry = new ProductFeaturesRegistry();
|
||||
});
|
||||
|
||||
describe('isFeatureAvailable', () => {
|
||||
describe('when tiers are disabled', () => {
|
||||
beforeEach(() => {
|
||||
tiersConfig = {
|
||||
enabled: false,
|
||||
products: undefined,
|
||||
};
|
||||
client = new PricingTiersClient(tiersConfig, productFeaturesRegistry);
|
||||
});
|
||||
|
||||
it('returns true for any feature, even if it does not exist', () => {
|
||||
expect(client.isFeatureAvailable('non-existent-feature')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for registered features indipendently of the tier configuration', () => {
|
||||
const feature: PricingProductFeature = {
|
||||
id: 'test-feature',
|
||||
description: 'A test feature for observability',
|
||||
products: [{ name: 'observability', tier: 'complete' }],
|
||||
};
|
||||
productFeaturesRegistry.register(feature);
|
||||
|
||||
expect(client.isFeatureAvailable('test-feature')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when tiers are enabled', () => {
|
||||
beforeEach(() => {
|
||||
tiersConfig = {
|
||||
enabled: true,
|
||||
products: [
|
||||
{ name: 'observability', tier: 'complete' },
|
||||
{ name: 'security', tier: 'essentials' },
|
||||
],
|
||||
};
|
||||
client = new PricingTiersClient(tiersConfig, productFeaturesRegistry);
|
||||
});
|
||||
|
||||
it('returns false for non-existent features', () => {
|
||||
expect(client.isFeatureAvailable('non-existent-feature')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when a feature has a matching active product', () => {
|
||||
const feature: PricingProductFeature = {
|
||||
id: 'observability-feature',
|
||||
description: 'A feature for observability products',
|
||||
products: [{ name: 'observability', tier: 'complete' }],
|
||||
};
|
||||
productFeaturesRegistry.register(feature);
|
||||
|
||||
expect(client.isFeatureAvailable('observability-feature')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when a feature has no matching active products', () => {
|
||||
const feature: PricingProductFeature = {
|
||||
id: 'cloud-feature',
|
||||
description: 'A feature for cloud products',
|
||||
products: [{ name: 'cloud', tier: 'complete' }],
|
||||
};
|
||||
productFeaturesRegistry.register(feature);
|
||||
|
||||
expect(client.isFeatureAvailable('cloud-feature')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when at least one product in a feature matches an active product', () => {
|
||||
const feature: PricingProductFeature = {
|
||||
id: 'mixed-feature',
|
||||
description: 'A feature available in multiple products',
|
||||
products: [
|
||||
{ name: 'cloud', tier: 'complete' },
|
||||
{ name: 'security', tier: 'essentials' },
|
||||
],
|
||||
};
|
||||
productFeaturesRegistry.register(feature);
|
||||
|
||||
expect(client.isFeatureAvailable('mixed-feature')).toBe(true);
|
||||
});
|
||||
|
||||
it('checks for exact product matches including tier', () => {
|
||||
const feature: PricingProductFeature = {
|
||||
id: 'tier-mismatch-feature',
|
||||
description: 'A feature with tier requirements',
|
||||
products: [{ name: 'security', tier: 'complete' }], // Note: tier is 'complete' but active product has 'essentials'
|
||||
};
|
||||
productFeaturesRegistry.register(feature);
|
||||
|
||||
expect(client.isFeatureAvailable('tier-mismatch-feature')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
81
src/core/packages/pricing/common/src/pricing_tiers_client.ts
Normal file
81
src/core/packages/pricing/common/src/pricing_tiers_client.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { isEqual } from 'lodash';
|
||||
import { IPricingTiersClient } from './types';
|
||||
import { PricingProduct, TiersConfig } from './pricing_tiers_config';
|
||||
import { ProductFeaturesRegistry } from './product_features_registry';
|
||||
|
||||
/**
|
||||
* Client implementation for checking feature availability based on pricing tiers.
|
||||
*
|
||||
* This client evaluates whether features are available based on the current pricing tier configuration
|
||||
* and the registered product features.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class PricingTiersClient implements IPricingTiersClient {
|
||||
/**
|
||||
* Creates a new PricingTiersClient instance.
|
||||
*
|
||||
* @param tiers - The current pricing tiers configuration
|
||||
* @param productFeaturesRegistry - Registry containing the available product features
|
||||
*/
|
||||
constructor(
|
||||
private readonly tiers: TiersConfig,
|
||||
private readonly productFeaturesRegistry: ProductFeaturesRegistry
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Checks if a product is active in the current pricing tier configuration.
|
||||
*
|
||||
* @param product - The product to check
|
||||
* @returns True if the product is active, false otherwise
|
||||
* @private
|
||||
*/
|
||||
private isActiveProduct = (product: PricingProduct) => {
|
||||
return Boolean(this.tiers.products?.some((currentProduct) => isEqual(currentProduct, product)));
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if pricing tiers are enabled in the current configuration.
|
||||
*
|
||||
* @returns True if pricing tiers are enabled, false otherwise
|
||||
* @private
|
||||
*/
|
||||
private isEnabled = () => {
|
||||
return this.tiers.enabled;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if a feature is available based on the current pricing tier configuration.
|
||||
* When pricing tiers are disabled, all features are considered available.
|
||||
* When pricing tiers are enabled, a feature is available if it's associated with at least one active product.
|
||||
*
|
||||
* @param featureId - The identifier of the feature to check
|
||||
* @returns True if the feature is available in the current pricing tier, false otherwise
|
||||
*/
|
||||
isFeatureAvailable = <TFeatureId extends string>(featureId: TFeatureId): boolean => {
|
||||
/**
|
||||
* We assume that when the pricing tiers are disabled, features are available globally
|
||||
* and not constrained by any product tier.
|
||||
*/
|
||||
if (!this.isEnabled()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const feature = this.productFeaturesRegistry.get(featureId);
|
||||
|
||||
if (feature) {
|
||||
return feature.products.some((product) => this.isActiveProduct(product));
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
83
src/core/packages/pricing/common/src/pricing_tiers_config.ts
Normal file
83
src/core/packages/pricing/common/src/pricing_tiers_config.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { TypeOf, offeringBasedSchema, schema } from '@kbn/config-schema';
|
||||
|
||||
/**
|
||||
* Schema defining the valid pricing product configurations.
|
||||
* Each product has a name and an associated tier that determines feature availability.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export const pricingProductsSchema = schema.oneOf([
|
||||
schema.object({
|
||||
name: schema.literal('observability'),
|
||||
tier: schema.oneOf([schema.literal('complete'), schema.literal('essentials')]),
|
||||
}),
|
||||
schema.object({
|
||||
name: schema.literal('ai_soc'),
|
||||
tier: schema.literal('search_ai_lake'),
|
||||
}),
|
||||
schema.object({
|
||||
name: schema.literal('security'),
|
||||
tier: schema.oneOf([
|
||||
schema.literal('complete'),
|
||||
schema.literal('essentials'),
|
||||
schema.literal('search_ai_lake'),
|
||||
]),
|
||||
}),
|
||||
schema.object({
|
||||
name: schema.literal('endpoint'),
|
||||
tier: schema.oneOf([
|
||||
schema.literal('complete'),
|
||||
schema.literal('essentials'),
|
||||
schema.literal('search_ai_lake'),
|
||||
]),
|
||||
}),
|
||||
schema.object({
|
||||
name: schema.literal('cloud'),
|
||||
tier: schema.oneOf([
|
||||
schema.literal('complete'),
|
||||
schema.literal('essentials'),
|
||||
schema.literal('search_ai_lake'),
|
||||
]),
|
||||
}),
|
||||
]);
|
||||
|
||||
/**
|
||||
* Represents a product with an associated pricing tier.
|
||||
* Used to determine feature availability based on the current pricing configuration.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type PricingProduct = TypeOf<typeof pricingProductsSchema>;
|
||||
|
||||
/**
|
||||
* Schema defining the pricing tiers configuration structure.
|
||||
* Includes whether tiers are enabled and which products are active.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export const tiersConfigSchema = schema.object({
|
||||
enabled: offeringBasedSchema({
|
||||
serverless: schema.boolean({ defaultValue: false }),
|
||||
traditional: schema.literal(false),
|
||||
options: { defaultValue: false },
|
||||
}),
|
||||
products: schema.maybe(schema.arrayOf(pricingProductsSchema)),
|
||||
});
|
||||
|
||||
/**
|
||||
* Configuration for pricing tiers that determines feature availability.
|
||||
* When enabled, features are only available if they're associated with an active product.
|
||||
* When disabled, all features are considered available.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type TiersConfig = TypeOf<typeof tiersConfigSchema>;
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { PricingProductFeature } from './types';
|
||||
|
||||
/**
|
||||
* Registry for managing pricing product features.
|
||||
* Provides methods to register, retrieve, and manage features that are available in specific pricing tiers.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class ProductFeaturesRegistry {
|
||||
/**
|
||||
* Internal storage for registered product features.
|
||||
* @private
|
||||
*/
|
||||
private readonly productFeatures: Map<string, PricingProductFeature>;
|
||||
|
||||
/**
|
||||
* Creates a new ProductFeaturesRegistry instance.
|
||||
*
|
||||
* @param initialFeatures - Optional initial set of features to populate the registry
|
||||
*/
|
||||
constructor(initialFeatures: Record<string, PricingProductFeature> = {}) {
|
||||
this.productFeatures = new Map(Object.entries(initialFeatures));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a product feature by its ID.
|
||||
*
|
||||
* @param featureId - The ID of the feature to retrieve
|
||||
* @returns The product feature if found, undefined otherwise
|
||||
*/
|
||||
get(featureId: string): PricingProductFeature | undefined {
|
||||
return this.productFeatures.get(featureId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new product feature in the registry.
|
||||
* Throws an error if a feature with the same ID is already registered.
|
||||
*
|
||||
* @param feature - The product feature to register
|
||||
* @throws Error if a feature with the same ID is already registered
|
||||
*/
|
||||
register(feature: PricingProductFeature) {
|
||||
if (this.productFeatures.has(feature.id)) {
|
||||
throw new Error(
|
||||
`A product feature with id "${feature.id}" is already registered, please change id or check whether is the same feature.`
|
||||
);
|
||||
}
|
||||
|
||||
this.productFeatures.set(feature.id, feature);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the registry to a plain JavaScript object.
|
||||
*
|
||||
* @returns A record mapping feature IDs to their corresponding feature objects
|
||||
*/
|
||||
asObject(): Record<string, PricingProductFeature> {
|
||||
return Object.fromEntries(this.productFeatures);
|
||||
}
|
||||
}
|
39
src/core/packages/pricing/common/src/types.ts
Normal file
39
src/core/packages/pricing/common/src/types.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { PricingProduct } from './pricing_tiers_config';
|
||||
|
||||
/**
|
||||
* Represents a feature that is registered for specific pricing tiers.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface PricingProductFeature {
|
||||
/* Unique identifier for the feature. */
|
||||
id: string;
|
||||
/* Human-readable description of the feature. */
|
||||
description: string;
|
||||
/* List of products and tiers where this feature is available. */
|
||||
products: PricingProduct[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Client interface for checking feature availability based on pricing tiers.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface IPricingTiersClient {
|
||||
/**
|
||||
* Determines if a feature is available based on the current pricing tier configuration.
|
||||
*
|
||||
* @param featureId - The identifier of the feature to check
|
||||
* @returns True if the feature is available in the current pricing tier, false otherwise
|
||||
*/
|
||||
isFeatureAvailable<TFeatureId extends string>(featureId: TFeatureId): boolean;
|
||||
}
|
20
src/core/packages/pricing/common/tsconfig.json
Normal file
20
src/core/packages/pricing/common/tsconfig.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"extends": "../../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/config-schema",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
]
|
||||
}
|
192
src/core/packages/pricing/server-internal/README.md
Normal file
192
src/core/packages/pricing/server-internal/README.md
Normal file
|
@ -0,0 +1,192 @@
|
|||
# @kbn/core-pricing-server-internal
|
||||
|
||||
## Overview
|
||||
|
||||
The `@kbn/core-pricing-server-internal` package provides a service for managing pricing tiers and feature availability in Kibana. It allows you to define different product tiers (e.g., "essentials" vs "complete") and control which features are available in each tier.
|
||||
|
||||
This package is part of Kibana's core and is designed to be consumed by other plugins that need to check if certain features should be available based on the current pricing tier configuration.
|
||||
|
||||
## Key Components
|
||||
|
||||
### PricingService
|
||||
|
||||
The main service that handles the initialization, configuration, and management of pricing tiers. It provides methods for registering product features and checking feature availability.
|
||||
|
||||
### PricingTiersClient
|
||||
|
||||
A client that allows plugins to check if a specific feature is available based on the current pricing tier configuration.
|
||||
|
||||
### ProductFeaturesRegistry
|
||||
|
||||
A registry that stores information about which features are available in which product tiers.
|
||||
|
||||
## Configuration
|
||||
|
||||
The pricing service is configured through the `pricing` configuration path in Kibana's configuration. The configuration schema is defined as follows:
|
||||
|
||||
```typescript
|
||||
export const pricingConfig = {
|
||||
path: 'pricing',
|
||||
schema: schema.object({
|
||||
tiers: schema.object({
|
||||
enabled: schema.boolean({ defaultValue: false }),
|
||||
products: schema.maybe(schema.arrayOf(pricingProductsSchema)),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
```
|
||||
|
||||
Where `pricingProductsSchema` defines the available products and tiers:
|
||||
|
||||
```typescript
|
||||
export const pricingProductsSchema = schema.oneOf([
|
||||
schema.object({
|
||||
name: schema.literal('observability'),
|
||||
tier: schema.oneOf([schema.literal('complete'), schema.literal('essentials')]),
|
||||
}),
|
||||
// More available products defined by any solution
|
||||
]);
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Consuming the PricingService in a Plugin
|
||||
|
||||
To use the pricing service in your plugin, you can consume it directly from the provided `core` dependencies.
|
||||
|
||||
Here's an example of how to consume the pricing service in a plugin:
|
||||
|
||||
```typescript
|
||||
// my-plugin/server/plugin.ts
|
||||
import { CoreSetup, CoreStart, Plugin } from '@kbn/core/server';
|
||||
import { PricingServiceSetup, PricingServiceStart } from '@kbn/core-pricing-server';
|
||||
|
||||
interface MyPluginSetupDeps {
|
||||
pricing: PricingServiceSetup;
|
||||
}
|
||||
|
||||
interface MyPluginStartDeps {
|
||||
pricing: PricingServiceStart;
|
||||
}
|
||||
|
||||
export class MyPlugin implements Plugin {
|
||||
public setup(core: CoreSetup, { pricing }: MyPluginSetupDeps) {
|
||||
// Register features that your plugin provides
|
||||
pricing.registerProductFeatures([
|
||||
{
|
||||
id: 'my-plugin:feature1',
|
||||
description: 'A feature for observability products',
|
||||
products: [
|
||||
{ name: 'observability', tier: 'complete' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'my-plugin:feature2',
|
||||
description: 'A feature for security products',
|
||||
products: [
|
||||
{ name: 'security', tier: 'essentials' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
public start(core: CoreStart, { pricing }: MyPluginStartDeps) {
|
||||
// Check if a feature is available based on the current pricing tier
|
||||
const isFeature1Available = pricing.isFeatureAvailable('my-plugin:feature1');
|
||||
const isFeature2Available = pricing.isFeatureAvailable('my-plugin:feature2');
|
||||
|
||||
// Conditionally enable features based on availability
|
||||
if (isFeature1Available) {
|
||||
// Enable feature1
|
||||
}
|
||||
|
||||
if (isFeature2Available) {
|
||||
// Enable feature2
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### API Endpoints
|
||||
|
||||
The pricing service exposes an internal API endpoint that returns the current pricing tiers configuration and registered product features:
|
||||
|
||||
```
|
||||
GET /internal/core/pricing
|
||||
```
|
||||
|
||||
Response format:
|
||||
|
||||
```json
|
||||
{
|
||||
"tiers": {
|
||||
"enabled": true,
|
||||
"products": [
|
||||
{ "name": "observability", "tier": "complete" },
|
||||
{ "name": "security", "tier": "essentials" }
|
||||
]
|
||||
},
|
||||
"product_features": {
|
||||
"my-plugin:feature1": {
|
||||
"id": "my-plugin:feature1",
|
||||
"products": [
|
||||
{ "name": "observability", "tier": "complete" },
|
||||
]
|
||||
},
|
||||
"my-plugin:feature2": {
|
||||
"id": "my-plugin:feature2",
|
||||
"products": [
|
||||
{ "name": "observability", "tier": "essentials" },
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Feature IDs**: Use a consistent naming convention for feature IDs, such as `pluginId:featureName`.
|
||||
|
||||
2. **Granular Features**: Define features at a granular level to allow for fine-grained control over which features are available in which tiers.
|
||||
|
||||
3. **Feature Documentation**: Document which features are available in which tiers to help users understand the differences between tiers.
|
||||
|
||||
## Testing
|
||||
|
||||
When testing your plugin with the pricing service, you can use the `@kbn/core-pricing-server-mocks` package to mock the pricing service:
|
||||
|
||||
```typescript
|
||||
import { pricingServiceMock } from '@kbn/core-pricing-server-mocks';
|
||||
|
||||
describe('My Plugin', () => {
|
||||
let pricingSetup: ReturnType<typeof pricingServiceMock.createSetupContract>;
|
||||
let pricingStart: ReturnType<typeof pricingServiceMock.createStartContract>;
|
||||
|
||||
beforeEach(() => {
|
||||
pricingSetup = pricingServiceMock.createSetupContract();
|
||||
pricingStart = pricingServiceMock.createStartContract();
|
||||
|
||||
// Mock feature availability
|
||||
pricingStart.isFeatureAvailable.mockImplementation((featureId) => {
|
||||
if (featureId === 'my-plugin:feature1') {
|
||||
return true;
|
||||
}
|
||||
if (featureId === 'my-plugin:feature2') {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
it('should enable feature1 when available', () => {
|
||||
// Test your plugin with the mocked pricing service
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Related Packages
|
||||
|
||||
- `@kbn/core-pricing-common`: Contains common types and interfaces used by both server and browser packages.
|
||||
- `@kbn/core-pricing-server`: The public API for the pricing service that plugins can consume.
|
||||
- `@kbn/core-pricing-browser`: The browser-side counterpart to the pricing service.
|
||||
- `@kbn/core-pricing-server-mocks`: Mocks for testing plugins that consume the pricing service.
|
12
src/core/packages/pricing/server-internal/index.ts
Normal file
12
src/core/packages/pricing/server-internal/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export type { PricingConfigType } from './src/pricing_config';
|
||||
export { pricingConfig } from './src/pricing_config';
|
||||
export { PricingService } from './src/pricing_service';
|
14
src/core/packages/pricing/server-internal/jest.config.js
Normal file
14
src/core/packages/pricing/server-internal/jest.config.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test/jest_node',
|
||||
rootDir: '../../../../..',
|
||||
roots: ['<rootDir>/src/core/packages/pricing/server-internal'],
|
||||
};
|
9
src/core/packages/pricing/server-internal/kibana.jsonc
Normal file
9
src/core/packages/pricing/server-internal/kibana.jsonc
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"type": "shared-server",
|
||||
"id": "@kbn/core-pricing-server-internal",
|
||||
"owner": [
|
||||
"@elastic/kibana-core"
|
||||
],
|
||||
"group": "platform",
|
||||
"visibility": "private"
|
||||
}
|
7
src/core/packages/pricing/server-internal/package.json
Normal file
7
src/core/packages/pricing/server-internal/package.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "@kbn/core-pricing-server-internal",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"author": "Kibana Core",
|
||||
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { tiersConfigSchema } from '@kbn/core-pricing-common/src/pricing_tiers_config';
|
||||
|
||||
export const pricingConfig = {
|
||||
path: 'pricing',
|
||||
schema: schema.object({
|
||||
tiers: tiersConfigSchema,
|
||||
}),
|
||||
};
|
||||
|
||||
export type PricingConfigType = TypeOf<typeof pricingConfig.schema>;
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { mockCoreContext } from '@kbn/core-base-server-mocks';
|
||||
import { mockRouter, RouterMock } from '@kbn/core-http-router-server-mocks';
|
||||
import {
|
||||
httpServiceMock,
|
||||
InternalHttpServicePrebootMock,
|
||||
InternalHttpServiceSetupMock,
|
||||
} from '@kbn/core-http-server-mocks';
|
||||
import { of } from 'rxjs';
|
||||
import { PricingService } from './pricing_service';
|
||||
import type { PricingConfigType } from './pricing_config';
|
||||
import type { PricingProductFeature } from '@kbn/core-pricing-common';
|
||||
|
||||
describe('PricingService', () => {
|
||||
let prebootHttp: InternalHttpServicePrebootMock;
|
||||
let setupHttp: InternalHttpServiceSetupMock;
|
||||
let service: PricingService;
|
||||
let router: RouterMock;
|
||||
let mockConfig: PricingConfigType;
|
||||
|
||||
beforeEach(() => {
|
||||
prebootHttp = httpServiceMock.createInternalPrebootContract();
|
||||
setupHttp = httpServiceMock.createInternalSetupContract();
|
||||
router = mockRouter.create();
|
||||
setupHttp.createRouter.mockReturnValue(router);
|
||||
|
||||
mockConfig = {
|
||||
tiers: {
|
||||
enabled: true,
|
||||
products: [
|
||||
{ name: 'observability', tier: 'complete' },
|
||||
{ name: 'security', tier: 'essentials' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const coreContext = mockCoreContext.create();
|
||||
// Mock the config service to return our test config
|
||||
coreContext.configService.atPath.mockReturnValue(of(mockConfig));
|
||||
|
||||
service = new PricingService(coreContext);
|
||||
});
|
||||
|
||||
describe('#preboot()', () => {
|
||||
it('registers the pricing routes with default config', () => {
|
||||
service.preboot({ http: prebootHttp });
|
||||
|
||||
// Preboot uses default config, not the loaded config
|
||||
const expectedDefaultConfig = { tiers: { enabled: false, products: [] } };
|
||||
expect((service as any).pricingConfig).toEqual(expectedDefaultConfig);
|
||||
|
||||
// Verify that routes are registered on the preboot HTTP service
|
||||
expect(prebootHttp.registerRoutes).toHaveBeenCalledWith('', expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setup()', () => {
|
||||
it('registers the pricing routes', async () => {
|
||||
await service.preboot({ http: prebootHttp });
|
||||
await service.setup({ http: setupHttp });
|
||||
|
||||
expect(setupHttp.createRouter).toHaveBeenCalledWith('');
|
||||
expect(router.get).toHaveBeenCalledTimes(1);
|
||||
expect(router.get).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
path: '/internal/core/pricing',
|
||||
security: expect.objectContaining({
|
||||
authz: expect.objectContaining({
|
||||
enabled: false,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('allows registering product features', async () => {
|
||||
await service.preboot({ http: prebootHttp });
|
||||
const setup = await service.setup({ http: setupHttp });
|
||||
|
||||
const mockFeatures: PricingProductFeature[] = [
|
||||
{
|
||||
id: 'feature1',
|
||||
description: 'A feature',
|
||||
products: [{ name: 'observability', tier: 'complete' }],
|
||||
},
|
||||
{
|
||||
id: 'feature2',
|
||||
description: 'Another feature',
|
||||
products: [{ name: 'security', tier: 'essentials' }],
|
||||
},
|
||||
];
|
||||
|
||||
setup.registerProductFeatures(mockFeatures);
|
||||
|
||||
// Verify the service has the features registered
|
||||
const registry = (service as any).productFeaturesRegistry;
|
||||
expect(registry.get('feature1')).toBeDefined();
|
||||
expect(registry.get('feature2')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#start()', () => {
|
||||
it('returns a PricingTiersClient with the configured tiers', async () => {
|
||||
await service.preboot({ http: prebootHttp });
|
||||
await service.setup({ http: setupHttp });
|
||||
const start = service.start();
|
||||
|
||||
expect(start).toHaveProperty('isFeatureAvailable');
|
||||
});
|
||||
|
||||
it('returns a PricingTiersClient that can check feature availability', async () => {
|
||||
await service.preboot({ http: prebootHttp });
|
||||
const setup = await service.setup({ http: setupHttp });
|
||||
|
||||
const mockFeatures: PricingProductFeature[] = [
|
||||
{
|
||||
id: 'feature1',
|
||||
description: 'A feature',
|
||||
products: [{ name: 'observability', tier: 'complete' }],
|
||||
},
|
||||
];
|
||||
|
||||
setup.registerProductFeatures(mockFeatures);
|
||||
const start = service.start();
|
||||
|
||||
// Since our mock config has observability product enabled, this feature should be available
|
||||
expect(start.isFeatureAvailable('feature1')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { CoreContext } from '@kbn/core-base-server-internal';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import type { IConfigService } from '@kbn/config';
|
||||
import {
|
||||
type PricingProductFeature,
|
||||
ProductFeaturesRegistry,
|
||||
PricingTiersClient,
|
||||
} from '@kbn/core-pricing-common';
|
||||
import type {
|
||||
InternalHttpServicePreboot,
|
||||
InternalHttpServiceSetup,
|
||||
} from '@kbn/core-http-server-internal';
|
||||
import type { PricingConfigType } from './pricing_config';
|
||||
import { registerRoutes } from './routes';
|
||||
|
||||
interface PrebootDeps {
|
||||
http: InternalHttpServicePreboot;
|
||||
}
|
||||
|
||||
interface SetupDeps {
|
||||
http: InternalHttpServiceSetup;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class PricingService {
|
||||
private readonly configService: IConfigService;
|
||||
private readonly logger: Logger;
|
||||
private readonly productFeaturesRegistry: ProductFeaturesRegistry;
|
||||
private pricingConfig: PricingConfigType;
|
||||
|
||||
constructor(core: CoreContext) {
|
||||
this.logger = core.logger.get('pricing-service');
|
||||
this.configService = core.configService;
|
||||
this.productFeaturesRegistry = new ProductFeaturesRegistry();
|
||||
this.pricingConfig = { tiers: { enabled: false, products: [] } };
|
||||
}
|
||||
|
||||
public preboot({ http }: PrebootDeps) {
|
||||
this.logger.debug('Prebooting pricing service');
|
||||
|
||||
// The preboot server has no need for real pricing.
|
||||
http.registerRoutes('', (router) => {
|
||||
registerRoutes(router, {
|
||||
pricingConfig: this.pricingConfig,
|
||||
productFeaturesRegistry: this.productFeaturesRegistry,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async setup({ http }: SetupDeps) {
|
||||
this.logger.debug('Setting up pricing service');
|
||||
|
||||
this.pricingConfig = await firstValueFrom(
|
||||
this.configService.atPath<PricingConfigType>('pricing')
|
||||
);
|
||||
|
||||
registerRoutes(http.createRouter(''), {
|
||||
pricingConfig: this.pricingConfig,
|
||||
productFeaturesRegistry: this.productFeaturesRegistry,
|
||||
});
|
||||
|
||||
return {
|
||||
registerProductFeatures: (features: PricingProductFeature[]) => {
|
||||
features.forEach((feature) => {
|
||||
this.productFeaturesRegistry.register(feature);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public start() {
|
||||
const tiersClient = new PricingTiersClient(
|
||||
this.pricingConfig.tiers,
|
||||
this.productFeaturesRegistry
|
||||
);
|
||||
|
||||
return {
|
||||
isFeatureAvailable: tiersClient.isFeatureAvailable,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { IRouter } from '@kbn/core-http-server';
|
||||
import type { ProductFeaturesRegistry } from '@kbn/core-pricing-common';
|
||||
import { registerPricingRoutes } from './pricing';
|
||||
import type { PricingConfigType } from '../pricing_config';
|
||||
|
||||
export function registerRoutes(
|
||||
router: IRouter,
|
||||
params: {
|
||||
pricingConfig: PricingConfigType;
|
||||
productFeaturesRegistry: ProductFeaturesRegistry;
|
||||
}
|
||||
) {
|
||||
registerPricingRoutes(router, params);
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { IRouter } from '@kbn/core-http-server';
|
||||
import type { ProductFeaturesRegistry } from '@kbn/core-pricing-common';
|
||||
import type { PricingConfigType } from '../pricing_config';
|
||||
|
||||
export function registerPricingRoutes(
|
||||
router: IRouter,
|
||||
params: {
|
||||
pricingConfig: PricingConfigType;
|
||||
productFeaturesRegistry: ProductFeaturesRegistry;
|
||||
}
|
||||
) {
|
||||
router.get(
|
||||
{
|
||||
path: '/internal/core/pricing',
|
||||
security: {
|
||||
authz: {
|
||||
enabled: false,
|
||||
reason:
|
||||
'Pricing information does not require authorization and should be always accessible.',
|
||||
},
|
||||
},
|
||||
options: { access: 'internal' },
|
||||
validate: false,
|
||||
},
|
||||
async (_context, _req, res) => {
|
||||
return res.ok({
|
||||
body: {
|
||||
tiers: params.pricingConfig.tiers,
|
||||
product_features: params.productFeaturesRegistry.asObject(),
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
28
src/core/packages/pricing/server-internal/tsconfig.json
Normal file
28
src/core/packages/pricing/server-internal/tsconfig.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"extends": "../../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/config",
|
||||
"@kbn/config-schema",
|
||||
"@kbn/core-pricing-common",
|
||||
"@kbn/core-http-server",
|
||||
"@kbn/core-base-server-internal",
|
||||
"@kbn/logging",
|
||||
"@kbn/core-http-server-internal",
|
||||
"@kbn/core-base-server-mocks",
|
||||
"@kbn/core-http-router-server-mocks",
|
||||
"@kbn/core-http-server-mocks",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
]
|
||||
}
|
3
src/core/packages/pricing/server-mocks/README.md
Normal file
3
src/core/packages/pricing/server-mocks/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# @kbn/core-pricing-server-mocks
|
||||
|
||||
Empty package generated by @kbn/generate
|
11
src/core/packages/pricing/server-mocks/index.ts
Normal file
11
src/core/packages/pricing/server-mocks/index.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export { pricingServiceMock } from './src/pricing_service.mock';
|
||||
export type { PricingServiceContract } from './src/pricing_service.mock';
|
14
src/core/packages/pricing/server-mocks/jest.config.js
Normal file
14
src/core/packages/pricing/server-mocks/jest.config.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test/jest_node',
|
||||
rootDir: '../../../../..',
|
||||
roots: ['<rootDir>/src/core/packages/pricing/server-mocks'],
|
||||
};
|
10
src/core/packages/pricing/server-mocks/kibana.jsonc
Normal file
10
src/core/packages/pricing/server-mocks/kibana.jsonc
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"type": "shared-server",
|
||||
"id": "@kbn/core-pricing-server-mocks",
|
||||
"owner": [
|
||||
"@elastic/kibana-core"
|
||||
],
|
||||
"group": "platform",
|
||||
"visibility": "shared",
|
||||
"devOnly": true
|
||||
}
|
7
src/core/packages/pricing/server-mocks/package.json
Normal file
7
src/core/packages/pricing/server-mocks/package.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "@kbn/core-pricing-server-mocks",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"author": "Kibana Core",
|
||||
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import type { PricingServiceSetup, PricingServiceStart } from '@kbn/core-pricing-server';
|
||||
import type { PricingService } from '@kbn/core-pricing-server-internal';
|
||||
|
||||
const createSetupContractMock = () => {
|
||||
const setupContract: jest.Mocked<PricingServiceSetup> = {
|
||||
registerProductFeatures: jest.fn(),
|
||||
};
|
||||
return setupContract;
|
||||
};
|
||||
|
||||
const createStartContractMock = () => {
|
||||
const startContract: jest.Mocked<PricingServiceStart> = {
|
||||
isFeatureAvailable: jest.fn(),
|
||||
};
|
||||
return startContract;
|
||||
};
|
||||
|
||||
export type PricingServiceContract = PublicMethodsOf<PricingService>;
|
||||
|
||||
const createMock = () => {
|
||||
const mocked: jest.Mocked<PricingServiceContract> = {
|
||||
preboot: jest.fn(),
|
||||
setup: jest.fn().mockReturnValue(createSetupContractMock()),
|
||||
start: jest.fn().mockReturnValue(createStartContractMock()),
|
||||
};
|
||||
return mocked;
|
||||
};
|
||||
|
||||
export const pricingServiceMock = {
|
||||
create: createMock,
|
||||
createSetupContract: createSetupContractMock,
|
||||
createStartContract: createStartContractMock,
|
||||
};
|
21
src/core/packages/pricing/server-mocks/tsconfig.json
Normal file
21
src/core/packages/pricing/server-mocks/tsconfig.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"extends": "../../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/utility-types",
|
||||
"@kbn/core-pricing-server",
|
||||
"@kbn/core-pricing-server-internal",
|
||||
]
|
||||
}
|
3
src/core/packages/pricing/server/README.md
Normal file
3
src/core/packages/pricing/server/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# @kbn/core-pricing-browser
|
||||
|
||||
This package contains the public types for Core's server-side pricing service.
|
10
src/core/packages/pricing/server/index.ts
Normal file
10
src/core/packages/pricing/server/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export type { PricingServiceSetup, PricingServiceStart } from './src/contracts';
|
14
src/core/packages/pricing/server/jest.config.js
Normal file
14
src/core/packages/pricing/server/jest.config.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../../../..',
|
||||
roots: ['<rootDir>/src/core/packages/pricing/server'],
|
||||
};
|
9
src/core/packages/pricing/server/kibana.jsonc
Normal file
9
src/core/packages/pricing/server/kibana.jsonc
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"type": "shared-server",
|
||||
"id": "@kbn/core-pricing-server",
|
||||
"owner": [
|
||||
"@elastic/kibana-core"
|
||||
],
|
||||
"group": "platform",
|
||||
"visibility": "shared"
|
||||
}
|
7
src/core/packages/pricing/server/package.json
Normal file
7
src/core/packages/pricing/server/package.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "@kbn/core-pricing-server",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"author": "Kibana Core",
|
||||
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
|
||||
}
|
61
src/core/packages/pricing/server/src/contracts.ts
Normal file
61
src/core/packages/pricing/server/src/contracts.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { IPricingTiersClient, PricingProductFeature } from '@kbn/core-pricing-common';
|
||||
|
||||
/**
|
||||
* APIs to manage pricing product features during the setup phase.
|
||||
*
|
||||
* Plugins that want to register features that are available in specific pricing tiers
|
||||
* should use the `registerProductFeatures` method during the setup phase.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface PricingServiceSetup {
|
||||
/**
|
||||
* Register product features that are available in specific pricing tiers.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // my-plugin/server/plugin.ts
|
||||
* public setup(core: CoreSetup) {
|
||||
* core.pricing.registerProductFeatures([
|
||||
* {
|
||||
* id: 'my_premium_feature',
|
||||
* description: 'A premium feature only available in specific tiers',
|
||||
* products: [{ name: 'security', tier: 'complete' }]
|
||||
* }
|
||||
* ]);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
registerProductFeatures(features: PricingProductFeature[]): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* APIs to access pricing tier information during the start phase.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface PricingServiceStart {
|
||||
/**
|
||||
* Check if a specific feature is available based on the current pricing tier configuration.
|
||||
* Delegates to the underlying {@link IPricingTiersClient.isFeatureAvailable} implementation.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // my-plugin/server/plugin.ts
|
||||
* public start(core: CoreStart) {
|
||||
* const isPremiumFeatureAvailable = core.pricing.isFeatureAvailable('my_premium_feature');
|
||||
* // Use the availability information to enable/disable functionality
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
isFeatureAvailable: IPricingTiersClient['isFeatureAvailable'];
|
||||
}
|
19
src/core/packages/pricing/server/tsconfig.json
Normal file
19
src/core/packages/pricing/server/tsconfig.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "../../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/core-pricing-common",
|
||||
]
|
||||
}
|
|
@ -28,6 +28,7 @@ import { loggingSystemMock } from '@kbn/core-logging-browser-mocks';
|
|||
import { customBrandingServiceMock } from '@kbn/core-custom-branding-browser-mocks';
|
||||
import { securityServiceMock } from '@kbn/core-security-browser-mocks';
|
||||
import { userProfileServiceMock } from '@kbn/core-user-profile-browser-mocks';
|
||||
import { pricingServiceMock } from '@kbn/core-pricing-browser-mocks';
|
||||
|
||||
export const analyticsServiceStartMock = analyticsServiceMock.createAnalyticsServiceStart();
|
||||
export const MockAnalyticsService = analyticsServiceMock.create();
|
||||
|
@ -173,3 +174,9 @@ export const UserProfileServiceConstructor = jest
|
|||
jest.doMock('@kbn/core-user-profile-browser-internal', () => ({
|
||||
UserProfileService: UserProfileServiceConstructor,
|
||||
}));
|
||||
|
||||
export const MockPricingService = pricingServiceMock.create();
|
||||
export const PricingServiceConstructor = jest.fn().mockImplementation(() => MockPricingService);
|
||||
jest.doMock('@kbn/core-pricing-browser-internal', () => ({
|
||||
PricingService: PricingServiceConstructor,
|
||||
}));
|
||||
|
|
|
@ -49,6 +49,8 @@ import {
|
|||
SecurityServiceConstructor,
|
||||
MockUserProfileService,
|
||||
UserProfileServiceConstructor,
|
||||
MockPricingService,
|
||||
PricingServiceConstructor,
|
||||
} from './core_system.test.mocks';
|
||||
import type { EnvironmentMode } from '@kbn/config';
|
||||
import { CoreSystem } from './core_system';
|
||||
|
@ -157,6 +159,7 @@ describe('constructor', () => {
|
|||
expect(CustomBrandingServiceConstructor).toHaveBeenCalledTimes(1);
|
||||
expect(SecurityServiceConstructor).toHaveBeenCalledTimes(1);
|
||||
expect(UserProfileServiceConstructor).toHaveBeenCalledTimes(1);
|
||||
expect(PricingServiceConstructor).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('passes injectedMetadata param to InjectedMetadataService', () => {
|
||||
|
@ -520,6 +523,11 @@ describe('#start()', () => {
|
|||
await startCore();
|
||||
expect(MockUserProfileService.start).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls pricing#start()', async () => {
|
||||
await startCore();
|
||||
expect(MockPricingService.start).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#stop()', () => {
|
||||
|
|
|
@ -38,6 +38,7 @@ import { RenderingService } from '@kbn/core-rendering-browser-internal';
|
|||
import { CoreAppsService } from '@kbn/core-apps-browser-internal';
|
||||
import type { InternalCoreSetup, InternalCoreStart } from '@kbn/core-lifecycle-browser-internal';
|
||||
import { PluginsService } from '@kbn/core-plugins-browser-internal';
|
||||
import { PricingService } from '@kbn/core-pricing-browser-internal';
|
||||
import { CustomBrandingService } from '@kbn/core-custom-branding-browser-internal';
|
||||
import { SecurityService } from '@kbn/core-security-browser-internal';
|
||||
import { UserProfileService } from '@kbn/core-user-profile-browser-internal';
|
||||
|
@ -111,6 +112,7 @@ export class CoreSystem {
|
|||
private readonly customBranding: CustomBrandingService;
|
||||
private readonly security: SecurityService;
|
||||
private readonly userProfile: UserProfileService;
|
||||
private readonly pricing: PricingService;
|
||||
private fatalErrorsSetup: FatalErrorsSetup | null = null;
|
||||
|
||||
constructor(params: CoreSystemParams) {
|
||||
|
@ -166,6 +168,7 @@ export class CoreSystem {
|
|||
this.deprecations = new DeprecationsService();
|
||||
this.executionContext = new ExecutionContextService();
|
||||
this.plugins = new PluginsService(this.coreContext, injectedMetadata.uiPlugins);
|
||||
this.pricing = new PricingService();
|
||||
this.coreApp = new CoreAppsService(this.coreContext);
|
||||
this.customBranding = new CustomBrandingService();
|
||||
|
||||
|
@ -385,6 +388,7 @@ export class CoreSystem {
|
|||
});
|
||||
|
||||
const featureFlags = await this.featureFlags.start();
|
||||
const pricing = await this.pricing.start({ http });
|
||||
|
||||
const core: InternalCoreStart = {
|
||||
analytics,
|
||||
|
@ -408,6 +412,7 @@ export class CoreSystem {
|
|||
security,
|
||||
userProfile,
|
||||
rendering,
|
||||
pricing,
|
||||
};
|
||||
|
||||
await this.plugins.start(core);
|
||||
|
|
|
@ -69,6 +69,8 @@
|
|||
"@kbn/core-injected-metadata-common-internal",
|
||||
"@kbn/core-feature-flags-browser-internal",
|
||||
"@kbn/react-mute-legacy-root-warning",
|
||||
"@kbn/core-pricing-browser-internal",
|
||||
"@kbn/core-pricing-browser-mocks",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -34,6 +34,7 @@ import { statusConfig } from '@kbn/core-status-server-internal';
|
|||
import { uiSettingsConfig } from '@kbn/core-ui-settings-server-internal';
|
||||
import { config as pluginsConfig } from '@kbn/core-plugins-server-internal';
|
||||
import { featureFlagsConfig } from '@kbn/core-feature-flags-server-internal';
|
||||
import { pricingConfig } from '@kbn/core-pricing-server-internal';
|
||||
import { elasticApmConfig } from './root/elastic_config';
|
||||
import { serverlessConfig } from './root/serverless_config';
|
||||
import { coreConfig } from './core_config';
|
||||
|
@ -59,6 +60,7 @@ export function registerServiceConfig(configService: ConfigService) {
|
|||
pathConfig,
|
||||
pidConfig,
|
||||
pluginsConfig,
|
||||
pricingConfig,
|
||||
savedObjectsConfig,
|
||||
savedObjectsMigrationConfig,
|
||||
serverlessConfig,
|
||||
|
|
|
@ -56,6 +56,7 @@ import { DiscoveredPlugins, PluginsService } from '@kbn/core-plugins-server-inte
|
|||
import { CoreAppsService } from '@kbn/core-apps-server-internal';
|
||||
import { SecurityService } from '@kbn/core-security-server-internal';
|
||||
import { UserProfileService } from '@kbn/core-user-profile-server-internal';
|
||||
import { PricingService } from '@kbn/core-pricing-server-internal';
|
||||
import { registerServiceConfig } from './register_service_config';
|
||||
import { MIGRATION_EXCEPTION_CODE } from './constants';
|
||||
import { coreConfig, type CoreConfigType } from './core_config';
|
||||
|
@ -91,6 +92,7 @@ export class Server {
|
|||
private readonly deprecations: DeprecationsService;
|
||||
private readonly executionContext: ExecutionContextService;
|
||||
private readonly prebootService: PrebootService;
|
||||
private readonly pricing: PricingService;
|
||||
private readonly docLinks: DocLinksService;
|
||||
private readonly customBranding: CustomBrandingService;
|
||||
private readonly userSettingsService: UserSettingsService;
|
||||
|
@ -143,6 +145,7 @@ export class Server {
|
|||
this.deprecations = new DeprecationsService(core);
|
||||
this.executionContext = new ExecutionContextService(core);
|
||||
this.prebootService = new PrebootService(core);
|
||||
this.pricing = new PricingService(core);
|
||||
this.docLinks = new DocLinksService(core);
|
||||
this.customBranding = new CustomBrandingService(core);
|
||||
this.userSettingsService = new UserSettingsService(core);
|
||||
|
@ -206,6 +209,8 @@ export class Server {
|
|||
|
||||
this.capabilities.preboot({ http: httpPreboot });
|
||||
|
||||
this.pricing.preboot({ http: httpPreboot });
|
||||
|
||||
const elasticsearchServicePreboot = await this.elasticsearch.preboot();
|
||||
|
||||
await this.status.preboot({ http: httpPreboot });
|
||||
|
@ -359,6 +364,8 @@ export class Server {
|
|||
rendering: renderingSetup,
|
||||
});
|
||||
|
||||
const pricingSetup = await this.pricing.setup({ http: httpSetup });
|
||||
|
||||
const coreSetup: InternalCoreSetup = {
|
||||
analytics: analyticsSetup,
|
||||
capabilities: capabilitiesSetup,
|
||||
|
@ -380,6 +387,7 @@ export class Server {
|
|||
metrics: metricsSetup,
|
||||
deprecations: deprecationsSetup,
|
||||
coreUsageData: coreUsageDataSetup,
|
||||
pricing: pricingSetup,
|
||||
userSettings: userSettingsServiceSetup,
|
||||
security: securitySetup,
|
||||
userProfile: userProfileSetup,
|
||||
|
@ -451,6 +459,8 @@ export class Server {
|
|||
|
||||
const featureFlagsStart = this.featureFlags.start();
|
||||
|
||||
const pricingStart = this.pricing.start();
|
||||
|
||||
this.httpRateLimiter.start();
|
||||
this.status.start();
|
||||
|
||||
|
@ -474,6 +484,7 @@ export class Server {
|
|||
deprecations: deprecationsStart,
|
||||
security: securityStart,
|
||||
userProfile: userProfileStart,
|
||||
pricing: pricingStart,
|
||||
};
|
||||
|
||||
this.coreApp.start(this.coreStart);
|
||||
|
|
|
@ -79,6 +79,7 @@
|
|||
"@kbn/core-feature-flags-server-internal",
|
||||
"@kbn/core-http-rate-limiter-internal",
|
||||
"@kbn/projects-solutions-groups",
|
||||
"@kbn/core-pricing-server-internal",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -281,6 +281,8 @@ export type {
|
|||
ErrorToastOptions,
|
||||
} from '@kbn/core-notifications-browser';
|
||||
|
||||
export type { PricingServiceStart } from '@kbn/core-pricing-browser';
|
||||
|
||||
export type { ToastsApi } from '@kbn/core-notifications-browser-internal';
|
||||
|
||||
export type { CustomBrandingStart, CustomBrandingSetup } from '@kbn/core-custom-branding-browser';
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
// TODO replace the line below with
|
||||
// preset: '@kbn/test/jest_integration_node
|
||||
// to do so, we must fix all integration tests first
|
||||
// see https://github.com/elastic/kibana/pull/130255/
|
||||
preset: '@kbn/test/jest_integration',
|
||||
rootDir: '../../../../..',
|
||||
roots: ['<rootDir>/src/core/server/integration_tests/pricing'],
|
||||
// must override to match all test given there is no `integration_tests` subfolder
|
||||
testMatch: ['**/*.test.{js,mjs,ts,tsx}'],
|
||||
};
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import supertest from 'supertest';
|
||||
import { REPO_ROOT } from '@kbn/repo-info';
|
||||
import { Env } from '@kbn/config';
|
||||
import { getEnvOptions } from '@kbn/config-mocks';
|
||||
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
|
||||
import { executionContextServiceMock } from '@kbn/core-execution-context-server-mocks';
|
||||
import { contextServiceMock } from '@kbn/core-http-context-server-mocks';
|
||||
import {
|
||||
HttpService,
|
||||
InternalHttpServicePreboot,
|
||||
InternalHttpServiceSetup,
|
||||
} from '@kbn/core-http-server-internal';
|
||||
import { createConfigService, createHttpService } from '@kbn/core-http-server-mocks';
|
||||
import { PricingService } from '@kbn/core-pricing-server-internal';
|
||||
import type { PricingProductFeature, PricingProduct } from '@kbn/core-pricing-common';
|
||||
|
||||
const coreId = Symbol('core');
|
||||
|
||||
const env = Env.createDefault(REPO_ROOT, getEnvOptions());
|
||||
|
||||
const configService = createConfigService();
|
||||
|
||||
describe('PricingService', () => {
|
||||
let server: HttpService;
|
||||
let httpPreboot: InternalHttpServicePreboot;
|
||||
let httpSetup: InternalHttpServiceSetup;
|
||||
|
||||
let service: PricingService;
|
||||
let serviceSetup: Awaited<ReturnType<PricingService['setup']>>;
|
||||
|
||||
beforeEach(async () => {
|
||||
server = createHttpService();
|
||||
httpPreboot = await server.preboot({ context: contextServiceMock.createPrebootContract() });
|
||||
httpSetup = await server.setup({
|
||||
context: contextServiceMock.createSetupContract(),
|
||||
executionContext: executionContextServiceMock.createInternalSetupContract(),
|
||||
});
|
||||
service = new PricingService({
|
||||
coreId,
|
||||
env,
|
||||
logger: loggingSystemMock.create(),
|
||||
configService,
|
||||
});
|
||||
await service.preboot({ http: httpPreboot });
|
||||
serviceSetup = await service.setup({ http: httpSetup });
|
||||
await server.start();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await server.stop();
|
||||
});
|
||||
|
||||
describe('/internal/core/pricing route', () => {
|
||||
it('is exposed and returns pricing configuration', async () => {
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.get('/internal/core/pricing')
|
||||
.expect(200);
|
||||
|
||||
expect(result.body).toHaveProperty('tiers');
|
||||
expect(result.body).toHaveProperty('product_features');
|
||||
expect(typeof result.body.tiers).toBe('object');
|
||||
expect(typeof result.body.product_features).toBe('object');
|
||||
});
|
||||
|
||||
it('returns default pricing configuration when no custom config is provided', async () => {
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.get('/internal/core/pricing')
|
||||
.expect(200);
|
||||
|
||||
expect(result.body).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"product_features": Object {},
|
||||
"tiers": Object {
|
||||
"enabled": true,
|
||||
"products": Array [],
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('includes registered product features in the response', async () => {
|
||||
// Register a product feature
|
||||
const testFeature: PricingProductFeature = {
|
||||
id: 'test_feature',
|
||||
description: 'A test feature for integration testing',
|
||||
products: [
|
||||
{ name: 'observability', tier: 'complete' },
|
||||
{ name: 'security', tier: 'essentials' },
|
||||
] as PricingProduct[],
|
||||
};
|
||||
|
||||
serviceSetup.registerProductFeatures([testFeature]);
|
||||
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.get('/internal/core/pricing')
|
||||
.expect(200);
|
||||
|
||||
expect(result.body.product_features).toHaveProperty('test_feature');
|
||||
expect(result.body.product_features.test_feature).toEqual(testFeature);
|
||||
});
|
||||
|
||||
it('handles multiple registered product features', async () => {
|
||||
const feature1: PricingProductFeature = {
|
||||
id: 'feature_1',
|
||||
description: 'First test feature',
|
||||
products: [
|
||||
{ name: 'observability', tier: 'complete' },
|
||||
{ name: 'security', tier: 'essentials' },
|
||||
] as PricingProduct[],
|
||||
};
|
||||
|
||||
const feature2: PricingProductFeature = {
|
||||
id: 'feature_2',
|
||||
description: 'Second test feature',
|
||||
products: [
|
||||
{
|
||||
name: 'security',
|
||||
tier: 'complete',
|
||||
},
|
||||
] as PricingProduct[],
|
||||
};
|
||||
|
||||
serviceSetup.registerProductFeatures([feature1, feature2]);
|
||||
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.get('/internal/core/pricing')
|
||||
.expect(200);
|
||||
|
||||
expect(result.body.product_features).toHaveProperty('feature_1');
|
||||
expect(result.body.product_features).toHaveProperty('feature_2');
|
||||
expect(result.body.product_features.feature_1).toEqual(feature1);
|
||||
expect(result.body.product_features.feature_2).toEqual(feature2);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -173,6 +173,9 @@
|
|||
"@kbn/core-feature-flags-browser-mocks",
|
||||
"@kbn/core-feature-flags-server",
|
||||
"@kbn/core-feature-flags-server-mocks",
|
||||
"@kbn/core-pricing-browser",
|
||||
"@kbn/core-pricing-server-internal",
|
||||
"@kbn/core-pricing-common",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -558,6 +558,20 @@
|
|||
"@kbn/core-preboot-server-internal/*": ["src/core/packages/preboot/server-internal/*"],
|
||||
"@kbn/core-preboot-server-mocks": ["src/core/packages/preboot/server-mocks"],
|
||||
"@kbn/core-preboot-server-mocks/*": ["src/core/packages/preboot/server-mocks/*"],
|
||||
"@kbn/core-pricing-browser": ["src/core/packages/pricing/browser"],
|
||||
"@kbn/core-pricing-browser/*": ["src/core/packages/pricing/browser/*"],
|
||||
"@kbn/core-pricing-browser-internal": ["src/core/packages/pricing/browser-internal"],
|
||||
"@kbn/core-pricing-browser-internal/*": ["src/core/packages/pricing/browser-internal/*"],
|
||||
"@kbn/core-pricing-browser-mocks": ["src/core/packages/pricing/browser-mocks"],
|
||||
"@kbn/core-pricing-browser-mocks/*": ["src/core/packages/pricing/browser-mocks/*"],
|
||||
"@kbn/core-pricing-common": ["src/core/packages/pricing/common"],
|
||||
"@kbn/core-pricing-common/*": ["src/core/packages/pricing/common/*"],
|
||||
"@kbn/core-pricing-server": ["src/core/packages/pricing/server"],
|
||||
"@kbn/core-pricing-server/*": ["src/core/packages/pricing/server/*"],
|
||||
"@kbn/core-pricing-server-internal": ["src/core/packages/pricing/server-internal"],
|
||||
"@kbn/core-pricing-server-internal/*": ["src/core/packages/pricing/server-internal/*"],
|
||||
"@kbn/core-pricing-server-mocks": ["src/core/packages/pricing/server-mocks"],
|
||||
"@kbn/core-pricing-server-mocks/*": ["src/core/packages/pricing/server-mocks/*"],
|
||||
"@kbn/core-provider-plugin": ["src/platform/test/plugin_functional/plugins/core_provider_plugin"],
|
||||
"@kbn/core-provider-plugin/*": ["src/platform/test/plugin_functional/plugins/core_provider_plugin/*"],
|
||||
"@kbn/core-rendering-browser": ["src/core/packages/rendering/browser"],
|
||||
|
|
|
@ -15,6 +15,7 @@ import { I18nProvider } from '@kbn/i18n-react';
|
|||
|
||||
import type {
|
||||
PluginsServiceStart,
|
||||
PricingServiceStart,
|
||||
SecurityServiceStart,
|
||||
UserProfileServiceStart,
|
||||
} from '@kbn/core/public';
|
||||
|
@ -103,6 +104,7 @@ export const StorybookContext: React.FC<{
|
|||
theme$: EMPTY,
|
||||
getTheme: () => ({ darkMode: false, name: 'amsterdam' }),
|
||||
},
|
||||
pricing: {} as unknown as PricingServiceStart,
|
||||
security: {} as unknown as SecurityServiceStart,
|
||||
userProfile: {} as unknown as UserProfileServiceStart,
|
||||
plugins: {} as unknown as PluginsServiceStart,
|
||||
|
|
28
yarn.lock
28
yarn.lock
|
@ -4846,6 +4846,34 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/core-pricing-browser-internal@link:src/core/packages/pricing/browser-internal":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/core-pricing-browser-mocks@link:src/core/packages/pricing/browser-mocks":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/core-pricing-browser@link:src/core/packages/pricing/browser":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/core-pricing-common@link:src/core/packages/pricing/common":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/core-pricing-server-internal@link:src/core/packages/pricing/server-internal":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/core-pricing-server-mocks@link:src/core/packages/pricing/server-mocks":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/core-pricing-server@link:src/core/packages/pricing/server":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/core-provider-plugin@link:src/platform/test/plugin_functional/plugins/core_provider_plugin":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue