[UI] Ember Data Migration - Sync Cleanup (#30634)

* removes namespace param from activation flags endpoint in api client

* updates sync activation modal to use api service

* updates sync destination sync page to use api service

* removes ember data type deps from sync engine and updates tests

* updates sync activation modal to always override namespace header in activate request
This commit is contained in:
Jordan Reimer 2025-05-15 11:15:18 -06:00 committed by GitHub
parent 6212f0986e
commit ed67e9e59e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 52 additions and 127 deletions

View File

@ -7,7 +7,6 @@ src/apis/SecretsApi.ts
src/apis/SystemApi.ts
src/apis/index.ts
src/index.ts
src/models/ActivationFlagsActivateRequest.ts
src/models/AliCloudConfigureRequest.ts
src/models/AliCloudLoginRequest.ts
src/models/AliCloudWriteAuthRoleRequest.ts

File diff suppressed because one or more lines are too long

View File

@ -82,20 +82,15 @@ class SystemApi extends runtime.BaseAPI {
/**
* Activate a flagged feature.
*/
activationFlagsActivate_2Raw(requestParameters, initOverrides) {
activationFlagsActivate_2Raw(initOverrides) {
return __awaiter(this, void 0, void 0, function* () {
if (requestParameters['activationFlagsActivateRequest'] == null) {
throw new runtime.RequiredError('activationFlagsActivateRequest', 'Required parameter "activationFlagsActivateRequest" was null or undefined when calling activationFlagsActivate_2().');
}
const queryParameters = {};
const headerParameters = {};
headerParameters['Content-Type'] = 'application/json';
const response = yield this.request({
path: `/sys/activation-flags/secrets-sync/activate`,
method: 'POST',
headers: headerParameters,
query: queryParameters,
body: (0, index_1.ActivationFlagsActivateRequestToJSON)(requestParameters['activationFlagsActivateRequest']),
}, initOverrides);
return new runtime.VoidApiResponse(response);
});
@ -103,9 +98,9 @@ class SystemApi extends runtime.BaseAPI {
/**
* Activate a flagged feature.
*/
activationFlagsActivate_2(activationFlagsActivateRequest, initOverrides) {
activationFlagsActivate_2(initOverrides) {
return __awaiter(this, void 0, void 0, function* () {
const response = yield this.activationFlagsActivate_2Raw({ activationFlagsActivateRequest: activationFlagsActivateRequest }, initOverrides);
const response = yield this.activationFlagsActivate_2Raw(initOverrides);
return yield response.value();
});
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,3 @@
export * from './ActivationFlagsActivateRequest';
export * from './AliCloudConfigureRequest';
export * from './AliCloudLoginRequest';
export * from './AliCloudWriteAuthRoleRequest';

View File

@ -1,6 +1,5 @@
/* tslint:disable */
/* eslint-disable */
export * from './ActivationFlagsActivateRequest';
export * from './AliCloudConfigureRequest';
export * from './AliCloudLoginRequest';
export * from './AliCloudWriteAuthRoleRequest';

View File

@ -1,4 +1,3 @@
export * from './ActivationFlagsActivateRequest';
export * from './AliCloudConfigureRequest';
export * from './AliCloudLoginRequest';
export * from './AliCloudWriteAuthRoleRequest';

View File

@ -16,7 +16,6 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
Object.defineProperty(exports, "__esModule", { value: true });
/* tslint:disable */
/* eslint-disable */
__exportStar(require("./ActivationFlagsActivateRequest"), exports);
__exportStar(require("./AliCloudConfigureRequest"), exports);
__exportStar(require("./AliCloudLoginRequest"), exports);
__exportStar(require("./AliCloudWriteAuthRoleRequest"), exports);

View File

@ -15,7 +15,6 @@
import * as runtime from '../runtime';
import type {
ActivationFlagsActivateRequest,
AuditingCalculateHashRequest,
AuditingCalculateHashResponse,
AuditingEnableDeviceRequest,
@ -264,8 +263,6 @@ import type {
WellKnownReadLabelResponse,
} from '../models/index';
import {
ActivationFlagsActivateRequestFromJSON,
ActivationFlagsActivateRequestToJSON,
AuditingCalculateHashRequestFromJSON,
AuditingCalculateHashRequestToJSON,
AuditingCalculateHashResponseFromJSON,
@ -760,10 +757,6 @@ import {
WellKnownReadLabelResponseToJSON,
} from '../models/index';
export interface SystemApiActivationFlagsActivate1Request {
activationFlagsActivateRequest: ActivationFlagsActivateRequest;
}
export interface SystemApiAuditingCalculateHashOperationRequest {
path: string;
auditingCalculateHashRequest: AuditingCalculateHashRequest;
@ -1893,26 +1886,16 @@ export class SystemApi extends runtime.BaseAPI {
/**
* Activate a flagged feature.
*/
async activationFlagsActivate_2Raw(requestParameters: SystemApiActivationFlagsActivate1Request, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<runtime.VoidResponse>> {
if (requestParameters['activationFlagsActivateRequest'] == null) {
throw new runtime.RequiredError(
'activationFlagsActivateRequest',
'Required parameter "activationFlagsActivateRequest" was null or undefined when calling activationFlagsActivate_2().'
);
}
async activationFlagsActivate_2Raw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<runtime.VoidResponse>> {
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
headerParameters['Content-Type'] = 'application/json';
const response = await this.request({
path: `/sys/activation-flags/secrets-sync/activate`,
method: 'POST',
headers: headerParameters,
query: queryParameters,
body: ActivationFlagsActivateRequestToJSON(requestParameters['activationFlagsActivateRequest']),
}, initOverrides);
return new runtime.VoidApiResponse(response);
@ -1921,8 +1904,8 @@ export class SystemApi extends runtime.BaseAPI {
/**
* Activate a flagged feature.
*/
async activationFlagsActivate_2(activationFlagsActivateRequest: ActivationFlagsActivateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.VoidResponse> {
const response = await this.activationFlagsActivate_2Raw({ activationFlagsActivateRequest: activationFlagsActivateRequest }, initOverrides);
async activationFlagsActivate_2(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.VoidResponse> {
const response = await this.activationFlagsActivate_2Raw(initOverrides);
return await response.value();
}

View File

@ -1,6 +1,5 @@
/* tslint:disable */
/* eslint-disable */
export * from './ActivationFlagsActivateRequest';
export * from './AliCloudConfigureRequest';
export * from './AliCloudLoginRequest';
export * from './AliCloudWriteAuthRoleRequest';

View File

@ -9,22 +9,21 @@ import { service } from '@ember/service';
import { action } from '@ember/object';
import { task } from 'ember-concurrency';
import { keyIsFolder } from 'core/utils/key-utils';
import errorMessage from 'vault/utils/error-message';
import type SyncDestinationModel from 'vault/models/sync/destination';
import type { Destination } from 'vault/sync';
import type RouterService from '@ember/routing/router-service';
import type Store from '@ember-data/store';
import type ApiService from 'vault/services/api';
import type PaginationService from 'vault/services/pagination';
import type FlashMessageService from 'vault/services/flash-messages';
import type { SearchSelectOption } from 'vault/vault/app-types';
import type { SearchSelectOption } from 'vault/app-types';
interface Args {
destination: SyncDestinationModel;
destination: Destination;
}
export default class DestinationSyncPageComponent extends Component<Args> {
@service('app-router') declare readonly router: RouterService;
@service declare readonly store: Store;
@service declare readonly api: ApiService;
@service declare readonly flashMessages: FlashMessageService;
@service declare readonly pagination: PaginationService;
@ -47,21 +46,20 @@ export default class DestinationSyncPageComponent extends Component<Args> {
return !this.mountPath || !this.secretPath || this.isSecretDirectory || this.setAssociation.isRunning;
}
willDestroy(): void {
this.pagination.clearDataset('sync/association');
super.willDestroy();
}
// unable to use built-in fetch functionality of SearchSelect since we need to filter by kv type
async fetchMounts() {
const mounts = [];
try {
const secretEngines = await this.store.query('secret-engine', {});
this.mounts = secretEngines.reduce((filtered: SearchSelectOption[], model) => {
if (model.type === 'kv' && model.version === 2) {
filtered.push({ name: model.path, id: model.path });
const { secret } = await this.api.sys.internalUiListEnabledVisibleMounts();
if (secret) {
for (const path in secret) {
const { type, options } = secret[path as keyof typeof secret];
if (type === 'kv' && options?.['version'] === '2') {
mounts.push({ name: path, id: path });
}
}
return filtered;
}, []);
}
this.mounts = mounts;
} catch (error) {
// the user is still able to manually enter the mount path
// InputSearch component will render in this case
@ -83,20 +81,16 @@ export default class DestinationSyncPageComponent extends Component<Args> {
this.error = ''; // reset error
try {
this.syncedSecret = '';
const { name: destinationName, type: destinationType } = this.args.destination;
const { name, type } = this.args.destination;
const mount = keyIsFolder(this.mountPath) ? this.mountPath.slice(0, -1) : this.mountPath; // strip trailing slash from mount path
const association = this.store.createRecord('sync/association', {
destinationName,
destinationType,
mount,
secretName: this.secretPath,
});
await association.save({ adapterOptions: { action: 'set' } });
const payload = { mount, secretName: this.secretPath };
await this.api.sys.systemWriteSyncDestinationsTypeNameAssociationsSet(name, type, payload);
this.syncedSecret = this.secretPath;
// reset the secret path to help make it clear that the sync was successful
this.secretPath = '';
} catch (error) {
this.error = `Sync operation error: \n ${errorMessage(error)}`;
const { message } = await this.api.parseError(error);
this.error = `Sync operation error: \n ${message}`;
}
});
}

View File

@ -8,12 +8,11 @@ import { tracked } from '@glimmer/tracking';
import { service } from '@ember/service';
import { task } from 'ember-concurrency';
import { waitFor } from '@ember/test-waiters';
import errorMessage from 'vault/utils/error-message';
import type FlagsService from 'vault/services/flags';
import type FlashMessageService from 'vault/services/flash-messages';
import type RouterService from '@ember/routing/router-service';
import type Store from '@ember-data/store';
import type ApiService from 'vault/services/api';
interface Args {
onClose: () => void;
@ -25,7 +24,7 @@ export default class SyncActivationModal extends Component<Args> {
@service declare readonly flags: FlagsService;
@service declare readonly flashMessages: FlashMessageService;
@service('app-router') declare readonly router: RouterService;
@service declare readonly store: Store;
@service declare readonly api: ApiService;
@tracked hasConfirmedDocs = false;
@ -36,19 +35,16 @@ export default class SyncActivationModal extends Component<Args> {
this.args.onConfirm();
// sync activation is managed by the root/administrative namespace so child namespaces are not sent.
// for non-managed clusters the root namespace path is technically an empty string so we pass null
// otherwise we pass 'admin' if HVD managed.
const namespace = this.flags.hvdManagedNamespaceRoot;
// for non-managed clusters the root namespace path is technically an empty string, otherwise we pass 'admin' if HVD managed.
const namespace = this.flags.hvdManagedNamespaceRoot || '';
try {
yield this.store
.adapterFor('application')
.ajax('/v1/sys/activation-flags/secrets-sync/activate', 'POST', { namespace });
yield this.api.sys.activationFlagsActivate_2(this.api.buildHeaders({ namespace }));
// must refresh and not transition because transition does not refresh the model from within a namespace
yield this.router.refresh('vault.cluster');
} catch (error) {
this.args.onError(errorMessage(error));
this.flashMessages.danger(`Error enabling feature \n ${errorMessage(error)}`);
const { message } = yield this.api.parseError(error);
this.args.onError(message);
this.flashMessages.danger(`Error enabling feature \n ${message}`);
} finally {
this.args.onClose();
}

View File

@ -4,11 +4,9 @@
*/
import Route from '@ember/routing/route';
import { service } from '@ember/service';
import { findDestination } from 'core/helpers/sync-destinations';
import formResolver from 'vault/forms/sync/resolver';
import type Store from '@ember-data/store';
import type { DestinationType } from 'vault/sync';
type Params = {
@ -16,8 +14,6 @@ type Params = {
};
export default class SyncSecretsDestinationsCreateDestinationRoute extends Route {
@service declare readonly store: Store;
model(params: Params) {
const { type } = params;
const { defaultValues } = findDestination(type);

View File

@ -3,4 +3,4 @@
SPDX-License-Identifier: BUSL-1.1
}}
<Secrets::Page::Destinations::Destination::Sync @destination={{this.model}} />
<Secrets::Page::Destinations::Destination::Sync @destination={{this.model.destination}} />

View File

@ -13,8 +13,6 @@
"ember-cli-typescript": "*",
"ember-auto-import": "*",
"@types/ember": "latest",
"@types/ember-data": "latest",
"@types/ember-data__store": "latest",
"@types/ember__array": "latest",
"@types/ember__component": "latest",
"@types/ember__controller": "latest",

View File

@ -212,7 +212,7 @@ module('Acceptance | sync | overview', function (hooks) {
});
this.server.post('/sys/activation-flags/secrets-sync/activate', (_, req) => {
assert.strictEqual(
req.requestHeaders['X-Vault-Namespace'],
req.requestHeaders['x-vault-namespace'],
'admin',
'Request is made to the admin namespace'
);

View File

@ -9,10 +9,9 @@ import { setupEngine } from 'ember-engines/test-support';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { setupDataStubs } from 'vault/tests/helpers/sync/setup-hooks';
import hbs from 'htmlbars-inline-precompile';
import { render, click, fillIn, settled } from '@ember/test-helpers';
import { render, click, fillIn } from '@ember/test-helpers';
import { PAGE } from 'vault/tests/helpers/sync/sync-selectors';
import { selectChoose } from 'ember-power-select/test-support';
import sinon from 'sinon';
import { Response } from 'miragejs';
const { destinations, searchSelect, messageError, kvSuggestion } = PAGE;
@ -138,25 +137,4 @@ module('Integration | Component | sync | Secrets::Page::Destinations::Destinatio
assert.dom(messageError).hasTextContaining(error, 'Error renders in alert banner');
});
test('it should clear sync associations from store in willDestroy hook', async function (assert) {
const clearDatasetStub = sinon.stub(this.owner.lookup('service:pagination'), 'clearDataset');
this.renderComponent = true;
await render(
hbs`
{{#if this.renderComponent}}
<Secrets::Page::Destinations::Destination::Sync @destination={{this.destination}} />
{{/if}}
`,
{ owner: this.engine }
);
this.set('renderComponent', false);
await settled();
assert.true(
clearDatasetStub.calledWith('sync/association'),
'Sync associations are cleared from store on component teardown'
);
});
});

View File

@ -17,6 +17,7 @@ import { PAGE } from 'vault/tests/helpers/sync/sync-selectors';
import { Response } from 'miragejs';
import { dateFormat } from 'core/helpers/date-format';
import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
import { listDestinationsTransform } from 'sync/utils/api-transforms';
const { title, tab, overviewCard, cta, overview, pagination, emptyStateTitle, emptyStateMessage } = PAGE;
@ -29,13 +30,14 @@ module('Integration | Component | sync | Page::Overview', function (hooks) {
// allow capabilities as root by default to allow users to POST to the secrets-sync/activate endpoint
this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub());
this.version = this.owner.lookup('service:version');
this.store = this.owner.lookup('service:store');
this.api = this.owner.lookup('service:api');
this.flags = this.owner.lookup('service:flags');
syncScenario(this.server);
syncHandlers(this.server);
this.destinations = await this.store.query('sync/destination', {});
const destinations = await this.api.sys.systemListSyncDestinations(true);
this.destinations = listDestinationsTransform(destinations);
this.setup = ({
canActivate = false,

View File

@ -93,7 +93,7 @@ module('Integration | Component | Secrets::SyncActivationModal', function (hooks
this.server.post('/sys/activation-flags/secrets-sync/activate', (_, req) => {
assert.strictEqual(
req.requestHeaders['X-Vault-Namespace'],
req.requestHeaders['x-vault-namespace'],
'admin',
'POST to secrets-sync/activate is called with admin namespace'
);