Skip to content

Commit 9a39537

Browse files
committed
Merge remote-tracking branch 'upstream/master' into profile-cache
2 parents 0c81733 + 9d7f445 commit 9a39537

File tree

9 files changed

+90
-249
lines changed

9 files changed

+90
-249
lines changed

packages/amazonq/.changes/next-release/Feature-2a696d00-e8c8-44a4-ab2c-2204b4d8e31d.json

Lines changed: 0 additions & 4 deletions
This file was deleted.

packages/amazonq/test/unit/codewhisperer/region/regionProfileManager.test.ts

Lines changed: 2 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ describe('RegionProfileManager', function () {
6565
const mockClient = {
6666
listAvailableProfiles: listProfilesStub,
6767
}
68-
const createClientStub = sinon.stub(sut, '_createQClient').resolves(mockClient)
68+
const createClientStub = sinon.stub(sut, 'createQClient').resolves(mockClient)
6969

7070
const r = await sut.listRegionProfile()
7171

@@ -234,65 +234,13 @@ describe('RegionProfileManager', function () {
234234
})
235235

236236
describe('createQClient', function () {
237-
it(`should configure the endpoint and region from a profile`, async function () {
238-
await setupConnection('idc')
239-
240-
const iadClient = await sut.createQClient({
241-
name: 'foo',
242-
region: 'us-east-1',
243-
arn: 'arn',
244-
description: 'description',
245-
})
246-
247-
assert.deepStrictEqual(iadClient.config.region, 'us-east-1')
248-
assert.deepStrictEqual(iadClient.endpoint.href, 'https://q.us-east-1.amazonaws.com/')
249-
250-
const fraClient = await sut.createQClient({
251-
name: 'bar',
252-
region: 'eu-central-1',
253-
arn: 'arn',
254-
description: 'description',
255-
})
256-
257-
assert.deepStrictEqual(fraClient.config.region, 'eu-central-1')
258-
assert.deepStrictEqual(fraClient.endpoint.href, 'https://q.eu-central-1.amazonaws.com/')
259-
})
260-
261-
it(`should throw if the region is not supported or recognizable by Q`, async function () {
262-
await setupConnection('idc')
263-
264-
await assert.rejects(
265-
async () => {
266-
await sut.createQClient({
267-
name: 'foo',
268-
region: 'ap-east-1',
269-
arn: 'arn',
270-
description: 'description',
271-
})
272-
},
273-
{ message: /trying to initiatize Q client with unrecognizable region/ }
274-
)
275-
276-
await assert.rejects(
277-
async () => {
278-
await sut.createQClient({
279-
name: 'foo',
280-
region: 'unknown-somewhere',
281-
arn: 'arn',
282-
description: 'description',
283-
})
284-
},
285-
{ message: /trying to initiatize Q client with unrecognizable region/ }
286-
)
287-
})
288-
289237
it(`should configure the endpoint and region correspondingly`, async function () {
290238
await setupConnection('idc')
291239
await sut.switchRegionProfile(profileFoo, 'user')
292240
assert.deepStrictEqual(sut.activeRegionProfile, profileFoo)
293241
const conn = authUtil.conn as SsoConnection
294242

295-
const client = await sut._createQClient('eu-central-1', 'https://amazon.com/', conn)
243+
const client = await sut.createQClient('eu-central-1', 'https://amazon.com/', conn)
296244

297245
assert.deepStrictEqual(client.config.region, 'eu-central-1')
298246
assert.deepStrictEqual(client.endpoint.href, 'https://amazon.com/')

packages/core/src/codewhisperer/activation.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,12 @@ import { AuthUtil } from './util/authUtil'
7272
import { ImportAdderProvider } from './service/importAdderProvider'
7373
import { TelemetryHelper } from './util/telemetryHelper'
7474
import { openUrl } from '../shared/utilities/vsCodeUtils'
75-
import { notifyNewCustomizations, onProfileChangedListener } from './util/customizationUtil'
75+
import {
76+
getAvailableCustomizationsList,
77+
getSelectedCustomization,
78+
notifyNewCustomizations,
79+
switchToBaseCustomizationAndNotify,
80+
} from './util/customizationUtil'
7681
import { CodeWhispererCommandBackend, CodeWhispererCommandDeclarations } from './commands/gettingStartedPageCommands'
7782
import { SecurityIssueHoverProvider } from './service/securityIssueHoverProvider'
7883
import { SecurityIssueCodeActionProvider } from './service/securityIssueCodeActionProvider'
@@ -338,7 +343,26 @@ export async function activate(context: ExtContext): Promise<void> {
338343
SecurityIssueCodeActionProvider.instance
339344
),
340345
vscode.commands.registerCommand('aws.amazonq.openEditorAtRange', openEditorAtRange),
341-
auth.regionProfileManager.onDidChangeRegionProfile(onProfileChangedListener)
346+
auth.regionProfileManager.onDidChangeRegionProfile(() => {
347+
// Validate user still has access to the selected customization.
348+
const selectedCustomization = getSelectedCustomization()
349+
// No need to validate base customization which has empty arn.
350+
if (selectedCustomization.arn.length > 0) {
351+
getAvailableCustomizationsList()
352+
.then(async (customizations) => {
353+
const r = customizations.find((it) => it.arn === selectedCustomization.arn)
354+
if (!r) {
355+
await switchToBaseCustomizationAndNotify()
356+
}
357+
})
358+
.catch((e) => {
359+
getLogger().error(
360+
`encounter error while validating selected customization on profile change: %s`,
361+
(e as Error).message
362+
)
363+
})
364+
}
365+
})
342366
)
343367

344368
// run the auth startup code with context for telemetry

packages/core/src/codewhisperer/client/codewhisperer.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,19 @@ import { AWSError, Credentials, Service } from 'aws-sdk'
77
import globals from '../../shared/extensionGlobals'
88
import * as CodeWhispererClient from './codewhispererclient'
99
import * as CodeWhispererUserClient from './codewhispereruserclient'
10-
import { SendTelemetryEventRequest } from './codewhispereruserclient'
10+
import { ListAvailableCustomizationsResponse, SendTelemetryEventRequest } from './codewhispereruserclient'
1111
import { ServiceOptions } from '../../shared/awsClientBuilder'
1212
import { hasVendedIamCredentials } from '../../auth/auth'
1313
import { CodeWhispererSettings } from '../util/codewhispererSettings'
1414
import { PromiseResult } from 'aws-sdk/lib/request'
1515
import { AuthUtil } from '../util/authUtil'
1616
import { isSsoConnection } from '../../auth/connection'
17+
import { pageableToCollection } from '../../shared/utilities/collectionUtils'
1718
import apiConfig = require('./service-2.json')
1819
import userApiConfig = require('./user-service-2.json')
1920
import { session } from '../util/codeWhispererSession'
2021
import { getLogger } from '../../shared/logger/logger'
22+
import { indent } from '../../shared/utilities/textUtilities'
2123
import { getClientId, getOptOutPreference, getOperatingSystem } from '../../shared/telemetry/util'
2224
import { extensionVersion, getServiceEnvVarConfig } from '../../shared/vscode/env'
2325
import { DevSettings } from '../../shared/settings'
@@ -217,6 +219,28 @@ export class DefaultCodeWhispererClient {
217219
.promise()
218220
}
219221

222+
public async listAvailableCustomizations(): Promise<ListAvailableCustomizationsResponse[]> {
223+
const client = await this.createUserSdkClient()
224+
const profile = AuthUtil.instance.regionProfileManager.activeRegionProfile
225+
const requester = async (request: CodeWhispererUserClient.ListAvailableCustomizationsRequest) =>
226+
client.listAvailableCustomizations(request).promise()
227+
return pageableToCollection(requester, { profileArn: profile?.arn }, 'nextToken')
228+
.promise()
229+
.then((resps) => {
230+
let logStr = 'amazonq: listAvailableCustomizations API request:'
231+
for (const resp of resps) {
232+
const requestId = resp.$response.requestId
233+
logStr += `\n${indent('RequestID: ', 4)}${requestId},\n${indent('Customizations:', 4)}`
234+
for (const [index, c] of resp.customizations.entries()) {
235+
const entry = `${index.toString().padStart(2, '0')}: ${c.name?.trim()}`
236+
logStr += `\n${indent(entry, 8)}`
237+
}
238+
}
239+
getLogger().debug(logStr)
240+
return resps
241+
})
242+
}
243+
220244
public async sendTelemetryEvent(request: SendTelemetryEventRequest) {
221245
const requestWithCommonFields: SendTelemetryEventRequest = {
222246
...request,

packages/core/src/codewhisperer/index.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,7 @@ export * as diagnosticsProvider from './service/diagnosticsProvider'
9999
export * from './ui/codeWhispererNodes'
100100
export { SecurityScanError, SecurityScanTimedOutError } from '../codewhisperer/models/errors'
101101
export * as CodeWhispererConstants from '../codewhisperer/models/constants'
102-
export {
103-
getSelectedCustomization,
104-
setSelectedCustomization,
105-
baseCustomization,
106-
onProfileChangedListener,
107-
CustomizationProvider,
108-
} from './util/customizationUtil'
102+
export { getSelectedCustomization, setSelectedCustomization, baseCustomization } from './util/customizationUtil'
109103
export { Container } from './service/serviceContainer'
110104
export * from './util/gitUtil'
111105
export * from './ui/prompters'

packages/core/src/codewhisperer/region/regionProfileManager.ts

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,7 @@ const endpoints = createConstantMap({
4848
* 'update' -> plugin auto select the profile on users' behalf as there is only 1 profile
4949
* 'reload' -> on plugin restart, plugin will try to reload previous selected profile
5050
*/
51-
export type ProfileSwitchIntent = 'user' | 'auth' | 'update' | 'reload' | 'customization'
52-
53-
export type ProfileChangedEvent = {
54-
profile: RegionProfile | undefined
55-
intent: ProfileSwitchIntent
56-
}
51+
export type ProfileSwitchIntent = 'user' | 'auth' | 'update' | 'reload'
5752

5853
// Only "valid" state will have non null profiles
5954
type CachedApiResult = {
@@ -64,7 +59,7 @@ type CachedApiResult = {
6459
export class RegionProfileManager {
6560
private static logger = getLogger()
6661
private _activeRegionProfile: RegionProfile | undefined
67-
private _onDidChangeRegionProfile = new vscode.EventEmitter<ProfileChangedEvent>()
62+
private _onDidChangeRegionProfile = new vscode.EventEmitter<RegionProfile | undefined>()
6863
public readonly onDidChangeRegionProfile = this._onDidChangeRegionProfile.event
6964

7065
// Store the last API results (for UI propuse) so we don't need to call service again if doesn't require "latest" result
@@ -135,7 +130,7 @@ export class RegionProfileManager {
135130
await this.cacheApiResult(undefined)
136131

137132
for (const [region, endpoint] of endpoints.entries()) {
138-
const client = await this._createQClient(region, endpoint, conn as SsoConnection)
133+
const client = await this.createQClient(region, endpoint, conn as SsoConnection)
139134
const requester = async (request: CodeWhispererUserClient.ListAvailableProfilesRequest) =>
140135
client.listAvailableProfiles(request).promise()
141136
const request: CodeWhispererUserClient.ListAvailableProfilesRequest = {}
@@ -187,7 +182,7 @@ export class RegionProfileManager {
187182
const ssoConn = this.connectionProvider() as SsoConnection
188183

189184
// only prompt to users when users switch from A profile to B profile
190-
if (source !== 'customization' && this.activeRegionProfile !== undefined && regionProfile !== undefined) {
185+
if (this.activeRegionProfile !== undefined && regionProfile !== undefined) {
191186
const response = await showConfirmationMessage({
192187
prompt: localize(
193188
'AWS.amazonq.profile.confirmation',
@@ -229,16 +224,13 @@ export class RegionProfileManager {
229224
})
230225
}
231226

232-
await this._switchRegionProfile(regionProfile, source)
227+
await this._switchRegionProfile(regionProfile)
233228
}
234229

235-
private async _switchRegionProfile(regionProfile: RegionProfile | undefined, source: ProfileSwitchIntent) {
230+
private async _switchRegionProfile(regionProfile: RegionProfile | undefined) {
236231
this._activeRegionProfile = regionProfile
237232

238-
this._onDidChangeRegionProfile.fire({
239-
profile: regionProfile,
240-
intent: source,
241-
})
233+
this._onDidChangeRegionProfile.fire(regionProfile)
242234
// dont show if it's a default (fallback)
243235
if (regionProfile && this.profiles.length > 1) {
244236
void vscode.window.showInformationMessage(`You are using the ${regionProfile.name} profile for Q.`).then()
@@ -391,21 +383,7 @@ export class RegionProfileManager {
391383
}
392384
}
393385

394-
// TODO: Should maintain sdk client in a better way
395-
async createQClient(profile: RegionProfile): Promise<CodeWhispererUserClient> {
396-
const conn = this.connectionProvider()
397-
if (conn === undefined || !isSsoConnection(conn)) {
398-
throw new Error('No valid SSO connection')
399-
}
400-
const endpoint = endpoints.get(profile.region)
401-
if (!endpoint) {
402-
throw new Error(`trying to initiatize Q client with unrecognizable region ${profile.region}`)
403-
}
404-
return this._createQClient(profile.region, endpoint, conn)
405-
}
406-
407-
// Visible for testing only, do not use this directly, please use createQClient(profile)
408-
async _createQClient(region: string, endpoint: string, conn: SsoConnection): Promise<CodeWhispererUserClient> {
386+
async createQClient(region: string, endpoint: string, conn: SsoConnection): Promise<CodeWhispererUserClient> {
409387
const token = (await conn.getToken()).accessToken
410388
const serviceOption: ServiceOptions = {
411389
apiConfig: userApiConfig,

0 commit comments

Comments
 (0)