Skip to content

Commit

Permalink
Fixes #387 by allowing more permissive parsing of URNs coming from en…
Browse files Browse the repository at this point in the history
…gine and adhering to 'string' spec in regard to it being any string without double colons
  • Loading branch information
lbialy committed Feb 12, 2024
1 parent 45c745d commit ffbf1f3
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 19 deletions.
41 changes: 22 additions & 19 deletions core/src/main/scala/besom/types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -171,46 +171,49 @@ object types:
* ```
* urn = "urn:pulumi:" stack "::" project "::" qualified type name "::" name ;
*
* stack = string ;
* project = string ;
* name = string ;
* string = (* any sequence of unicode code points that does not contain "::" *) ;
* stack = string ;
* project = string ;
* name = anystring ; // lol: https://github.com/pulumi/pulumi/commit/516979770f11d3a426239429731cf9f327c38836
* string = (* any sequence of unicode code points that does not contain "::" *) ;
* anystring = (* any sequence of unicode code points *) ;
*
* qualified type name = [ parent type "$" ] type ;
* parent type = type ;
*
* type = package ":" [ module ":" ] type name ;
* package = identifier ;
* module = identifier ; // this actually lies a bit because it has to allow "."
* module = identifier ;
* type name = identifier ;
* identifier = unicode letter { unicode letter | unicode digit | "_" } ; // this actually lies a bit because it has to allow "/"
* identifier = unicode letter { unicode letter | unicode digit | "_" } ; // this actually lies a bit because it has to allow "/", "." and "-"
* ```
*
* So let's start with the easy part, the first part of the regex is just a constant string: `urn:pulumi:`. Then we have these
* segments:
* ```
* 1. Identifier Regex: \p{L}[\p{L}\p{N}_/]*
* 1. Identifier Regex: \p{L}[-\p{L}\p{N}_/]*
* 2. Type Components Regex: The package, module, and type name follow the identifier pattern, separated by :.
* This can be represented as (\p{L}[\p{L}\p{N}_/]*) for each component, with optional components for module.
* This can be represented as (\p{L}[-\p{L}\p{N}_/]*) for each component, with optional components for module.
* 3. Qualified Type Name Regex: This includes any number of parent types (each following the type pattern) separated by $,
* and then the final resource type. The parent types are optional and non-greedy to ensure they don't consume the final
* resource type.
* 4. Stack, Project, and Name Regex: These are strings that do not contain ::. This can be represented as ([^:]+|[^:]*::[^:]*).
* 4. Stack, Project: These are strings that do not contain :: and are not empty. This can be represented as ((?:(?!::).)+).
* 5. Name: This is any string, including empty strings in permissive validation. This can be represented as ((?:(?!::).)*)?.
* ```
* The final regex is then an amalgamation of these components, with the parent type and resource type separated by :: and named
* capture groups for each component.
*/

private inline val urnRegex =
"""urn:pulumi:(?<stack>[^:]+|[^:]*::[^:]*)::(?<project>[^:]+|[^:]*::[^:]*)::(?<parentType>(?:(\p{L}[\p{L}\p{N}_/]*)(?::(\p{L}[\p{L}\p{N}_/\\.]*))?:(\p{L}[\p{L}\p{N}_/]*)(?:\$))*)(?<resourceType>(\p{L}[\p{L}\p{N}_/]*)(?::(\p{L}[\p{L}\p{N}_/\\.]*))?:(\p{L}[\p{L}\p{N}_/]*))::(?<resourceName>[^:]+|[^:]*::[^:]*)"""
private inline val urnRegexStrict =
"""urn:pulumi:(?<stack>(?:(?!::).)+)::(?<project>(?:(?!::).)+)::(?<parentType>(?:(\p{L}[-\p{L}\p{N}_/]*)(?::(\p{L}[-\p{L}\p{N}_/\\.]*))?:(\p{L}[-\p{L}\p{N}_/]*)(?:\$))*)(?<resourceType>(\p{L}[-\p{L}\p{N}_/]*)(?::(\p{L}[-\p{L}\p{N}_/\\.]*))?:(\p{L}[-\p{L}\p{N}_/]*))::(?<resourceName>(?:(?!::).)+)"""

private[types] val UrnRegex = urnRegex.r
private val UrnRegexPermissive =
"""urn:pulumi:(?<stack>(?:(?!::).)+)::(?<project>(?:(?!::).)+)::(?<parentType>(?:(\p{L}[-\p{L}\p{N}_/]*)(?::(\p{L}[-\p{L}\p{N}_/\\.]*))?:(\p{L}[-\p{L}\p{N}_/]*)(?:\$))*)(?<resourceType>(\p{L}[-\p{L}\p{N}_/]*)(?::(\p{L}[-\p{L}\p{N}_/\\.]*))?:(\p{L}[-\p{L}\p{N}_/]*))::(?<resourceName>(?s:.*))""".r

