From aaa9660d1c6d2bf701d19948c22b3d1c2be0fe83 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Mon, 23 Oct 2023 08:30:32 -0500 Subject: [PATCH] feat: add a Gradle task for downloading executable for Kubernetes Kind (#425) Signed-off-by: Jeromy Cannon --- fullstack-examples/build.gradle.kts | 5 + fullstack-gradle-plugin/build.gradle.kts | 8 + .../plugin/kind/release/Architecture.kt | 10 ++ .../plugin/kind/release/ArtifactTuple.kt | 44 +++++ .../plugin/kind/release/KindArtifactTask.kt | 157 ++++++++++++++++++ .../plugin/kind/release/OperatingSystem.kt | 7 + .../kind/release/KindArtifactTaskTest.java | 100 +++++++++++ 7 files changed, 331 insertions(+) create mode 100644 fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/kind/release/Architecture.kt create mode 100644 fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/kind/release/ArtifactTuple.kt create mode 100644 fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/kind/release/KindArtifactTask.kt create mode 100644 fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/kind/release/OperatingSystem.kt create mode 100644 fullstack-gradle-plugin/src/test/java/com/hedera/fullstack/gradle/plugin/kind/release/KindArtifactTaskTest.java diff --git a/fullstack-examples/build.gradle.kts b/fullstack-examples/build.gradle.kts index b408ee86d..ba561f485 100644 --- a/fullstack-examples/build.gradle.kts +++ b/fullstack-examples/build.gradle.kts @@ -19,6 +19,7 @@ import com.hedera.fullstack.gradle.plugin.HelmInstallChartTask import com.hedera.fullstack.gradle.plugin.HelmReleaseExistsTask import com.hedera.fullstack.gradle.plugin.HelmTestChartTask import com.hedera.fullstack.gradle.plugin.HelmUninstallChartTask +import com.hedera.fullstack.gradle.plugin.kind.release.KindArtifactTask plugins { id("com.hedera.fullstack.root") @@ -72,6 +73,10 @@ tasks.register("helmUninstallNotAChart") { ifExists.set(true) } +val kindVersion = "0.20.0" + +tasks.register("kindArtifact") { version.set(kindVersion) } + tasks.check { dependsOn("helmInstallNginxChart") dependsOn("helmNginxExists") diff --git a/fullstack-gradle-plugin/build.gradle.kts b/fullstack-gradle-plugin/build.gradle.kts index 408ab5c29..476a77a6e 100644 --- a/fullstack-gradle-plugin/build.gradle.kts +++ b/fullstack-gradle-plugin/build.gradle.kts @@ -15,16 +15,20 @@ */ plugins { + `kotlin-dsl` id("java-gradle-plugin") id("com.gradle.plugin-publish") version "1.2.1" id("com.hedera.fullstack.root") id("com.hedera.fullstack.conventions") id("com.hedera.fullstack.maven-publish") + kotlin("jvm") version "1.9.10" } dependencies { api(platform("com.hedera.fullstack:fullstack-bom")) implementation("com.hedera.fullstack:fullstack-helm-client") + implementation(kotlin("stdlib-jdk8")) + implementation("net.swiftzer.semver:semver:1.1.2") testImplementation("org.assertj:assertj-core:3.24.2") } @@ -40,3 +44,7 @@ gradlePlugin { } } } + +repositories { mavenCentral() } + +kotlin { jvmToolchain(17) } diff --git a/fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/kind/release/Architecture.kt b/fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/kind/release/Architecture.kt new file mode 100644 index 000000000..90fc73896 --- /dev/null +++ b/fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/kind/release/Architecture.kt @@ -0,0 +1,10 @@ +package com.hedera.fullstack.gradle.plugin.kind.release + +enum class Architecture(val descriptor: String) { + AMD64("amd64"), + ARM64("arm64"), + ARM("arm"), + PPC64LE("ppc64le"), + S390X("s390x"), + RISCV64("riscv64"); +} diff --git a/fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/kind/release/ArtifactTuple.kt b/fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/kind/release/ArtifactTuple.kt new file mode 100644 index 000000000..940baccb7 --- /dev/null +++ b/fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/kind/release/ArtifactTuple.kt @@ -0,0 +1,44 @@ +package com.hedera.fullstack.gradle.plugin.kind.release + +import java.io.Serializable + +class ArtifactTuple( + val operatingSystem: OperatingSystem, + val architecture: Architecture, +) : Serializable { + companion object { + @JvmStatic + fun standardTuples(): List { + return listOf( + ArtifactTuple(OperatingSystem.DARWIN, Architecture.AMD64), + ArtifactTuple(OperatingSystem.DARWIN, Architecture.ARM64), + ArtifactTuple(OperatingSystem.WINDOWS, Architecture.AMD64), + ArtifactTuple(OperatingSystem.LINUX, Architecture.AMD64), + ArtifactTuple(OperatingSystem.LINUX, Architecture.ARM64), + ) + } + + @JvmStatic + fun of(operatingSystem: OperatingSystem, architecture: Architecture): ArtifactTuple { + return ArtifactTuple(operatingSystem, architecture) + } + } + + override fun hashCode(): Int { + var result = operatingSystem.hashCode() + result = 31 * result + architecture.hashCode() + return result + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ArtifactTuple) return false + + if (operatingSystem != other.operatingSystem) return false + return architecture == other.architecture + } + + override fun toString(): String { + return "${operatingSystem.descriptor}-${architecture.descriptor}" + } +} diff --git a/fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/kind/release/KindArtifactTask.kt b/fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/kind/release/KindArtifactTask.kt new file mode 100644 index 000000000..d63564818 --- /dev/null +++ b/fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/kind/release/KindArtifactTask.kt @@ -0,0 +1,157 @@ +package com.hedera.fullstack.gradle.plugin.kind.release + +import net.swiftzer.semver.SemVer +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.logging.LogLevel +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.get +import java.net.URL +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.createDirectories +import kotlin.io.path.exists + +@CacheableTask +abstract class KindArtifactTask() : DefaultTask() { + @get:Input + val version: Property = project.objects.property(String::class.java).convention("0.20.0") + + @get:Input + val tuples: ListProperty = project.objects.listProperty(ArtifactTuple::class.java).convention( + ArtifactTuple.standardTuples() + ) + + @get:OutputDirectory + val output: DirectoryProperty = project.objects.directoryProperty() + + private var actualVersion: SemVer? = null + private var actualTuples: List? = null + private var workingDirectory: Path? = null + + companion object { + const val KIND_RELEASE_URL_TEMPLATE = "https://kind.sigs.k8s.io/dl/v%s/kind-%s-%s" + const val KIND_EXECUTABLE_PREFIX = "kind" + const val KIND_VERSION_FILE = "KIND_VERSION" + } + + init { + group = "kind" + description = "Downloads the kind executable for the supplied operating systems and architectures" + project.configure { + output.set(sourceSets["main"].resources.srcDirs.first().toPath().resolve("software").toFile()) + } + } + + @TaskAction + fun execute() { + validate() + createWorkingDirectory() + + for (tuple in actualTuples!!) { + download(tuple) + } + + project.logger.log(LogLevel.WARN, "Kind download output directory ${output.get().asFile.toPath()}") + + writeVersionFile(output.get().asFile.toPath()) + } + + private fun validate() { + try { + actualVersion = SemVer.parse(version.get()) + } catch (e: IllegalArgumentException) { + throw StopExecutionException("The supplied version is not valid: ${version.get()}") + } + + if (!tuples.isPresent) { + throw StopExecutionException("No tuples were supplied") + } + + if (tuples.get().isEmpty()) { + throw StopExecutionException("No tuples were supplied") + } + + actualTuples = tuples.get() + + if (!output.get().asFile.exists()) { + try { + output.get().asFile.mkdirs() + } catch (e: Exception) { + throw GradleException("Unable to create base artifact directory") + } + } + } + + private fun createWorkingDirectory() { + try { + workingDirectory = Files.createTempDirectory("kind-artifacts") + workingDirectory!!.toFile().deleteOnExit() + } catch (e: Exception) { + throw StopExecutionException("Unable to create working directory") + } + } + + private fun download(tuple: ArtifactTuple) { + val downloadUrl = String.format( + KIND_RELEASE_URL_TEMPLATE, + actualVersion!!.toString(), + tuple.operatingSystem.descriptor, + tuple.architecture.descriptor + ) + + val tempFile = workingDirectory!!.resolve(KIND_EXECUTABLE_PREFIX + tuple.operatingSystem.fileExtension) + + try { + val url = URL(downloadUrl) + url.openStream().use { input -> + Files.newOutputStream(tempFile).use { output -> + input.copyTo(output) + output.flush() + } + } + } catch (e: Exception) { + throw GradleException("Unable to download artifact from: $downloadUrl to: $tempFile") + } + + val destination = + output.get().asFile.toPath().resolve(tuple.operatingSystem.descriptor) + .resolve(tuple.architecture.descriptor) + copyFile(tempFile, destination) + } + + private fun copyFile(source: Path, destination: Path) { + if (!destination.exists()) { + try { + destination.createDirectories() + } catch (e: Exception) { + throw GradleException("Unable to create destination directory") + } + } + + try { + project.logger.log(LogLevel.DEBUG, "Copying ${source} to ${destination}") + project.copy { + from(source) + into(destination) + includeEmptyDirs = false + } + } catch (e: Exception) { + throw GradleException("Unable to write '${source}' to '${destination}'") + } + } + + private fun writeVersionFile(path: Path) { + val versionFile = path.resolve(KIND_VERSION_FILE) + try { + Files.writeString(versionFile, actualVersion!!.toString()) + } catch (e: Exception) { + throw GradleException("Unable to write version file") + } + } +} diff --git a/fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/kind/release/OperatingSystem.kt b/fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/kind/release/OperatingSystem.kt new file mode 100644 index 000000000..897080ed0 --- /dev/null +++ b/fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/kind/release/OperatingSystem.kt @@ -0,0 +1,7 @@ +package com.hedera.fullstack.gradle.plugin.kind.release + +enum class OperatingSystem(val descriptor: String, val fileExtension: String) { + DARWIN("darwin", ""), + LINUX("linux", ""), + WINDOWS("windows", ".exe"); +} diff --git a/fullstack-gradle-plugin/src/test/java/com/hedera/fullstack/gradle/plugin/kind/release/KindArtifactTaskTest.java b/fullstack-gradle-plugin/src/test/java/com/hedera/fullstack/gradle/plugin/kind/release/KindArtifactTaskTest.java new file mode 100644 index 000000000..18a70dac5 --- /dev/null +++ b/fullstack-gradle-plugin/src/test/java/com/hedera/fullstack/gradle/plugin/kind/release/KindArtifactTaskTest.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * 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 com.hedera.fullstack.gradle.plugin.kind.release; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.nio.file.Path; +import org.gradle.api.Project; +import org.gradle.api.file.Directory; +import org.gradle.testfixtures.ProjectBuilder; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class KindArtifactTaskTest { + static final String OS = System.getProperty("os.name").toLowerCase(); + static final String BIT = System.getProperty("os.arch").toLowerCase(); + + private static Project project; + + @BeforeAll + static void beforeAll() { + project = ProjectBuilder.builder().build(); + project.getPlugins().apply("java"); + } + + private static boolean isWindows() { + return (OS.contains("win")); + } + + private static boolean isMac() { + return (OS.contains("mac")); + } + + private static boolean isLinux() { + return (OS.contains("linux")); + } + + private static boolean isArm64() { + return (BIT.contains("arm64") || BIT.contains("aarch64")); + } + + private static boolean isAmd64() { + return (BIT.contains("amd64") || BIT.contains("x86_64")); + } + + private static String getOs() { + if (isWindows()) { + return "windows"; + } else if (isMac()) { + return "darwin"; + } else if (isLinux()) { + return "linux"; + } else { + return "unknown"; + } + } + + private static String getArchitecture() { + if (isArm64()) { + return "arm64"; + } else if (isAmd64()) { + return "amd64"; + } else { + return "unknown"; + } + } + + @Test + @DisplayName("Test kind artifact download") + void testKindArtifactDownload() { + KindArtifactTask kindArtifactTask = project.getTasks() + .create("kindArtifactDownloadTask", KindArtifactTask.class, task -> { + task.getVersion().set("0.20.0"); + }); + kindArtifactTask.execute(); + Directory directory = kindArtifactTask.getOutput().get(); + File kindExecutable = Path.of(directory.getAsFile().getAbsolutePath()) + .resolve(getOs()) + .resolve(getArchitecture()) + .resolve("kind" + (isWindows() ? ".exe" : "")) + .toFile(); + assertThat(kindExecutable).exists(); + } +}