feat: 🎸 Add handler to make search requests to client daemon (#1989)
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-11691
This commit is contained in:
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';
|
||||
@@ -183,6 +183,10 @@ export default function initializeMockIPC(server, config) {
|
||||
return faker.system.filePath();
|
||||
}
|
||||
addTokenToClientDaemon() {}
|
||||
searchClientDaemon() {}
|
||||
isClientDaemonRunning() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user