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 a Gradle task for downloading executable for Kubernetes Kind #425

Merged
merged 6 commits into from
Oct 23, 2023
Merged
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
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();
}
}