Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add jenkins plugin downloader #32

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ plugins {
}

group = "xyz.jpenilla"
version = "2.2.1-SNAPSHOT"
version = "2.2.2-SNAPSHOT"
description = "Gradle plugins adding run tasks for Minecraft server and proxy software"

repositories {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import xyz.jpenilla.runtask.pluginsapi.github.GitHubApi
import xyz.jpenilla.runtask.pluginsapi.github.GitHubApiImpl
import xyz.jpenilla.runtask.pluginsapi.hangar.HangarApi
import xyz.jpenilla.runtask.pluginsapi.hangar.HangarApiImpl
import xyz.jpenilla.runtask.pluginsapi.jenkins.JenkinsPluginProvider
import xyz.jpenilla.runtask.pluginsapi.jenkins.JenkinsPluginProviderImpl
import xyz.jpenilla.runtask.pluginsapi.modrinth.ModrinthApi
import xyz.jpenilla.runtask.pluginsapi.modrinth.ModrinthApiImpl
import xyz.jpenilla.runtask.pluginsapi.url.UrlPluginProvider
Expand Down Expand Up @@ -67,6 +69,7 @@ public abstract class DownloadPluginsSpec @Inject constructor(
registry.registerFactory(ModrinthApi::class) { name -> objects.newInstance(ModrinthApiImpl::class, name) }
registry.registerFactory(GitHubApi::class) { name -> objects.newInstance(GitHubApiImpl::class, name) }
registry.registerFactory(UrlPluginProvider::class) { name -> objects.newInstance(UrlPluginProviderImpl::class, name) }
registry.registerFactory(JenkinsPluginProvider::class) { name -> objects.newInstance(JenkinsPluginProviderImpl::class, name) }

register("hangar", HangarApi::class) {
url.set(HangarApi.DEFAULT_URL)
Expand All @@ -76,6 +79,7 @@ public abstract class DownloadPluginsSpec @Inject constructor(
}
register("github", GitHubApi::class)
register("url", UrlPluginProvider::class)
register("jenkins", JenkinsPluginProvider::class)
}

/**
Expand All @@ -96,6 +100,7 @@ public abstract class DownloadPluginsSpec @Inject constructor(
is ModrinthApi -> ModrinthApi::class
is GitHubApi -> GitHubApi::class
is UrlPluginProvider -> UrlPluginProvider::class
is JenkinsPluginProvider -> JenkinsPluginProvider::class
else -> throw IllegalStateException("Unknown PluginApi type: ${api.javaClass.name}")
}
configure(name, type) {
Expand Down Expand Up @@ -181,6 +186,27 @@ public abstract class DownloadPluginsSpec @Inject constructor(
url.configure { add(urlString) }
}

// jenkins extensions

/**
* Access to the built-in [JenkinsPluginProvider].
*/
@get:Internal
public val jenkins: NamedDomainObjectProvider<JenkinsPluginProvider>
get() = named("jenkins", JenkinsPluginProvider::class)

/**
* Add a plugin download.
*
* @param baseUrl The root url to the jenkins instance
* @param job The id of the target job to download the plugin from
* @param artifactRegex In case multiple artifacts are provided, a regex to pick the correct artifact
* @param build A specific build for the [job] or the lastSuccessfulBuild if none provided
*/
public fun jenkins(baseUrl: String, job: String, artifactRegex: Regex? = null, build: String? = null) {
jenkins.configure { add(baseUrl, job, artifactRegex, build) }
}

// All zero-arg methods must be annotated or Gradle will think it's an input
@Internal
override fun getAsMap(): SortedMap<String, PluginApi<*, *>> = registry.asMap
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package xyz.jpenilla.runtask.pluginsapi

import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Optional
import xyz.jpenilla.runtask.util.HashingAlgorithm
import xyz.jpenilla.runtask.util.calculateHash
import xyz.jpenilla.runtask.util.toHexString
Expand Down Expand Up @@ -173,3 +174,46 @@ public abstract class UrlDownload : PluginApiDownload() {
return toHexString(url.get().byteInputStream().calculateHash(HashingAlgorithm.SHA1))
}
}

public abstract class JenkinsDownload : PluginApiDownload() {

@get:Input
public abstract val baseUrl: Property<String>

@get:Input
public abstract val job: Property<String>

@get:Input @get:Optional
public abstract val artifactRegex: Property<Regex>

@get:Input @get:Optional
public abstract val build: Property<String>

override fun toString(): String {
return "JenkinsDownload(baseUrl=$baseUrl, job=$job, artifactRegex=$artifactRegex, build=$build)"
}

override fun equals(other: Any?): Boolean {
if (this === other) {
return true
}
if (javaClass != other?.javaClass) {
return false
}

other as JenkinsDownload

return baseUrl.get() == other.baseUrl.get() &&
job.get() == other.job.get() &&
artifactRegex.orNull == other.artifactRegex.orNull &&
build.orNull == other.build.orNull
}

override fun hashCode(): Int {
var result = baseUrl.hashCode()
result = 31 * result + job.hashCode()
result = 31 * result + artifactRegex.hashCode()
result = 31 * result + build.hashCode()
return result
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ internal abstract class PluginDownloadServiceImpl : PluginDownloadService {
is ModrinthApiDownload -> resolveModrinthPlugin(project, download)
is GitHubApiDownload -> resolveGitHubPlugin(project, download)
is UrlDownload -> resolveUrl(project, download)
is JenkinsDownload -> resolveJenkins(project, download)
}
}

Expand All @@ -104,7 +105,54 @@ internal abstract class PluginDownloadServiceImpl : PluginDownloadService {
val version = manifest.urlProvider[urlHash] ?: PluginVersion(fileName = "$urlHash.jar", displayName = download.url.get())
val targetFile = targetDir.resolve(version.fileName)
val setter: (PluginVersion) -> Unit = { manifest.urlProvider[urlHash] = it }
val ctx = DownloadCtx(project, "url", download.url.get(), targetDir, targetFile, version, setter)
val ctx = DownloadCtx(project, "url", { download.url.get() }, targetDir, targetFile, version, setter)
return download(ctx)
}

private fun resolveJenkins(project: Project, download: JenkinsDownload): Path {
val cacheDir = parameters.cacheDirectory.get().asFile.toPath()
val targetDir = cacheDir.resolve(Constants.JENKINS_PLUGIN_DIR)

val baseUrl = download.baseUrl.get().trimEnd('/')
val job = download.job.get()
val regex = download.artifactRegex.orNull
val jobUrl = "$baseUrl/job/$job"
val build = download.build.getOrElse(
URI("$jobUrl/${Constants.JENKINS_LAST_SUCCESSFUL_BUILD}/buildNumber")
.toURL().readText()
)
val restEndpoint = URI(Constants.JENKINS_REST_ENDPOINT.format(jobUrl, build))

val provider = manifest.jenkinsProvider.computeIfAbsent(baseUrl) { JenkinsProvider() }
val versions = provider.computeIfAbsent(job) { PluginVersions() }
val version = versions[build] ?: PluginVersion(
fileName = "$job-$build.jar",
displayName = "jenkins:$baseUrl/$job/$build"
)

val targetFile = targetDir.resolve(version.fileName)
val setter: (PluginVersion) -> Unit = { versions[build] = it }

val downloadUrlSupplier: () -> String = supplier@{
val artifacts = mapper.readValue<JenkinsBuildResponse>(restEndpoint.toURL()).artifacts
if (artifacts.isEmpty()) {
throw IllegalStateException("No artifacts provided for build $build at $jobUrl")
}
if (artifacts.size == 1) {
val path = artifacts.first().relativePath
if (regex != null && !(regex.containsMatchIn(path))) {
throw NullPointerException("Regex does not match only-found artifact: $path")
}
return@supplier "$jobUrl/$build/artifact/$path"
}
if (regex == null) {
throw NullPointerException("Regex is null but multiple artifacts were found for $jobUrl/$build")
}
val artifactPaths = artifacts.map { it.relativePath }
val artifact = artifactPaths.firstOrNull { regex.containsMatchIn(it) } ?: throw NullPointerException("Failed to find artifact for regex ($regex) - Artifacts are: ${artifactPaths.joinToString(", ")}")
"$jobUrl/$build/artifact/$artifact"
}
val ctx = DownloadCtx(project, jobUrl, downloadUrlSupplier, targetDir, targetFile, version, setter)
return download(ctx)
}

Expand All @@ -130,7 +178,7 @@ internal abstract class PluginDownloadServiceImpl : PluginDownloadService {
val downloadUrl = "$apiUrl/api/v1/projects/$apiPlugin/versions/$apiVersion/$platformType/download"

val setter: (PluginVersion) -> Unit = { platform[apiVersion] = it }
val ctx = DownloadCtx(project, apiUrl, downloadUrl, targetDir, targetFile, version, setter)
val ctx = DownloadCtx(project, apiUrl, { downloadUrl }, targetDir, targetFile, version, setter)
return download(ctx)
}

Expand All @@ -152,7 +200,7 @@ internal abstract class PluginDownloadServiceImpl : PluginDownloadService {

val versionRequestUrl = "$apiUrl/v2/project/$apiPlugin/version/$apiVersion"
val versionJsonPath = download(
DownloadCtx(project, apiUrl, versionRequestUrl, targetDir, jsonFile, jsonVersion, setter = { plugin[jsonVersionName] = it })
DownloadCtx(project, apiUrl, { versionRequestUrl }, targetDir, jsonFile, jsonVersion, setter = { plugin[jsonVersionName] = it })
)
val versionInfo = mapper.readValue<ModrinthVersionResponse>(versionJsonPath.toFile())
val primaryFile = versionInfo.files.find { it.primary } ?: error("Could not find primary file for $download in $versionInfo")
Expand All @@ -165,7 +213,7 @@ internal abstract class PluginDownloadServiceImpl : PluginDownloadService {
val targetFile = targetDir.resolve(version.fileName)

return download(
DownloadCtx(project, apiUrl, primaryFile.url, targetDir, targetFile, version, setter = { plugin[apiVersion] = it })
DownloadCtx(project, apiUrl, { primaryFile.url }, targetDir, targetFile, version, setter = { plugin[apiVersion] = it })
)
}

Expand All @@ -188,7 +236,7 @@ internal abstract class PluginDownloadServiceImpl : PluginDownloadService {
val downloadUrl = "https://github.com/$owner/$repo/releases/download/$tag/$asset"

val setter: (PluginVersion) -> Unit = { tagProvider[asset] = it }
val ctx = DownloadCtx(project, "github.com", downloadUrl, targetDir, targetFile, version, setter)
val ctx = DownloadCtx(project, "github.com", { downloadUrl }, targetDir, targetFile, version, setter)
return download(ctx)
}

Expand Down Expand Up @@ -217,7 +265,7 @@ internal abstract class PluginDownloadServiceImpl : PluginDownloadService {
}

private fun downloadFile(ctx: DownloadCtx): Path {
val url = URI.create(ctx.downloadUrl).toURL()
val url = URI.create(ctx.downloadUrl()).toURL()
val connection = url.openConnection() as HttpURLConnection

try {
Expand Down Expand Up @@ -277,7 +325,7 @@ internal abstract class PluginDownloadServiceImpl : PluginDownloadService {
private data class DownloadCtx(
val project: Project,
val baseUrl: String,
val downloadUrl: String,
val downloadUrl: () -> String,
val targetDir: Path,
val targetFile: Path,
val version: PluginVersion,
Expand All @@ -290,7 +338,8 @@ private data class PluginsManifest(
val hangarProviders: MutableMap<String, HangarProvider> = HashMap(),
val modrinthProviders: MutableMap<String, ModrinthProvider> = HashMap(),
val githubProvider: GitHubProvider = GitHubProvider(),
val urlProvider: PluginVersions = PluginVersions()
val urlProvider: PluginVersions = PluginVersions(),
val jenkinsProvider: MutableMap<String, JenkinsProvider> = HashMap()
)

// hangar types:
Expand Down Expand Up @@ -330,6 +379,20 @@ private typealias GitHubRepo = MutableMap<String, PluginVersions>

private fun GitHubRepo(): GitHubRepo = HashMap()

// jenkins types:
private typealias JenkinsProvider = MutableMap<String, PluginVersions>

private fun JenkinsProvider(): JenkinsProvider = HashMap()

@JsonIgnoreProperties(ignoreUnknown = true)
private data class JenkinsBuildResponse(
val artifacts: List<Artifact>
) {
data class Artifact(
val relativePath: String
)
}

// general types:
private typealias PluginVersions = MutableMap<String, PluginVersion>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Run Task Gradle Plugins
* Copyright (c) 2023 Jason Penilla
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.jpenilla.runtask.pluginsapi.jenkins

import xyz.jpenilla.runtask.pluginsapi.JenkinsDownload
import xyz.jpenilla.runtask.pluginsapi.PluginApi

/**
* [PluginApi] implementation for downloading plugins from Jenkins CI.
*/
public interface JenkinsPluginProvider : PluginApi<JenkinsPluginProvider, JenkinsDownload> {

/**
* Add a plugin download.
*
* @param baseUrl The root url to the jenkins instance
* @param job The id of the target job to download the plugin from
* @param artifactRegex In case multiple artifacts are provided, a regex to pick the correct artifact
* @param build A specific build for the [job] or the lastSuccessfulBuild if none provided
*/
public fun add(baseUrl: String, job: String, artifactRegex: Regex? = null, build: String? = null)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Run Task Gradle Plugins
* Copyright (c) 2023 Jason Penilla
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.jpenilla.runtask.pluginsapi.jenkins

import org.gradle.api.model.ObjectFactory
import org.gradle.kotlin.dsl.newInstance
import xyz.jpenilla.runtask.pluginsapi.JenkinsDownload
import javax.inject.Inject

public abstract class JenkinsPluginProviderImpl @Inject constructor(private val name: String, private val objects: ObjectFactory) : JenkinsPluginProvider {

private val jobs: MutableList<JenkinsDownload> = mutableListOf()

override fun getName(): String = name

override fun add(baseUrl: String, job: String, artifactRegex: Regex?, build: String?) {
val download = objects.newInstance(JenkinsDownload::class)
download.baseUrl.set(baseUrl)
download.job.set(job)
if (artifactRegex != null) {
download.artifactRegex.set(artifactRegex)
}
if (build != null) {
download.build.set(build)
}
jobs += download
}

override fun copyConfiguration(api: JenkinsPluginProvider) {
jobs.addAll(api.downloads)
}

override val downloads: Iterable<JenkinsDownload>
get() = jobs
}
4 changes: 4 additions & 0 deletions plugin/src/main/kotlin/xyz/jpenilla/runtask/util/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ internal object Constants {
const val MODRINTH_PLUGIN_DIR = "modrinth"
const val GITHUB_PLUGIN_DIR = "github"
const val URL_PLUGIN_DIR = "url"
const val JENKINS_PLUGIN_DIR = "jenkins"

const val JENKINS_LAST_SUCCESSFUL_BUILD = "lastSuccessfulBuild"
const val JENKINS_REST_ENDPOINT = "%s/%s/api/json?tree=artifacts[relativePath]"

object Plugins {
const val SHADOW_PLUGIN_ID = "com.github.johnrengelman.shadow"
Expand Down
1 change: 1 addition & 0 deletions tester/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ val paperPlugins = runPaper.downloadPluginsSpec {
github("jpenilla", "MiniMOTD", "v2.0.13", "minimotd-bukkit-2.0.13.jar")
hangar("squaremap", "1.2.0")
url("https://download.luckperms.net/1515/bukkit/loader/LuckPerms-Bukkit-5.4.102.jar")
jenkins("https://ci.athion.net", "FastAsyncWorldEdit", Regex("Bukkit"))
}

tasks {
Expand Down