diff --git a/.changes/next-release/bugfix-31222d0f-5ff2-4495-9bf2-e354eabc82c7.json b/.changes/next-release/bugfix-31222d0f-5ff2-4495-9bf2-e354eabc82c7.json new file mode 100644 index 00000000000..de8cc0d43e2 --- /dev/null +++ b/.changes/next-release/bugfix-31222d0f-5ff2-4495-9bf2-e354eabc82c7.json @@ -0,0 +1,4 @@ +{ + "type" : "bugfix", + "description" : "Amazon Q: Fix issue where context menu items are not available after re-opening projects or restarting the IDE" +} \ No newline at end of file diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/QRegionProfileManagerTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/QRegionProfileManagerTest.kt index 628ab8e36c9..53f5d3170f2 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/QRegionProfileManagerTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/QRegionProfileManagerTest.kt @@ -191,20 +191,6 @@ class QRegionProfileManagerTest { @Test fun `validateProfile should cross validate selected profile with latest API response for current project and remove it if its not longer accessible`() { - val client = clientRule.create() - val mockResponse: SdkIterable = SdkIterable { - listOf( - Profile.builder().profileName("foo").arn("foo-arn-v2").build(), - Profile.builder().profileName("bar").arn("bar-arn").build(), - ).toMutableList().iterator() - } - val iterable: ListAvailableProfilesIterable = mock { - on { it.profiles() } doReturn mockResponse - } - client.stub { - onGeneric { listAvailableProfilesPaginator(any>()) } doReturn iterable - } - val activeConn = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance()) ?: fail("connection shouldn't be null") val anotherConn = authRule.createConnection(ManagedSsoProfile(ssoRegion = "us-east-1", startUrl = "anotherUrl", scopes = Q_SCOPES)) @@ -214,6 +200,15 @@ class QRegionProfileManagerTest { this.connectionIdToActiveProfile[activeConn.id] = fooProfile this.connectionIdToActiveProfile[anotherConn.id] = barProfile } + resourceCache.addEntry( + activeConn.getConnectionSettings(), + QProfileResources.LIST_REGION_PROFILES, + listOf( + QRegionProfile("foo", "foo-arn-v2"), + QRegionProfile("bar", "bar-arn"), + ) + ) + sut.loadState(state) assertThat(sut.activeProfile(project)).isEqualTo(fooProfile) @@ -222,6 +217,24 @@ class QRegionProfileManagerTest { assertThat(sut.state.connectionIdToActiveProfile).isEqualTo(mapOf(anotherConn.id to barProfile)) } + @Test + fun `validateProfile does not clear profile if profiles cannot be listed`() { + val activeConn = + ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance()) ?: fail("connection shouldn't be null") + val anotherConn = authRule.createConnection(ManagedSsoProfile(ssoRegion = "us-east-1", startUrl = "anotherUrl", scopes = Q_SCOPES)) + val fooProfile = QRegionProfile("foo", "foo-arn") + val barProfile = QRegionProfile("bar", "bar-arn") + val state = QProfileState().apply { + this.connectionIdToActiveProfile[activeConn.id] = fooProfile + this.connectionIdToActiveProfile[anotherConn.id] = barProfile + } + sut.loadState(state) + assertThat(sut.activeProfile(project)).isEqualTo(fooProfile) + + sut.validateProfile(project) + assertThat(sut.activeProfile(project)).isEqualTo(fooProfile) + } + @Test fun `clientSettings should return the region Q profile specify`() { MockClientManager.useRealImplementations(disposableRule.disposable) diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QRegionProfileManager.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QRegionProfileManager.kt index b3b242c363d..5d052877477 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QRegionProfileManager.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QRegionProfileManager.kt @@ -16,10 +16,10 @@ import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.intellij.util.xmlb.annotations.MapAnnotation import com.intellij.util.xmlb.annotations.Property import software.amazon.awssdk.core.SdkClient +import software.amazon.awssdk.services.codewhispererruntime.model.AccessDeniedException import software.aws.toolkits.core.TokenConnectionSettings import software.aws.toolkits.core.utils.debug import software.aws.toolkits.core.utils.getLogger -import software.aws.toolkits.core.utils.tryOrNull import software.aws.toolkits.core.utils.warn import software.aws.toolkits.jetbrains.core.AwsClientManager import software.aws.toolkits.jetbrains.core.AwsResourceCache @@ -59,15 +59,28 @@ class QRegionProfileManager : PersistentStateComponent, Disposabl ) } - // should be call on project startup to validate if profile is still active + /** + * Called on project startup to validate if selected profile is still active + */ + @Deprecated("This is a giant hack and we are not handling all the cases") @RequiresBackgroundThread fun validateProfile(project: Project) { val conn = getIdcConnectionOrNull(project) val selected = activeProfile(project) ?: return - val profiles = tryOrNull { + val profiles = try { listRegionProfiles(project) + } catch (e: Exception) { + if (e is AccessDeniedException) { + null + } else { + // if we can't list profiles assume it is valid + LOG.warn { "Continuing with $selected since listAvailableProfiles failed" } + return + } } + // succeeded in listing profiles, but none match selected + // profiles should be null if access denied or connection is not IdC if (profiles == null || profiles.none { it.arn == selected.arn }) { invalidateProfile(selected.arn) switchProfile(project, null, intent = QProfileSwitchIntent.Reload) @@ -83,6 +96,7 @@ class QRegionProfileManager : PersistentStateComponent, Disposabl fun listRegionProfiles(project: Project): List? { val connection = getIdcConnectionOrNull(project) ?: return null + return try { val connectionSettings = connection.getConnectionSettings() val mappedProfiles = AwsResourceCache.getInstance().getResourceNow(