inline def apply(s: String): URN =
requireConst(s)
inline if !constValue[Matches[
s.type,
urnRegex.type
urnRegexStrict.type
]]
then
error(
Expand All @@ -220,7 +223,7 @@ object types:

// TODO this should be only usable in Decoder and RawResourceResult.fromResponse
private[besom] def from(s: String): Try[URN] = Try {
if UrnRegex.matches(s) then s
if UrnRegexPermissive.matches(s) then s
else throw IllegalArgumentException(s"URN $s is not valid")
}

Expand All @@ -236,17 +239,17 @@ object types:
/** @return
* the Pulumi stack name
*/
def stack: String = URN.UrnRegex.findFirstMatchIn(urn).get.group("stack")
def stack: String = URN.UrnRegexPermissive.findFirstMatchIn(urn).get.group("stack")

/** @return
* the Pulumi project name
*/
def project: String = URN.UrnRegex.findFirstMatchIn(urn).get.group("project")
def project: String = URN.UrnRegexPermissive.findFirstMatchIn(urn).get.group("project")

/** @return
* the type of the parent [[besom.internal.Resource]]
*/
def parentType: Vector[ResourceType] = URN.UrnRegex
def parentType: Vector[ResourceType] = URN.UrnRegexPermissive
.findFirstMatchIn(urn)
.fold(Vector.empty) { m =>
m.group("parentType") match
Expand All @@ -258,12 +261,12 @@ object types:
* the type of this [[besom.internal.Resource]]
*/
def resourceType: ResourceType =
ResourceType.unsafeOf(URN.UrnRegex.findFirstMatchIn(urn).get.group("resourceType"))
ResourceType.unsafeOf(URN.UrnRegexPermissive.findFirstMatchIn(urn).get.group("resourceType"))

/** @return
* the logical name of this [[besom.internal.Resource]]
*/
def resourceName: String = URN.UrnRegex.findFirstMatchIn(urn).get.group("resourceName")
def resourceName: String = URN.UrnRegexPermissive.findFirstMatchIn(urn).get.group("resourceName")
end URN

// TODO This should not be a subtype of string, user's access to underlying string has no meaning
Expand Down
49 changes: 49 additions & 0 deletions core/src/test/scala/besom/util/URNTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import besom.types.*

class URNTest extends munit.FunSuite with CompileAssertions:

// case class Sample(urn: String, )

val exampleResourceString =
"urn:pulumi:stack::project::custom:resources:Resource$besom:testing/test:Resource::my-test-resource"

Expand Down Expand Up @@ -152,4 +154,51 @@ class URNTest extends munit.FunSuite with CompileAssertions:
)
}

test("URN should disallow dumb stuff at compile time") {
// empty resource name, nope nope nope
failsToCompile("""
import besom.types.URN
URN("urn:pulumi:stack::project::resource:type::")
""")

// this is tripped by newline in resource name
failsToCompile("""
import besom.types.URN
URN("urn:pulumi:stack::project::resource:type::some really ::^&\n*():: crazy name")
""")
}

List(
"urn:pulumi:test::test::pulumi:pulumi:Stack::test-test",
"urn:pulumi:stack-name::project-name::my:customtype$aws:s3/bucket:Bucket::bob",

// these 3 are pure garbage, the resource type is non-compliant in all 3
// they are duplicated with fixed resource types below
// "urn:pulumi:stack::project::type::",
// "urn:pulumi:stack::project::type::some really ::^&\n*():: crazy name",
// "urn:pulumi:stack::project with whitespace::type::some name",

"urn:pulumi:stack::project::resource:type::",
"urn:pulumi:stack::project::resource:type::some really ::^&\n*():: crazy name",
"urn:pulumi:stack::project with whitespace::resource:type::some name",
"urn:pulumi:test::test::pkgA:index:t1-new$pkgA:index:t2::n1-new-sub", // had to add hyphen to Identifier to make it work
"urn:pulumi:dev::iac-workshop::pulumi:pulumi:Stack::iac-workshop-dev",
"urn:pulumi:dev::iac-workshop::apigateway:index:RestAPI::helloWorldApi",
"urn:pulumi:dev::workshop::apigateway:index:RestAPI$aws:apigateway/restApi:RestApi::helloWorldApi",
"urn:pulumi:dev::workshop::apigateway:index:RestAPI$aws:lambda/permission:Permission::helloWorldApi-fa520765",
"urn:pulumi:stage::demo::eks:index:Cluster$pulumi:providers:kubernetes::eks-provider",
"urn:pulumi:defStack::defProject::kubernetes:storage.k8s.io/v1beta1:CSIDriver::defName",
"urn:pulumi:stack::project::my:my$aws:sns/topicSubscription:TopicSubscription::TopicSubscription",
"urn:pulumi:foo::countdown::aws:cloudwatch/logSubscriptionFilter:LogSubscriptionFilter::countDown_watcher",
"urn:pulumi:stack::project::pulumi:providers:aws::default_4_13_0",
"urn:pulumi:foo::todo::aws:s3/bucketObject:BucketObject::todo4c238266/index.html",
"urn:pulumi:dev::awsx-pulumi-issue::awsx:ec2:Vpc$aws:ec2/vpc:Vpc$aws:ec2/subnet:Subnet$aws:ec2/routeTable:RouteTable$aws:ec2/routeTableAssociation:RouteTableAssociation::example-private-vpc-public-1",
"urn:pulumi:dev::eks::pulumi:providers:aws::default_4_36_0"
).foreach { u =>
test(s"runtime URN should parse $u") {
val maybeParsed = URN.from(u)
assert(maybeParsed.isSuccess)
}
}

end URNTest

0 comments on commit ffbf1f3

Please sign in to comment.