From 1488bdf95e75f26ef58354e89afaf04c5a0bac4b Mon Sep 17 00:00:00 2001 From: Cameron Perera Date: Fri, 12 Jan 2024 16:15:15 -0600 Subject: [PATCH] Icu 12027 let the user know if the dc isnt compatible with the controller and link them to the previous version (#2073) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 🤖 allow use of hashicorp release endpoints in desktop ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12027 * refactor: 💡 use page_size to bypass pagination in query adapter ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12027 * refactor: 💡 add list_token mirage scopes list response ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12027 * feat: 🎸 add logic for unsupported controllers ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12027 * refactor: 💡 add comment to application adapter change ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12027 * refactor: 💡 rename scopes controller method ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12027 * refactor: 💡 remove scopes controller ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12027 * refactor: 💡 use beforeModel and setupController hook for scopes along with converting scopes model back to original state ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12027 * refactor: 💡 remove styling to hide modal dismiss button ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12027 * refactor: 💡 update comment for opening external links ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12027 * refactor: 💡 revert model hook changes for scopes ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12027 * test: 💍 remove target connect test from scopes acceptance tests ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12027 * test: 💍 add tests for unsupported controller logic ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12027 * test: 💍 update modal test to waitFor elements to render ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12027 * refactor: 💡 remove use of set and get from scopes router ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12027 * refactor: 💡 default isPaginationSupported to false ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12027 * test: 💍 remove unsupported controller tests ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12027 * test: 💍 add tests back but skip them ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12027 * test: 💍 fix failing tests ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12027 --- addons/api/addon/adapters/application.js | 7 + addons/api/mirage/config.js | 6 +- addons/core/translations/en-us.yaml | 9 ++ ui/desktop/app/routes/scopes.js | 71 ++++++++++- .../app/routes/scopes/scope/authenticate.js | 1 + ui/desktop/app/styles/app.scss | 6 + ui/desktop/app/templates/scopes.hbs | 49 ++++++- .../scopes/scope/projects/sessions/index.hbs | 2 +- .../src/config/content-security-policy.js | 4 + ui/desktop/electron-app/src/index.js | 9 +- .../tests/acceptance/authentication-test.js | 2 +- .../projects/sessions/index-test.js | 10 +- ui/desktop/tests/acceptance/scopes-test.js | 120 +++++++++++++++--- 13 files changed, 264 insertions(+), 32 deletions(-) diff --git a/addons/api/addon/adapters/application.js b/addons/api/addon/adapters/application.js index 072635881..30561606c 100644 --- a/addons/api/addon/adapters/application.js +++ b/addons/api/addon/adapters/application.js @@ -118,6 +118,13 @@ export default class ApplicationAdapter extends RESTAdapter.extend( let result; let data = []; + // If the query has a page_size, we skip the pagination logic + // and return the initial result. This is being used to determine + // if the controller supports pagination + if (query.page_size) { + result = await super.query(store, schema, query); + return prenormalizeArrayResponse(result); + } // Run this loop as long as the response_type is delta, // which indicates that there are more items in the list do { diff --git a/addons/api/mirage/config.js b/addons/api/mirage/config.js index 9d9720af2..1e57fb989 100644 --- a/addons/api/mirage/config.js +++ b/addons/api/mirage/config.js @@ -73,7 +73,11 @@ function routes() { return scope.scope ? scope.scope.id === scope_id : false; }); } - return resultSet.filter(makeBooleanFilter(filter)); + const filteredResultSet = resultSet.filter(makeBooleanFilter(filter)); + // add a list_token to the response to simulate pagination + // without having list_token, the unsupported controller modal + // will show when running with mirage + return { items: filteredResultSet.models, list_token: '12345' }; }, ); this.post('/scopes', function ({ scopes }) { diff --git a/addons/core/translations/en-us.yaml b/addons/core/translations/en-us.yaml index 63bcf9866..304b5fc01 100644 --- a/addons/core/translations/en-us.yaml +++ b/addons/core/translations/en-us.yaml @@ -93,3 +93,12 @@ onboarding: learn: Learn about Boundary Desktop app cli: Learn about Boundary command-line (CLI) api: Learn about Boundary API +unsupported-controller: + title: Unsupported controller version + description: + message: The controller you have connected to is not supported by this version of the desktop app. You’ll be unable to see and connect to any targets as a result. + install: Download the supported version of the desktop app to fix this issue. + buttons: + download: Download supported version + releases-page: Go to releases page + error: Unable to download supported version of desktop app. You’ll need to download version 1.7.1 from the releases page diff --git a/ui/desktop/app/routes/scopes.js b/ui/desktop/app/routes/scopes.js index 5e41c3579..1e8d011a1 100644 --- a/ui/desktop/app/routes/scopes.js +++ b/ui/desktop/app/routes/scopes.js @@ -11,9 +11,28 @@ export default class ScopesRoute extends Route { // =services @service store; + @service ipc; // =methods + async beforeModel() { + this.isPaginationSupported = false; + const adapter = this.store.adapterFor('application'); + const scopeSchema = this.store.modelFor('scope'); + + try { + const scopesCheck = await adapter.query(this.store, scopeSchema, { + page_size: 1, + recursive: true, + }); + if (scopesCheck.list_token) { + this.isPaginationSupported = true; + } + } catch (e) { + // no op + } + } + /** * Attempt to load all scopes from the API. This is allowed * to fail, since in some cases the user may not have permission to read a @@ -23,9 +42,59 @@ export default class ScopesRoute extends Route { * @param {string} params.scope_id * @return {Promise{[ScopeModel]}} */ - model() { + async model() { // NOTE: In the absence of a `scope_id` query parameter, this endpoint is // expected to default to the global scope, thus returning org scopes. return this.store.query('scope', {}).catch(() => A([])); } + + /** + * Adds `isPaginationSupported`, `downloadLink`, and + * `downloadError` to the controller. + * @param {Controller} controller + */ + async setupController(controller) { + super.setupController(...arguments); + + controller.set('isPaginationSupported', this.isPaginationSupported); + + let downloadLink; + let downloadError = false; + + if (!this.isPaginationSupported) { + const metaDataUrl = + 'https://api.releases.hashicorp.com/v1/releases/boundary-desktop/1.7.1'; + const { isWindows, isMac, isLinux } = await this.ipc.invoke('checkOS'); + + try { + const metaDataResponse = await fetch(metaDataUrl); + const metaData = await metaDataResponse.json(); + + if (isWindows) { + downloadLink = this.extractOsSpecificUrl(metaData, 'windows', '.zip'); + } else if (isMac) { + downloadLink = this.extractOsSpecificUrl(metaData, 'darwin', '.dmg'); + } else if (isLinux) { + downloadLink = this.extractOsSpecificUrl(metaData, 'linux', '.deb'); + } + + if (!downloadLink) throw new Error('No build found'); + } catch (e) { + // this is a catch for any errors that may occur and shows the user + // an error alert directing them to the releases page + downloadError = true; + } + } + + controller.set('downloadLink', downloadLink); + controller.set('downloadError', downloadError); + } + + extractOsSpecificUrl(metaData, os, fileExtension) { + const build = metaData.builds.find( + (build) => build.os === os && build.url.endsWith(fileExtension), + ); + + return build?.url; + } } diff --git a/ui/desktop/app/routes/scopes/scope/authenticate.js b/ui/desktop/app/routes/scopes/scope/authenticate.js index e1524c814..878a07eb1 100644 --- a/ui/desktop/app/routes/scopes/scope/authenticate.js +++ b/ui/desktop/app/routes/scopes/scope/authenticate.js @@ -30,6 +30,7 @@ export default class ScopesScopeAuthenticateRoute extends Route { */ model() { const { id: scope_id } = this.modelFor('scopes.scope'); + return hash({ scope: this.modelFor('scopes.scope'), scopes: this.modelFor('scopes').filter((scope) => scope.isOrg), diff --git a/ui/desktop/app/styles/app.scss b/ui/desktop/app/styles/app.scss index ae411bc9e..aed182887 100644 --- a/ui/desktop/app/styles/app.scss +++ b/ui/desktop/app/styles/app.scss @@ -684,3 +684,9 @@ $chevron-down: url("data:image/svg+xml;utf-8, + + {{#if this.downloadError}} + + {{t 'errors.generic.title'}} + {{t 'unsupported-controller.error'}} + + + {{/if}} + + {{t 'unsupported-controller.description.message'}} + + + {{t 'unsupported-controller.description.install'}} + + + + + + +{{/unless}} \ No newline at end of file diff --git a/ui/desktop/app/templates/scopes/scope/projects/sessions/index.hbs b/ui/desktop/app/templates/scopes/scope/projects/sessions/index.hbs index e65665b26..d7631bb73 100644 --- a/ui/desktop/app/templates/scopes/scope/projects/sessions/index.hbs +++ b/ui/desktop/app/templates/scopes/scope/projects/sessions/index.hbs @@ -149,7 +149,7 @@ @text={{t 'actions.cancel'}} @color='secondary' {{on 'click' (route-action 'cancelSession' B.data)}} - data-test-session-cancel-button + data-test-session-cancel-button={{B.data.id}} /> {{/if}} diff --git a/ui/desktop/electron-app/src/config/content-security-policy.js b/ui/desktop/electron-app/src/config/content-security-policy.js index fc6fcce7a..3bce830b2 100644 --- a/ui/desktop/electron-app/src/config/content-security-policy.js +++ b/ui/desktop/electron-app/src/config/content-security-policy.js @@ -39,6 +39,10 @@ const generateCSPHeader = () => { policy['connect-src'].push(runtimeSettings.clusterUrl); } + // add hashicorp release endpoints to connect-src + policy['connect-src'].push('https://api.releases.hashicorp.com'); + policy['connect-src'].push('https://releases.hashicorp.com'); + return Object.keys(policy) .map((key) => `${key} ${policy[key].join(' ')};`) .join(' '); diff --git a/ui/desktop/electron-app/src/index.js b/ui/desktop/electron-app/src/index.js index dbe5c862e..07370978b 100644 --- a/ui/desktop/electron-app/src/index.js +++ b/ui/desktop/electron-app/src/index.js @@ -125,9 +125,14 @@ const createWindow = (partition, closeWindowCB) => { }); // Opens external links in the host default browser. - // We just allow boundaryproject.io domain to open on external window (for now). + // We allow boundaryproject.io domain to open on external window (for now) + // and releases.hashicorp.com domain to download the desktop app or + // link to the release page for the desktop app. browserWindow.webContents.setWindowOpenHandler(({ url }) => { - if (url.startsWith('https://boundaryproject.io/')) { + if ( + url.startsWith('https://boundaryproject.io/') || + url.startsWith('https://releases.hashicorp.com/boundary-desktop/') + ) { shell.openExternal(url); } diff --git a/ui/desktop/tests/acceptance/authentication-test.js b/ui/desktop/tests/acceptance/authentication-test.js index b2968f602..818c6d197 100644 --- a/ui/desktop/tests/acceptance/authentication-test.js +++ b/ui/desktop/tests/acceptance/authentication-test.js @@ -157,7 +157,7 @@ module('Acceptance | authentication', function (hooks) { test('visiting authenticate route when the scope cannot be loaded is allowed', async function (assert) { assert.expect(1); - this.server.get('/scopes', () => { + this.server.get('/scopes/global', () => { return new Response(404); }); await visit(urls.authenticate.global); diff --git a/ui/desktop/tests/acceptance/projects/sessions/index-test.js b/ui/desktop/tests/acceptance/projects/sessions/index-test.js index 57b842a9c..d3e345c18 100644 --- a/ui/desktop/tests/acceptance/projects/sessions/index-test.js +++ b/ui/desktop/tests/acceptance/projects/sessions/index-test.js @@ -295,7 +295,7 @@ module('Acceptance | projects | sessions | index', function (hooks) { await click(`[href="${urls.sessions}"]`); assert - .dom('tbody tr:first-child [data-test-session-cancel-button]') + .dom(`[data-test-session-cancel-button="${instances.session.id}"]`) .isVisible(); }); @@ -307,19 +307,17 @@ module('Acceptance | projects | sessions | index', function (hooks) { await click(`[href="${urls.sessions}"]`); assert - .dom('tbody tr:first-child [data-test-session-cancel-button]') + .dom(`[data-test-session-cancel-button="${instances.session.id}"]`) .isVisible(); }); test('cannot click cancel button without cancel permissions', async function (assert) { instances.session.update({ authorized_actions: [] }); this.stubClientDaemonSearch('sessions', 'sessions', 'targets'); - await visit(urls.projects); - - await click(`[href="${urls.sessions}"]`); + await visit(urls.sessions); assert - .dom('tbody tr:first-child [data-test-session-cancel-button]') + .dom(`[data-test-session-cancel-button="${instances.session.id}"]`) .isNotVisible(); }); diff --git a/ui/desktop/tests/acceptance/scopes-test.js b/ui/desktop/tests/acceptance/scopes-test.js index 49acf53d9..e10107cda 100644 --- a/ui/desktop/tests/acceptance/scopes-test.js +++ b/ui/desktop/tests/acceptance/scopes-test.js @@ -4,7 +4,14 @@ */ import { module, test } from 'qunit'; -import { visit, currentURL, click, find, findAll } from '@ember/test-helpers'; +import { + visit, + currentURL, + click, + find, + findAll, + waitFor, +} from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; import { Response } from 'miragejs'; @@ -23,6 +30,13 @@ module('Acceptance | scopes', function (hooks) { setupStubs(hooks); const APP_STATE_TITLE = '.hds-application-state__title'; + const META_DATA_RESPONSE = { + builds: [ + { os: 'windows', url: 'windows.fake.download.zip' }, + { os: 'darwin', url: 'darwin.fake.download.dmg' }, + { os: 'linux', url: 'linux.fake.download.deb' }, + ], + }; const instances = { scopes: { @@ -235,27 +249,95 @@ module('Acceptance | scopes', function (hooks) { assert.ok(find(APP_STATE_TITLE).textContent.trim(), 'No Targets Available'); }); - test.skip('connecting to a target', async function (assert) { - assert.expect(3); - this.ipcStub.ipcService.withArgs('cliExists').returns(true); - this.ipcStub.ipcService.withArgs('connect').returns({ - session_id: instances.session.id, - address: 'a_123', - port: 'p_123', - protocol: 'tcp', + test.skip('pagination is not supported - windows build', async function (assert) { + this.ipcStub.withArgs('checkOS').returns({ + isWindows: true, + isMac: false, + isLinux: false, + }); + this.server.get('https://api.releases.hashicorp.com/*', () => { + return new Response(200, {}, META_DATA_RESPONSE); + }); + this.server.get('/scopes', () => { + // no "list_token" field + return new Response(200, {}, { scopes: [] }); }); - const confirmService = this.owner.lookup('service:confirm'); - confirmService.enabled = true; await visit(urls.targets); - await click('[data-test-targets-connect-button]'); - assert.ok(find('.dialog-detail'), 'Success dialog'); - assert.strictEqual(findAll('.rose-dialog-footer button').length, 1); - assert.strictEqual( - find('.rose-dialog-footer button').textContent.trim(), - 'Close', - 'Cannot retry', - ); + await waitFor('[data-test-unsupported-controller]'); + assert.dom('[data-test-unsupported-controller]').exists(); + assert + .dom('[data-test-download-link]') + .hasAttribute('href', 'windows.fake.download.zip'); + }); + + test.skip('pagination is not supported - mac build', async function (assert) { + this.ipcStub.withArgs('checkOS').returns({ + isWindows: false, + isMac: true, + isLinux: false, + }); + this.server.get('/scopes', () => { + // no "list_token" field + return new Response(200, {}, { scopes: [] }); + }); + this.server.get('https://api.releases.hashicorp.com/*', () => { + return new Response(200, {}, META_DATA_RESPONSE); + }); + + await visit(urls.targets); + + await waitFor('[data-test-unsupported-controller]'); + assert.dom('[data-test-unsupported-controller]').exists(); + assert + .dom('[data-test-download-link]') + .hasAttribute('href', 'darwin.fake.download.dmg'); + }); + + test.skip('pagination is not supported - linux build', async function (assert) { + this.ipcStub.withArgs('checkOS').returns({ + isWindows: false, + isMac: false, + isLinux: true, + }); + this.server.get('/scopes', () => { + // no "list_token" field + return new Response(200, {}, { scopes: [] }); + }); + this.server.get('https://api.releases.hashicorp.com/*', () => { + return new Response(200, {}, META_DATA_RESPONSE); + }); + + await visit(urls.targets); + + await waitFor('[data-test-unsupported-controller]'); + assert.dom('[data-test-unsupported-controller]').exists(); + assert + .dom('[data-test-download-link]') + .hasAttribute('href', 'linux.fake.download.deb'); + }); + + test.skip('pagination is not supported - failed to fetch metaData', async function (assert) { + this.ipcStub.withArgs('checkOS').returns({ + isWindows: true, + isMac: false, + isLinux: false, + }); + this.server.get('/scopes', () => { + // no "list_token" field + return new Response(200, {}, { scopes: [] }); + }); + this.server.get('https://api.releases.hashicorp.com/*', () => { + return new Response(500); + }); + + await visit(urls.targets); + + await waitFor('[data-test-unsupported-controller-alert]'); + assert.dom('[data-test-unsupported-controller-alert]').exists(); + assert + .dom('[data-test-releases-link]') + .hasAttribute('href', 'https://releases.hashicorp.com/boundary-desktop/'); }); });