feat: 🎸 Add handler to make search requests to client daemon (#1989)

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-11691
This commit is contained in:
Zhihe Li committed 2024-01-16 10:17:45 -05:00
1 parent b8b970ce4d
commit 4bf1a88764
15 files changed
+195 -34

No files matched your search

@@ -0,0 +1,67 @@
import { pluralize } from 'ember-inflector';
/**
* Not all types are yet supported by the client daemon so we'll
* just whitelist the ones we need to start with.
* @type {string[]}
*/
const supportedTypes = ['target', 'session'];
/**
* Handler to sit in front of the API request layer
* so we can request from the daemon first
*/
const ClientDaemonHandler = {
async request(context, next) {
switch (context.request.op) {
case 'query': {
const { store, data } = context.request;
const { type, query } = data;
const isClientDaemonRunning = await this.ipc.invoke(
'isClientDaemonRunning'
);
if (!supportedTypes.includes(type) || !isClientDaemonRunning) {
return next(context.request);
}
// TODO: Remove usages of recursive from callers and scope_id since
// all calls to daemon are recursive, scope_id can be part of search query instead
// eslint-disable-next-line no-unused-vars
let { recursive, scope_id, ...modifiedQuery } = query;
const auth_token_id = this.session.data?.authenticated?.id;
modifiedQuery = {
...modifiedQuery,
auth_token_id,
resource: pluralize(type),
};
// 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(
await this.ipc.invoke('searchClientDaemon', modifiedQuery)
);
const payload = { items: results ?? [] };
const schema = store.modelFor(type);
const serializer = store.serializerFor(type);
const normalizedPayload = serializer.normalizeResponse(
store,
schema,
payload,
null,
'query'
);
// TODO: Add some pagination
return store.push(normalizedPayload);
}
default:
return next(context.request);
}
},
};
export default ClientDaemonHandler;
@@ -0,0 +1,6 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
export { default } from 'api/handlers/client-daemon-handler';
+4
View File
@@ -183,6 +183,10 @@ export default function initializeMockIPC(server, config) {
return faker.system.filePath();
}
addTokenToClientDaemon() {}
searchClientDaemon() {}
isClientDaemonRunning() {
return false;
}
}
/**
+21
View File
@@ -0,0 +1,21 @@
import Store, { CacheHandler } from '@ember-data/store';
import RequestManager from '@ember-data/request';
import { LegacyNetworkHandler } from '@ember-data/legacy-compat';
import { inject as service } from '@ember/service';
import ClientDaemonHandler from 'api/handlers/client-daemon-handler';
export default class extends Store {
@service ipc;
@service session;
requestManager = new RequestManager();
constructor(args) {
super(args);
ClientDaemonHandler.request = ClientDaemonHandler.request.bind(this);
this.requestManager.use([ClientDaemonHandler, LegacyNetworkHandler]);
this.requestManager.useCache(CacheHandler);
}
}
@@ -119,6 +119,9 @@ handle('closeWindow', () => app.quit());
*/
handle('checkCommand', async (command) => which(command, { nothrow: true }));
/**
* Adds the user's token to the client daemon.
*/
handle('addTokenToClientDaemon', async (data) =>
clientDaemonManager.addToken(data)
);
@@ -132,6 +135,21 @@ handle('checkOS', () => ({
isWindows: isWindows(),
}));
/**
* Call the client daemon's search endpoint to retrieve cached results.
*/
handle('searchClientDaemon', async (request) =>
clientDaemonManager.search(request)
);
/**
* Check to see if the client daemon is running. We use the presence of a
* socket path as a proxy for whether the daemon is running.
*/
handle('isClientDaemonRunning', async (request) =>
Boolean(clientDaemonManager.socketPath)
);
/**
* Handler to help create terminal windows. We don't use the helper `handle` method
* as we need access to the event and don't need to be using `ipcMain.handle`.
@@ -24,7 +24,7 @@ class ClientDaemonManager {
const data = spawnSync(daemonStatusCommand);
const status = jsonify(data);
this.#socketPath = status.socket_address;
this.#socketPath = status?.socket_address;
return status;
}
@@ -33,7 +33,13 @@ class ClientDaemonManager {
* @returns {Promise<void>}
*/
async start() {
const startDaemonCommand = ['daemon', 'start'];
// TODO: Finalize how often the daemon should be refreshing
const startDaemonCommand = [
'daemon',
'start',
'-refresh-interval-seconds',
'10',
];
await spawn(startDaemonCommand);
this.status();
}
@@ -67,6 +73,17 @@ class ClientDaemonManager {
};
return unixSocketRequest(request, postBody);
}
search(requestData) {
const queryString = new URLSearchParams(requestData);
const request = {
method: 'GET',
path: `http://internal.boundary.local/v1/search?${queryString.toString()}`,
socketPath: this.#socketPath,
};
return unixSocketRequest(request);
}
}
// Export an instance so we get a singleton
@@ -22,6 +22,7 @@ import {
invalidateSession,
} from 'ember-simple-auth/test-support';
import WindowMockIPC from '../helpers/window-mock-ipc';
import Store from 'api/services/store';
module('Acceptance | authentication', function (hooks) {
setupApplicationTest(hooks);
@@ -128,6 +129,9 @@ module('Acceptance | authentication', function (hooks) {
// Mock the postMessage interface used by IPC.
this.owner.register('service:browser/window', WindowMockIPC);
setDefaultClusterUrl(this);
// Use the original store so we don't try and hit the client daemon
this.owner.register('service:store', Store);
});
test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) {
@@ -13,6 +13,7 @@ import { invalidateSession } from 'ember-simple-auth/test-support';
import { setupBrowserFakes } from 'ember-browser-services/test-support';
import WindowMockIPC from '../helpers/window-mock-ipc';
import config from '../../config/environment';
import Store from 'api/services/store';
module('Acceptance | clusterUrl', function (hooks) {
setupApplicationTest(hooks);
@@ -100,6 +101,9 @@ module('Acceptance | clusterUrl', function (hooks) {
urls.authenticate.methods.global = `${urls.authenticate.global}/${instances.authMethods.global.id}`;
urls.projects = `${urls.scopes.global}/projects`;
urls.targets = `${urls.projects}/targets`;
// Use the original store so we don't try and hit the client daemon
this.owner.register('service:store', Store);
});
hooks.afterEach(function () {
@@ -22,6 +22,7 @@ import {
STATUS_SESSION_CANCELING,
STATUS_SESSION_TERMINATED,
} from 'api/models/session';
import Store from 'api/services/store';
module('Acceptance | projects | sessions', function (hooks) {
setupApplicationTest(hooks);
@@ -122,6 +123,13 @@ module('Acceptance | projects | sessions', function (hooks) {
const ipcService = this.owner.lookup('service:ipc');
stubs.ipcService = sinon.stub(ipcService, 'invoke');
// Use the original store so we don't try and hit the client daemon
this.owner.register('service:store', Store);
});
hooks.afterEach(function () {
sinon.restore();
});
test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) {
@@ -15,6 +15,7 @@ import { authenticateSession } from 'ember-simple-auth/test-support';
import sinon from 'sinon';
import WindowMockIPC from '../../../helpers/window-mock-ipc';
import { STATUS_SESSION_ACTIVE } from 'api/models/session';
import Store from 'api/services/store';
module('Acceptance | projects | sessions | session', function (hooks) {
setupApplicationTest(hooks);
@@ -121,9 +122,13 @@ module('Acceptance | projects | sessions | session', function (hooks) {
const ipcService = this.owner.lookup('service:ipc');
stubs.ipcService = sinon.stub(ipcService, 'invoke');
// Use the original store so we don't try and hit the client daemon
this.owner.register('service:store', Store);
});
hooks.afterEach(function () {
sinon.restore();
// reset onUncaughtException to original state
QUnit.onUncaughtException = originalUncaughtException;
});
@@ -21,6 +21,7 @@ import {
invalidateSession,
currentSession,
} from 'ember-simple-auth/test-support';
import Store from 'api/services/store';
module('Acceptance | projects | targets', function (hooks) {
setupApplicationTest(hooks);
@@ -138,6 +139,13 @@ module('Acceptance | projects | targets', function (hooks) {
const ipcService = this.owner.lookup('service:ipc');
stubs.ipcService = sinon.stub(ipcService, 'invoke');
// Use the original store so we don't try and hit the client daemon
this.owner.register('service:store', Store);
});
hooks.afterEach(function () {
sinon.restore();
});
test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) {
@@ -10,6 +10,7 @@ import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import sinon from 'sinon';
import { authenticateSession } from 'ember-simple-auth/test-support';
import WindowMockIPC from '../../../helpers/window-mock-ipc';
import Store from 'api/services/store';
module('Acceptance | projects | targets | target', function (hooks) {
setupApplicationTest(hooks);
@@ -129,6 +130,13 @@ module('Acceptance | projects | targets | target', function (hooks) {
const ipcService = this.owner.lookup('service:ipc');
stubs.ipcService = sinon.stub(ipcService, 'invoke');
// Use the original store so we don't try and hit the client daemon
this.owner.register('service:store', Store);
});
hooks.afterEach(function () {
sinon.restore();
});
test('user can connect to a target with an address', async function (assert) {
+7 -32
View File
@@ -15,14 +15,13 @@ import {
authenticateSession,
invalidateSession,
} from 'ember-simple-auth/test-support';
import WindowMockIPC from '../helpers/window-mock-ipc';
import Store from 'api/services/store';
module('Acceptance | scopes', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
let mockIPC;
let messageHandler;
const instances = {
scopes: {
global: null,
@@ -129,42 +128,18 @@ module('Acceptance | scopes', function (hooks) {
urls.globalTargets = `${urls.globalProjects}/targets`;
urls.target = `${urls.targets}/${instances.target.id}`;
class MockIPC {
clusterUrl = null;
invoke(method, payload) {
return this[method](payload);
}
getClusterUrl() {
return this.clusterUrl;
}
setClusterUrl(clusterUrl) {
this.clusterUrl = clusterUrl;
return this.clusterUrl;
}
}
mockIPC = new MockIPC();
messageHandler = async function (event) {
if (event.origin !== window.location.origin) return;
const { method, payload } = event.data;
if (method) {
const response = await mockIPC.invoke(method, payload);
event.ports[0].postMessage(response);
}
};
window.addEventListener('message', messageHandler);
this.owner.register('service:browser/window', WindowMockIPC);
setDefaultClusterUrl(this);
const ipcService = this.owner.lookup('service:ipc');
stubs.ipcService = sinon.stub(ipcService, 'invoke');
// Use the original store so we don't try and hit the client daemon
this.owner.register('service:store', Store);
});
hooks.afterEach(function () {
window.removeEventListener('message', messageHandler);
sinon.restore();
});
test('visiting index', async function (assert) {
@@ -25,6 +25,10 @@ class MockIPC {
hasMacOSChrome() {}
showWindowActions() {}
addTokenToClientDaemon() {}
searchClientDaemon() {}
isClientDaemonRunning() {
return false;
}
}
/**
@@ -0,0 +1,12 @@
import { module, test } from 'qunit';
import { setupTest } from 'desktop/tests/helpers';
module('Unit | Service | store', function (hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
test('it exists', function (assert) {
let service = this.owner.lookup('service:store');
assert.ok(service);
});
});