Skip to content

Commit

Permalink
feat: add a Gradle task for downloading executable for Kubernetes Kind (
Browse files Browse the repository at this point in the history
#425)

Signed-off-by: Jeromy Cannon <[email protected]>
  • Loading branch information
jeromy-cannon authored Oct 23, 2023
1 parent 201bda5 commit aaa9660
Show file tree
Hide file tree
Showing 7 changed files with 331 additions and 0 deletions.
5 changes: 5 additions & 0 deletions fullstack-examples/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -72,6 +73,10 @@ tasks.register<HelmUninstallChartTask>("helmUninstallNotAChart") {
ifExists.set(true)
}

val kindVersion = "0.20.0"

tasks.register<KindArtifactTask>("kindArtifact") { version.set(kindVersion) }

tasks.check {
dependsOn("helmInstallNginxChart")
dependsOn("helmNginxExists")
Expand Down
8 changes: 8 additions & 0 deletions fullstack-gradle-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

Expand All @@ -40,3 +44,7 @@ gradlePlugin {
}
}
}

repositories { mavenCentral() }

kotlin { jvmToolchain(17) }
Original file line number Diff line number Diff line change
@@ -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");
}
Original file line number Diff line number Diff line change
@@ -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<ArtifactTuple> {
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}"
}
}
Original file line number Diff line number Diff line change
@@ -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<String> = project.objects.property(String::class.java).convention("0.20.0")

@get:Input
val tuples: ListProperty<ArtifactTuple> = 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<ArtifactTuple>? = 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<JavaPluginExtension> {
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")
}
}
}
Original file line number Diff line number Diff line change
@@ -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");
}
Original file line number Diff line number Diff line change
@@ -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();
}
}

0 comments on commit aaa9660

Please sign in to comment.