175 lines
5.7 KiB
JavaScript
175 lines
5.7 KiB
JavaScript
/**
|
|
* Copyright (c) HashiCorp, Inc.
|
|
* SPDX-License-Identifier: BUSL-1.1
|
|
*/
|
|
|
|
/* global __electronLog */
|
|
import { inject as service } from '@ember/service';
|
|
import { getOwner, setOwner } from '@ember/application';
|
|
import { generateMQLExpression } from '../utils/mql-query';
|
|
import { paginateResults } from '../utils/paginate-results';
|
|
|
|
/**
|
|
* Not all types are yet supported by the cache daemon so we'll
|
|
* just whitelist the ones we need to start with. Keys are the resource
|
|
* types and the values are the fields that are searchable by the client.
|
|
* @type {{session: string[], alias: string[], target: string[]}}
|
|
*/
|
|
const supportedTypes = {
|
|
target: ['id', 'name', 'description', 'address', 'scope_id'],
|
|
session: ['id', 'type', 'status', 'endpoint', 'scope_id', 'target_id'],
|
|
alias: ['id', 'type', 'value', 'destination_id'],
|
|
};
|
|
|
|
/**
|
|
* Map the model resource name to the cache daemon resource name
|
|
* @type {{session: string, alias: string, target: string}}
|
|
*/
|
|
export const resourceNames = {
|
|
target: 'targets',
|
|
session: 'sessions',
|
|
alias: 'resolvable-aliases',
|
|
};
|
|
|
|
const fetchControllerData = async (context, next) => {
|
|
const { data } = context.request;
|
|
const { query: originalQuery } = data;
|
|
// remove cache daemon specific query params
|
|
// eslint-disable-next-line no-unused-vars
|
|
const { query, page, pageSize, ...remainingQuery } = originalQuery;
|
|
|
|
// If we get an error or cache daemon is unavailable, fall back to calling the API
|
|
context.request.data.query = remainingQuery;
|
|
const results = await next(context.request);
|
|
const models = results.content.toArray();
|
|
|
|
const paginatedResult = paginateResults(models, page, pageSize);
|
|
paginatedResult.meta = { totalItems: models.length };
|
|
return paginatedResult;
|
|
};
|
|
|
|
/**
|
|
* Handler to sit in front of the API request layer
|
|
* so we can request from the daemon first
|
|
*/
|
|
export default class CacheDaemonHandler {
|
|
@service session;
|
|
@service ipc;
|
|
|
|
constructor(context) {
|
|
setOwner(this, getOwner(context));
|
|
}
|
|
|
|
async request(context, next) {
|
|
switch (context.request.op) {
|
|
case 'query': {
|
|
const { store, data } = context.request;
|
|
const { type, query, options: { pushToStore = true } = {} } = data;
|
|
const isCacheDaemonRunning = await this.ipc.invoke(
|
|
'isCacheDaemonRunning',
|
|
);
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
let { recursive, scope_id, page, pageSize, ...remainingQuery } = query;
|
|
let searchQuery = '';
|
|
|
|
if (
|
|
!Object.keys(supportedTypes).includes(type) ||
|
|
!isCacheDaemonRunning
|
|
) {
|
|
return fetchControllerData(context, next);
|
|
}
|
|
|
|
if (remainingQuery.query) {
|
|
const { search, filters } = remainingQuery.query;
|
|
|
|
searchQuery = generateMQLExpression({
|
|
search: {
|
|
text: search,
|
|
fields: supportedTypes[type],
|
|
},
|
|
filters,
|
|
});
|
|
}
|
|
const sessionData = this.session.data?.authenticated;
|
|
const auth_token_id = sessionData?.id;
|
|
const token = sessionData?.token;
|
|
remainingQuery = {
|
|
...remainingQuery,
|
|
query: searchQuery,
|
|
auth_token_id,
|
|
token,
|
|
resource: resourceNames[type],
|
|
};
|
|
|
|
let cacheDaemonResults = {};
|
|
try {
|
|
cacheDaemonResults = await this.ipc.invoke(
|
|
'searchCacheDaemon',
|
|
remainingQuery,
|
|
);
|
|
} catch (e) {
|
|
// If we got a 403, most likely the cache daemon was restarted and our token is no longer valid
|
|
// I'm not sure if we can get a 401 since we always send a token but we'll handle it in the same way
|
|
if (e.statusCode === 403 || e.statusCode === 401) {
|
|
try {
|
|
await this.ipc.invoke('addTokenToDaemons', {
|
|
tokenId: auth_token_id,
|
|
token,
|
|
});
|
|
cacheDaemonResults = await this.ipc.invoke(
|
|
'searchCacheDaemon',
|
|
remainingQuery,
|
|
);
|
|
} catch (err) {
|
|
// If it fails again just fall back to fetching controller data
|
|
__electronLog?.error('Failed to add token to daemons', err);
|
|
}
|
|
} else {
|
|
__electronLog?.error(
|
|
'Failed to search cache daemon; falling back to search controller',
|
|
e,
|
|
);
|
|
|
|
return fetchControllerData(context, next);
|
|
}
|
|
}
|
|
|
|
// Currently returns with a singular top level field with resource name
|
|
// e.g. { targets: [...] } or { sessions: [..] }
|
|
// So this just unwraps to the array, or undefined
|
|
const [results] = Object.values(cacheDaemonResults);
|
|
const payload = { items: paginateResults(results, page, pageSize) };
|
|
|
|
const schema = store.modelFor(type);
|
|
const serializer = store.serializerFor(type);
|
|
const normalizedPayload = serializer.normalizeResponse(
|
|
store,
|
|
schema,
|
|
payload,
|
|
null,
|
|
'query',
|
|
);
|
|
|
|
// Return the raw data if we don't push to the store.
|
|
let records = normalizedPayload.data.map((record) => ({
|
|
...record.attributes,
|
|
id: record.id,
|
|
}));
|
|
if (pushToStore) {
|
|
records = store.push(normalizedPayload);
|
|
}
|
|
|
|
// Set a meta property on the array to store the total items of the results.
|
|
// This isn't conventional but is better than returning an ArrayProxy
|
|
// or EmberArray since the ember store query method asserts it has to be an array
|
|
// so we can't just return an object.
|
|
records.meta = { totalItems: results?.length ?? 0 };
|
|
return records;
|
|
}
|
|
default:
|
|
return next(context.request);
|
|
}
|
|
}
|
|
}
|