Skip to content

Commit

Permalink
Extract shared classes to a separate SDK (#300)
Browse files Browse the repository at this point in the history
## Task

Resolves: #41 

## Description

This PR is the first step. After manually releasing the SDK (by
_manually_ I mean through a CI job triggered through workflow dispatch),
tagging it, and updating its version to SNAPSHOT again, we'll need
another PR that would remove the built-in classes and replace them with
a dependency (which should be quite easy to do, I already tested it
out). I think there's no point in automating anything at this point,
especially if we want to fix this bug before we release the article (+ I
don't expect these files to change too much, so it shouldn't be too
painful).
  • Loading branch information
jplewa authored Aug 30, 2023
1 parent 791cb18 commit 6d5ec21
Show file tree
Hide file tree
Showing 11 changed files with 1,203 additions and 1 deletion.
27 changes: 27 additions & 0 deletions .github/workflows/publish_sdk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Prepare release of SDK

on:
workflow_dispatch:

jobs:
publish:
name: Publish package to Maven Central
runs-on: ubuntu-latest
steps:
- name: Check out project sources
uses: actions/checkout@v3
- name: Set up Java
uses: actions/setup-java@v3
with:
distribution: adopt
java-version: 11
- name: Publish package
run: |
./gradlew publishPulumiKotlinSdkPublicationToMavenCentralRepository \
-Psigning.enabled=true \
-Psigning.key="${{ secrets.GPG_KEY }}" \
-Psigning.key.password="${{ secrets.GPG_KEY_PASSWORD }}" \
-Psonatype.username="${{ secrets.SONATYPE_USERNAME }}" \
-Psonatype.password="${{ secrets.SONATYPE_PASSWORD }}" \
-Dorg.gradle.daemon=false \
-q
13 changes: 13 additions & 0 deletions .github/workflows/publish_to_maven_local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ on:
workflow_dispatch:

jobs:
publish-sdk:
name: Publish pulumi-kotlin SDK to Maven Local Repository
runs-on: ubuntu-latest
steps:
- name: Check out project sources
uses: actions/checkout@v3
- name: Set up Java
uses: actions/setup-java@v3
with:
distribution: adopt
java-version: 11
- name: Publish to Maven Local
run: ./gradlew sdk:publishPulumiKotlinSdkPublicationToMavenLocal -Dorg.gradle.daemon=false -q
publish:
name: Publish pulumi-${{ matrix.provider }}-kotlin (${{ matrix.majorVersion }}) to Maven Local Repository
runs-on: [ self-hosted, active ]
Expand Down
128 changes: 128 additions & 0 deletions sdk/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import org.jetbrains.dokka.gradle.DokkaTask

plugins {
kotlin("jvm")
`java-library`
`maven-publish`
id("org.jetbrains.dokka")
signing
}

group = "org.virtuslab"
version = "0.9.4.0-SNAPSHOT"
base.archivesName.set("pulumi-kotlin-sdk")

repositories {
mavenCentral()
}

dependencies {
api("com.pulumi:pulumi:0.9.4")
api("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.7.2")
}

tasks.test {
useJUnitPlatform()
}

task<Jar>("sourcesJar") {
group = "build"
from(sourceSets.main.get().allSource)
archiveClassifier.set("sources")
}

tasks.withType<DokkaTask> {
moduleName.set("pulumi-kotlin")
}

task<Jar>("dokkaJavadocJar") {
dependsOn(tasks["dokkaHtml"])
group = "documentation"
from(tasks["dokkaHtml"])
archiveClassifier.set("javadoc")
}

publishing {
repositories {
maven {
name = "MavenCentral"
url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
credentials {
username = findProperty("sonatype.username") as String?
password = findProperty("sonatype.password") as String?
}
}
}
publications {
create<MavenPublication>("pulumiKotlinSdk") {
artifact(tasks.named("sourcesJar"))
artifact(tasks.named("dokkaJavadocJar"))
from(components["java"])
artifactId = "pulumi-kotlin"
}

publications
.forEach {
if (it is MavenPublication) {
configurePom(it)
if ((findProperty("signing.enabled") as String).toBoolean()) {
val signingKey = findProperty("signing.key") as String?
val signingKeyPassword = findProperty("signing.key.password") as String?

signing {
sign(it)
useInMemoryPgpKeys(signingKey, signingKeyPassword)
}
}
}
}
}
}

fun configurePom(mavenPublication: MavenPublication) {
mavenPublication.pom {
name.set("Pulumi Kotlin")
description.set(
"Build cloud applications and infrastructure by combining the safety and reliability of infrastructure " +
"as code with the power of the Kotlin programming language.",
)
url.set("https://github.com/VirtuslabRnD/pulumi-kotlin")
inceptionYear.set("2022")

issueManagement {
system.set("GitHub")
url.set("https://github.com/VirtuslabRnD/pulumi-kotlin/issues")
}

licenses {
license {
name.set("The Apache License, Version 2.0")
url.set("https://www.apache.org/licenses/LICENSE-2.0.txt")
}
}

developers {
developer {
name.set("Dariusz Dzikon")
email.set("[email protected]")
organization.set("VirtusLab")
}
developer {
name.set("Michal Fudala")
email.set("[email protected]")
organization.set("VirtusLab")
}
developer {
name.set("Julia Plewa")
email.set("[email protected]")
organization.set("VirtusLab")
}
}

scm {
url.set("https://github.com/VirtuslabRnD/pulumi-kotlin/tree/v$version")
connection.set("scm:git:git://github.com/VirtuslabRnD/pulumi-kotlin.git")
developerConnection.set("scm:git:ssh://github.com:VirtuslabRnD/pulumi-kotlin.git")
}
}
}
115 changes: 115 additions & 0 deletions sdk/src/main/kotlin/com/pulumi/kotlin/Common.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package com.pulumi.kotlin

import com.pulumi.Context
import com.pulumi.core.Output
import kotlinx.coroutines.runBlocking
import com.pulumi.resources.ComponentResource as JavaComponentResource
import com.pulumi.resources.CustomResource as JavaCustomResource
import com.pulumi.resources.ProviderResource as JavaProviderResource
import com.pulumi.resources.Resource as JavaResource

@DslMarker
annotation class PulumiTagMarker

@Suppress("RedundantSuspendModifier")
suspend inline fun <T> T.applySuspend(block: T.() -> Unit): T {
block()
return this
}

interface ConvertibleToJava<T> {
fun toJava(): T
}

/**
* Parent class for resources within Kotlin SDK - equivalent to [JavaResource].
*
* Each resource within Kotlin SDK should have corresponding [ResourceMapper],
* in order to properly translate Java resources to Kotlin representation.
*
* This class serves only as parent for all resources and should not be instantiated,
* it cannot be sealed, because generated subclasses will be placed in other packages.
*/
@Suppress("UnnecessaryAbstractClass")
abstract class KotlinResource private constructor(internal open val javaResource: JavaResource) {

val pulumiResourceName: String
get() = javaResource.pulumiResourceName()

val pulumiResourceType: String
get() = javaResource.pulumiResourceType()

val urn: Output<String>
get() = javaResource.urn()

val pulumiChildResources: Set<KotlinResource>
get() = javaResource.pulumiChildResources()
.map {
GlobalResourceMapper.tryMap(it)!!
}
.toSet()

protected constructor(
javaResource: JavaResource,
mapper: ResourceMapper<KotlinResource>,
) : this(javaResource) {
GlobalResourceMapper.registerMapper(mapper)
}
}

/**
* Parent class for component resources within Kotlin SDK - equivalent to [JavaComponentResource].
*/
@Suppress("UnnecessaryAbstractClass")
abstract class KotlinComponentResource private constructor(
override val javaResource: JavaComponentResource,
mapper: ResourceMapper<KotlinResource>,
) : KotlinResource(javaResource, mapper)

/**
* Parent class for custom resources within Kotlin SDK - equivalent to [JavaCustomResource].
*/
@Suppress("UnnecessaryAbstractClass")
abstract class KotlinCustomResource internal constructor(
override val javaResource: JavaCustomResource,
mapper: ResourceMapper<KotlinResource>,
) : KotlinResource(javaResource, mapper) {
val id: Output<String>
get() = javaResource.id()
}

/**
* Parent class for provider resources within Kotlin SDK - equivalent to [JavaProviderResource].
*/
@Suppress("UnnecessaryAbstractClass")
abstract class KotlinProviderResource internal constructor(
override val javaResource: JavaProviderResource,
mapper: ResourceMapper<KotlinResource>,
) : KotlinResource(javaResource, mapper)

object Pulumi {

/**
* Run a Pulumi stack callback and wait for result.
* In case of an error terminates the process with [System.exit].
*
* @param block the stack to run in Pulumi runtime
*/
fun run(block: suspend (Context) -> Unit) {
com.pulumi.Pulumi.run {
runBlocking {
block(it)
}
}
}
}

/**
* Append a value wrapped in an [Output] to exported stack outputs.
* <p>
* This method mutates the context internal state.
* @param name name of the [Output]
* @param value the value to be wrapped in [Output]
* @return the current [Context]
*/
fun Context.export(name: String, value: Any): Context = export(name, Output.of(value))
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.pulumi.kotlin

class PulumiNullFieldException(name: String) : RuntimeException(
"Field $name is required but was not set (or was set to null)",
)
78 changes: 78 additions & 0 deletions sdk/src/main/kotlin/com/pulumi/kotlin/ResourceMapping.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.pulumi.kotlin

import com.pulumi.core.Output
import java.util.Optional
import com.pulumi.resources.Resource as JavaResource

/**
* Interface for creation of mappers for particular resource types.
* It assumes that only subtypes of [JavaResource] from Java SDK can be mapped to Kotlin types.
*
* Type [T] is a specific subtype of [KotlinResource] representing provider's resource in Kotlin SDK,
* mapper will produce objects of this type.
*/
interface ResourceMapper<out T : KotlinResource> {
/**
* Returns `true` if given subtype of [JavaResource] matches the type
* of [KotlinResource]'s backing object (can be mapped to type [T]), `false` otherwise.
*/
fun supportsMappingOfType(javaResource: JavaResource): Boolean

/**
* Creates new instance of corresponding [KotlinResource] for given [JavaResource],
* with given [javaResource] as backing object.
*/
fun map(javaResource: JavaResource): T
}

/**
* General mapper for mapping Resources backed by java objects ([JavaResource]).
*
* **In order to work properly, a Kotlin resource should be declared first with use of type-safe builder.
* Only then a corresponding mapper will be registered in application's context.**
*/
internal object GlobalResourceMapper {
private val mappers: MutableList<ResourceMapper<KotlinResource>> = mutableListOf()

/**
* Looks for corresponding [ResourceMapper] to given [javaResource] and maps it to proper [KotlinResource].
* Returns null, if given [javaResource] is null.
*/
internal fun tryMap(javaResource: JavaResource?): KotlinResource? {
if (javaResource == null) return null

val mapper = requireNotNull(mappers.find { it.supportsMappingOfType(javaResource) }) {
"mapper for a type ${javaResource::class.java} was either not declared or not instantiated"
}

return mapper.map(javaResource)
}

/**
* If given [optionalJavaResource] is present, looks for corresponding [ResourceMapper]
* and maps it to proper [KotlinResource]. Otherwise, returns null.
*/
internal fun tryMap(optionalJavaResource: Optional<JavaResource>?): KotlinResource? {
return if (optionalJavaResource?.isPresent == true) tryMap(optionalJavaResource.get()) else null
}

/**
* Looks for corresponding [ResourceMapper] to given [outputJavaResource]
* and transforms it to proper [Output] with [KotlinResource].
* Returned [Output] can be empty if given [outputJavaResource] is empty.
*/
internal fun tryMap(outputJavaResource: Output<JavaResource>?): Output<KotlinResource?>? {
return outputJavaResource?.applyValue { tryMap(it) }
}

/**
* Adds given mapper to set of available mappers.
* Returns `true` if mapper was successfully added, `false` if it already existed within the internal collection.
*/
internal fun registerMapper(mapper: ResourceMapper<KotlinResource>) = mappers.add(mapper)

/**
* Removes every registered mapper from internal collection.
*/
internal fun clearMappers() = mappers.clear()
}
Loading

0 comments on commit 6d5ec21

Please sign in to comment.