Skip to content

Commit 5db1dc2

Browse files
authored
ARN Anywhere (#3021)
1 parent 52dbe48 commit 5db1dc2

File tree

8 files changed

+269
-0
lines changed

8 files changed

+269
-0
lines changed

jetbrains-core/resources/META-INF/plugin.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,12 @@ with what features/services are supported.
263263
<fileEditorProvider implementation="software.aws.toolkits.jetbrains.services.dynamodb.editor.DynamoDbTableEditorProvider"/>
264264
<fileIconProvider order="first" implementation="software.aws.toolkits.jetbrains.services.dynamodb.editor.DynamoDbFileIconProvider"/>
265265

266+
<!-- Open in Console -->
267+
<referenceProviderType key="commentsReferenceProvider"
268+
implementationClass="software.aws.toolkits.jetbrains.services.federation.psireferences.ArnPsiReferenceProvider"/>
269+
<psi.referenceContributor id="arnContributor" implementation="software.aws.toolkits.jetbrains.services.federation.psireferences.ArnPsiReferenceContributor"/>
270+
<documentationProvider implementation="software.aws.toolkits.jetbrains.services.federation.psireferences.ArnReferenceDocumentationProvider"/>
271+
266272
<registryKey key="aws.telemetry.endpoint" description="Endpoint to use for publishing AWS client-side telemetry" defaultValue="https://client-telemetry.us-east-1.amazonaws.com" restartRequired="true"/>
267273
<registryKey key="aws.telemetry.identityPool" description="Cognito identity pool to use for publishing AWS client-side telemetry" defaultValue="us-east-1:820fd6d1-95c0-4ca4-bffb-3f01d32da842" restartRequired="true"/>
268274
<registryKey key="aws.telemetry.region" description="Region to use for publishing AWS client-side telemetry" defaultValue="us-east-1" restartRequired="true"/>

jetbrains-core/src/software/aws/toolkits/jetbrains/services/federation/AwsConsoleUrlFactory.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ class AwsConsoleUrlFactory(
105105
c.execute(
106106
request
107107
).use { resp ->
108+
if (resp.statusLine.statusCode !in 200..399) {
109+
throw RuntimeException("getSigninToken request to AWS Signin endpoint failed: ${resp.statusLine}")
110+
}
108111
resp.entity.content.readAllBytes().decodeToString()
109112
}
110113
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.services.federation.psireferences
5+
6+
import com.intellij.patterns.PsiElementPattern
7+
import com.intellij.psi.ElementManipulators
8+
import com.intellij.psi.PsiElement
9+
import com.intellij.psi.PsiReferenceContributor
10+
import com.intellij.psi.PsiReferenceRegistrar
11+
import com.intellij.util.ProcessingContext
12+
13+
class ArnPsiReferenceContributor : PsiReferenceContributor() {
14+
override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) {
15+
registrar.registerReferenceProvider(
16+
object : PsiElementPattern.Capture<PsiElement>(
17+
PsiElement::class.java
18+
) {
19+
override fun accepts(o: Any?, context: ProcessingContext): Boolean {
20+
if (o == null || o !is PsiElement) return false
21+
val manipulator = ElementManipulators.getManipulator(o)
22+
if (manipulator == null) return false
23+
24+
if (manipulator.getRangeInElement(o).substring(o.text).contains("arn:")) {
25+
return true
26+
}
27+
28+
return false
29+
}
30+
},
31+
ArnPsiReferenceProvider(),
32+
PsiReferenceRegistrar.LOWER_PRIORITY
33+
)
34+
}
35+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.services.federation.psireferences
5+
6+
import com.intellij.openapi.util.TextRange
7+
import com.intellij.psi.PsiElement
8+
import com.intellij.psi.PsiReference
9+
import com.intellij.psi.PsiReferenceProvider
10+
import com.intellij.util.ProcessingContext
11+
12+
class ArnPsiReferenceProvider : PsiReferenceProvider() {
13+
override fun getReferencesByElement(element: PsiElement, context: ProcessingContext): Array<PsiReference> {
14+
val matches = ARN_REGEX.findAll(element.text)
15+
return matches.map {
16+
ArnReference(
17+
element,
18+
TextRange.from(it.range.start, it.value.length),
19+
it.value
20+
)
21+
}.toList().toTypedArray()
22+
}
23+
24+
companion object {
25+
// partition service region account (optional)
26+
// v v v v resource-type resource
27+
val ARN_REGEX = "arn:aws[^/:]*:[^/:]*:[^:]*:[^/:]*:(?:[^:\\s\\/]*[:\\/])?(?:[^\\s'\\\"\\*\\?\\\\]*)".toRegex()
28+
}
29+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.services.federation.psireferences
5+
6+
import com.intellij.ide.BrowserUtil
7+
import com.intellij.openapi.application.ApplicationManager
8+
import com.intellij.openapi.paths.WebReference
9+
import com.intellij.openapi.util.TextRange
10+
import com.intellij.psi.PsiElement
11+
import com.intellij.psi.SyntheticElement
12+
import com.intellij.psi.impl.FakePsiElement
13+
import software.aws.toolkits.core.utils.error
14+
import software.aws.toolkits.core.utils.getLogger
15+
import software.aws.toolkits.jetbrains.core.credentials.getConnectionSettings
16+
import software.aws.toolkits.jetbrains.services.federation.AwsConsoleUrlFactory
17+
import software.aws.toolkits.jetbrains.utils.notifyError
18+
import software.aws.toolkits.jetbrains.utils.notifyNoActiveCredentialsError
19+
import software.aws.toolkits.resources.message
20+
21+
class ArnReference(element: PsiElement, textRange: TextRange, private val arn: String) : WebReference(element, textRange) {
22+
inner class MyFakePsiElement : FakePsiElement(), SyntheticElement {
23+
override fun getName() = arn
24+
override fun getParent() = element
25+
override fun getPresentableText() = arn
26+
27+
override fun navigate(requestFocus: Boolean) {
28+
val project = element.project
29+
val connectionSettings = project.getConnectionSettings()
30+
31+
if (connectionSettings == null) {
32+
notifyNoActiveCredentialsError(project)
33+
return
34+
}
35+
36+
val (credProvider, region) = connectionSettings
37+
ApplicationManager.getApplication().executeOnPooledThread {
38+
try {
39+
BrowserUtil.browse(AwsConsoleUrlFactory().getSigninUrl(credProvider.resolveCredentials(), "/go/view/$arn", region))
40+
} catch (e: Exception) {
41+
val message = message("general.open_in_aws_console.error")
42+
notifyError(content = message, project = project)
43+
getLogger<ArnReference>().error(e) { message }
44+
}
45+
}
46+
}
47+
}
48+
override fun resolve() = MyFakePsiElement()
49+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.services.federation.psireferences
5+
6+
import com.intellij.lang.documentation.AbstractDocumentationProvider
7+
import com.intellij.psi.PsiElement
8+
import software.aws.toolkits.resources.message
9+
10+
class ArnReferenceDocumentationProvider : AbstractDocumentationProvider() {
11+
override fun getQuickNavigateInfo(element: PsiElement?, originalElement: PsiElement?): String? {
12+
if (element is ArnReference.MyFakePsiElement) {
13+
return message("general.open_in_aws_console")
14+
}
15+
16+
return null
17+
}
18+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.services.federation.psireferences
5+
6+
import com.intellij.ide.highlighter.JavaFileType
7+
import com.intellij.lang.java.JavaLanguage
8+
import com.intellij.openapi.application.runReadAction
9+
import com.intellij.patterns.PlatformPatterns
10+
import com.intellij.psi.impl.source.resolve.reference.CommentsReferenceContributor
11+
import com.intellij.psi.impl.source.resolve.reference.ReferenceProvidersRegistry
12+
import com.intellij.psi.javadoc.PsiDocToken
13+
import com.intellij.psi.search.PsiElementProcessor
14+
import com.intellij.psi.util.PsiTreeUtil
15+
import com.intellij.testFramework.runInEdtAndGet
16+
import com.jetbrains.extensions.python.toPsi
17+
import org.assertj.core.api.Assertions.assertThat
18+
import org.junit.Rule
19+
import org.junit.Test
20+
import software.aws.toolkits.jetbrains.utils.rules.CodeInsightTestFixtureRule
21+
22+
class ArnPsiReferenceProviderTest {
23+
@Rule
24+
@JvmField
25+
val projectRule = CodeInsightTestFixtureRule()
26+
27+
@Test
28+
fun `matches valid arns`() {
29+
val valid = listOf(
30+
"arn:aws:lambda::123456789012:function:adsfadfsa",
31+
"arn:aws:lambda:us-west-2:123456789012:function:adsfadfsa",
32+
"arn:aws:s3:::bucket_name",
33+
"arn:aws-cn:s3:::bucket_name",
34+
"arn:aws-us-gov:s3:::bucket_name"
35+
)
36+
37+
valid.forEach {
38+
assertThat(ArnPsiReferenceProvider.ARN_REGEX.findAll(it).toList())
39+
.withFailMessage { "Input should have matched regex with single result, but did not: $it" }
40+
.hasSize(1)
41+
}
42+
}
43+
44+
@Test
45+
fun `doesn't match invalid arns`() {
46+
val invalid = listOf(
47+
"arn:asdfadsfadfsfdsafdas",
48+
"arn:::::function:adsfadfsa"
49+
)
50+
51+
invalid.forEach {
52+
assertThat(ArnPsiReferenceProvider.ARN_REGEX.findAll(it).toList()).withFailMessage { "Input should not have matched regex, but did: $it" }
53+
.isEmpty()
54+
}
55+
}
56+
57+
@Test
58+
fun `matches subset if arn is partially valid`() {
59+
val pairs = listOf(
60+
"arn:arn:aws:lambda::123456789012:function:ad\"sfadfsa" to "arn:aws:lambda::123456789012:function:ad",
61+
"arn:aws:iam::123456789012:user/Development/product_1234/*" to "arn:aws:iam::123456789012:user/Development/product_1234/",
62+
"arn:aws:s3:::my_corporate_bucket/Development/*" to "arn:aws:s3:::my_corporate_bucket/Development/"
63+
)
64+
65+
pairs.forEach { pair ->
66+
val (str, match) = pair
67+
assertThat(ArnPsiReferenceProvider.ARN_REGEX.findAll(str).toList())
68+
.withFailMessage { "Input should have partially matched regex with single result but did not: $str" }
69+
.satisfies {
70+
assertThat(it).hasSize(1)
71+
assertThat(it.first().value).isEqualTo(match)
72+
}
73+
}
74+
}
75+
76+
@Test
77+
fun `attaches annotation to ARN-like PsiElements`() {
78+
// language=TEXT
79+
val expected = "arn:aws:lambda::123456789012:function"
80+
// language=Java
81+
val contents = """
82+
class TestClass {
83+
// an amazing comment with arn: $expected
84+
String multiple = "$expected adsfasdfdaf some filler $expected";
85+
/*
86+
* a C-style $expected comment
87+
*/
88+
String single = "$expected";
89+
/**
90+
* a very good javadoc with $expected and things
91+
*/
92+
String partialMatch = "\"$expected\"";
93+
}
94+
""".trimIndent()
95+
96+
// we don't have access to [JavaReferenceContributor] in our sandbox (it comes from the Java Internationalization plugin),
97+
// so register the PsiDocToken contributor manually to be able to test JavaDoc ARN resolution
98+
ReferenceProvidersRegistry.getInstance().getRegistrar(JavaLanguage.INSTANCE).registerReferenceProvider(
99+
PlatformPatterns.psiElement(
100+
PsiDocToken::class.java
101+
),
102+
CommentsReferenceContributor.COMMENTS_REFERENCE_PROVIDER_TYPE.provider
103+
)
104+
105+
val file = runInEdtAndGet {
106+
projectRule.fixture.configureByText(JavaFileType.INSTANCE, contents)
107+
}.virtualFile
108+
109+
val elements = mutableListOf<ArnReference>()
110+
runReadAction {
111+
PsiTreeUtil.processElements(
112+
file.toPsi(projectRule.project),
113+
PsiElementProcessor { child ->
114+
elements.addAll(child.references.filterIsInstance<ArnReference>())
115+
116+
return@PsiElementProcessor true
117+
}
118+
)
119+
120+
assertThat(elements).hasSize(7).allSatisfy {
121+
assertThat(it.value)
122+
.withFailMessage { "Expected ArnReference with value of '$expected' from PsiElement: $it" }
123+
.isEqualTo(expected)
124+
}
125+
}
126+
}
127+
}

resources/resources/software/aws/toolkits/resources/MessagesBundle.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,8 @@ general.message=Message
649649
general.name.label=Name:
650650
general.notification.action.hide_forever=Don't show again
651651
general.notification.action.hide_once=Dismiss
652+
general.open_in_aws_console=Open in AWS Console
653+
general.open_in_aws_console.error=Failed to open link in browser
652654
general.policy=Policy
653655
general.refresh=Refresh
654656
general.save=Save

0 commit comments

Comments
 (0)