1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-09 00:18:00 +01:00
unleash.unleash/src/lib/services/addon-service.ts

299 lines
10 KiB
TypeScript
Raw Normal View History

import memoizee from 'memoizee';
import { ValidationError } from 'joi';
import { getAddons, IAddonProviders } from '../addons';
import * as events from '../types/events';
import { addonSchema } from './addon-schema';
import NameExistsError from '../error/name-exists-error';
import { IEventStore } from '../types/stores/event-store';
import { IFeatureToggleStore } from '../types/stores/feature-toggle-store';
import { Logger } from '../logger';
import TagTypeService from './tag-type-service';
import { IAddon, IAddonDto, IAddonStore } from '../types/stores/addon-store';
import { IUnleashStores, IUnleashConfig } from '../types';
import { IAddonDefinition } from '../types/model';
import { minutesToMilliseconds } from 'date-fns';
const SUPPORTED_EVENTS = Object.keys(events).map((k) => events[k]);
const MASKED_VALUE = '*****';
const WILDCARD_OPTION = '*';
interface ISensitiveParams {
[key: string]: string[];
}
export default class AddonService {
eventStore: IEventStore;
addonStore: IAddonStore;
featureToggleStore: IFeatureToggleStore;
logger: Logger;
tagTypeService: TagTypeService;
addonProviders: IAddonProviders;
sensitiveParams: ISensitiveParams;
fetchAddonConfigs: (() => Promise<IAddon[]>) &
memoizee.Memoized<() => Promise<IAddon[]>>;
constructor(
{
addonStore,
eventStore,
featureToggleStore,
}: Pick<
IUnleashStores,
'addonStore' | 'eventStore' | 'featureToggleStore'
>,
{ getLogger, server }: Pick<IUnleashConfig, 'getLogger' | 'server'>,
tagTypeService: TagTypeService,
addons?: IAddonProviders,
) {
this.eventStore = eventStore;
this.addonStore = addonStore;
this.featureToggleStore = featureToggleStore;
this.logger = getLogger('services/addon-service.js');
this.tagTypeService = tagTypeService;
this.addonProviders =
addons ||
getAddons({
getLogger,
unleashUrl: server.unleashUrl,
});
this.sensitiveParams = this.loadSensitiveParams(this.addonProviders);
if (addonStore) {
this.registerEventHandler();
}
// Memoized private function
this.fetchAddonConfigs = memoizee(
async () => addonStore.getAll({ enabled: true }),
{
promise: true,
maxAge: minutesToMilliseconds(1),
},
);
}
loadSensitiveParams(addonProviders: IAddonProviders): ISensitiveParams {
const providerDefinitions = Object.values(addonProviders).map(
(p) => p.definition,
);
return providerDefinitions.reduce((obj, definition) => {
const sensitiveParams = definition.parameters
.filter((p) => p.sensitive)
.map((p) => p.name);
const o = { ...obj };
o[definition.name] = sensitiveParams;
return o;
}, {});
}
registerEventHandler(): void {
SUPPORTED_EVENTS.forEach((eventName) =>
this.eventStore.on(eventName, this.handleEvent(eventName)),
);
}
handleEvent(eventName: string): (IEvent) => void {
const { addonProviders } = this;
return (event) => {
this.fetchAddonConfigs().then((addonInstances) => {
addonInstances
.filter((addon) => addon.events.includes(eventName))
.filter(
(addon) =>
!addon.projects ||
addon.projects.length == 0 ||
addon.projects[0] === WILDCARD_OPTION ||
addon.projects.includes(event.project),
)
.filter(
(addon) =>
!addon.environments ||
addon.environments.length == 0 ||
addon.environments[0] === WILDCARD_OPTION ||
addon.environments.includes(event.environment),
)
.filter((addon) => addonProviders[addon.provider])
.forEach((addon) =>
addonProviders[addon.provider].handleEvent(
event,
addon.parameters,
),
);
});
};
}
// Should be used by the controller.
async getAddons(): Promise<IAddon[]> {
const addonConfigs = await this.addonStore.getAll();
return addonConfigs.map((a) => this.filterSensitiveFields(a));
}
filterSensitiveFields(addonConfig: IAddon): IAddon {
const { sensitiveParams } = this;
const a = { ...addonConfig };
a.parameters = Object.keys(a.parameters).reduce((obj, paramKey) => {
const o = { ...obj };
if (sensitiveParams[a.provider].includes(paramKey)) {
o[paramKey] = MASKED_VALUE;
} else {
o[paramKey] = a.parameters[paramKey];
}
return o;
}, {});
return a;
}
async getAddon(id: number): Promise<IAddon> {
const addonConfig = await this.addonStore.get(id);
return this.filterSensitiveFields(addonConfig);
}
getProviderDefinitions(): IAddonDefinition[] {
const { addonProviders } = this;
return Object.values(addonProviders).map((p) => p.definition);
}
async addTagTypes(providerName: string): Promise<void> {
const provider = this.addonProviders[providerName];
if (provider) {
const tagTypes = provider.definition.tagTypes || [];
const createTags = tagTypes.map(async (tagType) => {
try {
await this.tagTypeService.validateUnique(tagType);
await this.tagTypeService.createTagType(
tagType,
providerName,
);
} catch (err) {
if (!(err instanceof NameExistsError)) {
this.logger.error(err);
}
}
});
await Promise.all(createTags);
}
return Promise.resolve();
}
async createAddon(data: IAddonDto, userName: string): Promise<IAddon> {
const addonConfig = await addonSchema.validateAsync(data);
await this.validateKnownProvider(addonConfig);
await this.validateRequiredParameters(addonConfig);
const createdAddon = await this.addonStore.insert(addonConfig);
await this.addTagTypes(createdAddon.provider);
this.logger.info(
`User ${userName} created addon ${addonConfig.provider}`,
);
await this.eventStore.store({
type: events.ADDON_CONFIG_CREATED,
createdBy: userName,
data: { provider: addonConfig.provider },
});
return createdAddon;
}
async updateAddon(
id: number,
data: IAddonDto,
userName: string,
): Promise<IAddon> {
const addonConfig = await addonSchema.validateAsync(data);
await this.validateRequiredParameters(addonConfig);
if (this.sensitiveParams[addonConfig.provider].length > 0) {
const existingConfig = await this.addonStore.get(id);
addonConfig.parameters = Object.keys(addonConfig.parameters).reduce(
(params, key) => {
const o = { ...params };
if (addonConfig.parameters[key] === MASKED_VALUE) {
o[key] = existingConfig.parameters[key];
} else {
o[key] = addonConfig.parameters[key];
}
return o;
},
{},
);
}
const result = await this.addonStore.update(id, addonConfig);
await this.eventStore.store({
type: events.ADDON_CONFIG_UPDATED,
createdBy: userName,
data: { id, provider: addonConfig.provider },
});
this.logger.info(`User ${userName} updated addon ${id}`);
return result;
}
async removeAddon(id: number, userName: string): Promise<void> {
await this.addonStore.delete(id);
await this.eventStore.store({
type: events.ADDON_CONFIG_DELETED,
createdBy: userName,
data: { id },
});
this.logger.info(`User ${userName} removed addon ${id}`);
}
async validateKnownProvider(config: Partial<IAddonDto>): Promise<boolean> {
OpenAPI: addon operations (#3421) This PR updates the OpenAPI schemas for all the operations tagged with "addons". In doing so, I also uncovered a few bugs and inconsistencies. These have also been fixed. ## Changes I've added inline comments to the changed files to call out anything that I think is worth clarifying specifically. As an overall description, this PR does the following: Splits `addon-schema` into `addon-schema` and `addon-create-update-schema`. The former is used when describing addons that exist within Unleash and contain IDs and `created_at` timestamps. The latter is used when creating or updating addons. Adds examples and descriptions to all relevant schemas (and their dependencies). Updates addons operations descriptions and response codes (including the recently introduced 413 and 415). Fixes a bug where the server would crash if it didn't recognize the addon provider (test added). Fixes a bug where updating an addon wouldn't return anything, even if the API said that it would. (test added) Resolves some inconsistencies in handling of addon description. (tests added) ### Addon descriptions when creating addons, descriptions are optional. The original `addonSchema` said they could be `null | string | undefined`. This caused some inconsistencies in return values. Sometimes they were returned, other times not. I've made it so that `descriptions` are now always returned from the API. If it's not defined or if it's set to `null`, the API will return `description: null`. ### `IAddonDto` `IAddonDto`, the type we used internally to model the incoming addons (for create and update) says that `description` is required. This hasn't been true at least since we introduced OpenAPI schemas. As such, the update and insert methods that the service uses were incompatible with the **actual** data that we require. I've changed the type to reflect reality for now. Assuming the tests pass, this **should** all be good, but I'd like the reviewer(s) to give this a think too. --------- Co-authored-by: Christopher Kolstad <chriswk@getunleash.ai>
2023-04-18 12:50:34 +02:00
if (!config.provider) {
throw new ValidationError(
'No addon provider supplied. The property was either missing or an empty value.',
[],
undefined,
);
}
const p = this.addonProviders[config.provider];
if (!p) {
OpenAPI: addon operations (#3421) This PR updates the OpenAPI schemas for all the operations tagged with "addons". In doing so, I also uncovered a few bugs and inconsistencies. These have also been fixed. ## Changes I've added inline comments to the changed files to call out anything that I think is worth clarifying specifically. As an overall description, this PR does the following: Splits `addon-schema` into `addon-schema` and `addon-create-update-schema`. The former is used when describing addons that exist within Unleash and contain IDs and `created_at` timestamps. The latter is used when creating or updating addons. Adds examples and descriptions to all relevant schemas (and their dependencies). Updates addons operations descriptions and response codes (including the recently introduced 413 and 415). Fixes a bug where the server would crash if it didn't recognize the addon provider (test added). Fixes a bug where updating an addon wouldn't return anything, even if the API said that it would. (test added) Resolves some inconsistencies in handling of addon description. (tests added) ### Addon descriptions when creating addons, descriptions are optional. The original `addonSchema` said they could be `null | string | undefined`. This caused some inconsistencies in return values. Sometimes they were returned, other times not. I've made it so that `descriptions` are now always returned from the API. If it's not defined or if it's set to `null`, the API will return `description: null`. ### `IAddonDto` `IAddonDto`, the type we used internally to model the incoming addons (for create and update) says that `description` is required. This hasn't been true at least since we introduced OpenAPI schemas. As such, the update and insert methods that the service uses were incompatible with the **actual** data that we require. I've changed the type to reflect reality for now. Assuming the tests pass, this **should** all be good, but I'd like the reviewer(s) to give this a think too. --------- Co-authored-by: Christopher Kolstad <chriswk@getunleash.ai>
2023-04-18 12:50:34 +02:00
throw new ValidationError(
`Unknown addon provider ${config.provider}`,
[],
undefined,
);
} else {
return true;
}
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async validateRequiredParameters({
provider,
parameters,
}): Promise<boolean> {
const providerDefinition = this.addonProviders[provider].definition;
const requiredParamsMissing = providerDefinition.parameters
.filter((p) => p.required)
.map((p) => p.name)
.filter(
(requiredParam) =>
!Object.keys(parameters).includes(requiredParam),
);
if (requiredParamsMissing.length > 0) {
throw new ValidationError(
`Missing required parameters: ${requiredParamsMissing.join(
',',
)} `,
[],
undefined,
);
}
return true;
}
}