diff --git a/.github/workflows/zxc-compile-code.yaml b/.github/workflows/zxc-compile-code.yaml index 788389b28..f79ec2f4f 100644 --- a/.github/workflows/zxc-compile-code.yaml +++ b/.github/workflows/zxc-compile-code.yaml @@ -162,6 +162,15 @@ jobs: gradle-version: ${{ inputs.gradle-version }} arguments: assemble --scan + - name: Examples Compile + id: gradle-build-examples + uses: gradle/gradle-build-action@243af859f8ca30903d9d7f7936897ca0358ba691 # v2.7.1 + with: + gradle-version: ${{ inputs.gradle-version }} + arguments: assemble --scan + build-root-directory: fullstack-examples + gradle-executable: gradlew + - name: Spotless Check uses: gradle/gradle-build-action@243af859f8ca30903d9d7f7936897ca0358ba691 # v2.7.1 if: ${{ inputs.enable-spotless-check && steps.gradle-build.conclusion == 'success' && !cancelled() }} @@ -169,6 +178,15 @@ jobs: gradle-version: ${{ inputs.gradle-version }} arguments: spotlessCheck --scan + - name: Examples Spotless Check + uses: gradle/gradle-build-action@243af859f8ca30903d9d7f7936897ca0358ba691 # v2.7.1 + if: ${{ inputs.enable-spotless-check && steps.gradle-build-examples.conclusion == 'success' && !cancelled() }} + with: + gradle-version: ${{ inputs.gradle-version }} + arguments: spotlessCheck --scan + build-root-directory: fullstack-examples + gradle-executable: gradlew + - name: Unit Tests id: gradle-test uses: gradle/gradle-build-action@243af859f8ca30903d9d7f7936897ca0358ba691 # v2.7.1 @@ -177,6 +195,16 @@ jobs: gradle-version: ${{ inputs.gradle-version }} arguments: check --scan + - name: Examples Unit Tests + id: gradle-test-examples + uses: gradle/gradle-build-action@243af859f8ca30903d9d7f7936897ca0358ba691 # v2.7.1 + if: ${{ inputs.enable-unit-tests && steps.gradle-build-examples.conclusion == 'success' && !cancelled() && !failure() }} + with: + gradle-version: ${{ inputs.gradle-version }} + arguments: check --scan + build-root-directory: fullstack-examples + gradle-executable: gradlew + - name: Publish Unit Test Report uses: actionite/publish-unit-test-result-action@1e01e49081c6c4073913aa4b7980fa83e709f322 # v2.3.0 if: ${{ inputs.enable-unit-tests && steps.gradle-build.conclusion == 'success' && !cancelled() && !failure() }} diff --git a/build-logic/project-plugins/src/main/kotlin/com.hedera.fullstack.jpms-modules.gradle.kts b/build-logic/project-plugins/src/main/kotlin/com.hedera.fullstack.jpms-modules.gradle.kts index 713db9cf0..b5dd44792 100644 --- a/build-logic/project-plugins/src/main/kotlin/com.hedera.fullstack.jpms-modules.gradle.kts +++ b/build-logic/project-plugins/src/main/kotlin/com.hedera.fullstack.jpms-modules.gradle.kts @@ -22,8 +22,6 @@ plugins { } javaModuleDependencies { - versionsFromConsistentResolution(":fullstack-helm-client") - moduleNameToGA.put("com.hedera.fullstack.junit.support", "com.hedera.fullstack:fullstack-junit-support") moduleNameToGA.put("com.hedera.fullstack.test.toolkit", "com.hedera.fullstack:fullstack-test-toolkit") } diff --git a/build-logic/project-plugins/src/main/kotlin/com.hedera.fullstack.root.gradle.kts b/build-logic/project-plugins/src/main/kotlin/com.hedera.fullstack.root.gradle.kts index e69de29bb..c85cda355 100644 --- a/build-logic/project-plugins/src/main/kotlin/com.hedera.fullstack.root.gradle.kts +++ b/build-logic/project-plugins/src/main/kotlin/com.hedera.fullstack.root.gradle.kts @@ -0,0 +1,23 @@ +/* + * Copyright 2023 Hedera Hashgraph, LLC + * + * This software is the confidential and proprietary information of + * Hedera Hashgraph, LLC. ("Confidential Information"). You shall not + * disclose such Confidential Information and shall use it only in + * accordance with the terms of the license agreement you entered into + * with Hedera Hashgraph. + * + * HEDERA HASHGRAPH MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY OF + * THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + * TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE, OR NON-INFRINGEMENT. HEDERA HASHGRAPH SHALL NOT BE LIABLE FOR + * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR + * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. + */ + +plugins { + id("com.autonomousapps.dependency-analysis") + id("com.hedera.fullstack.aggregate-reports") + id("com.hedera.fullstack.spotless-conventions") + id("com.hedera.fullstack.spotless-kotlin-conventions") +} diff --git a/build-logic/project-plugins/src/main/kotlin/com.hedera.fullstack.umbrella.gradle.kts b/build-logic/project-plugins/src/main/kotlin/com.hedera.fullstack.umbrella.gradle.kts deleted file mode 100644 index 9ee7a6707..000000000 --- a/build-logic/project-plugins/src/main/kotlin/com.hedera.fullstack.umbrella.gradle.kts +++ /dev/null @@ -1,7 +0,0 @@ - -plugins { - id("com.autonomousapps.dependency-analysis") - id("com.hedera.fullstack.aggregate-reports") - id("com.hedera.fullstack.spotless-conventions") - id("com.hedera.fullstack.spotless-kotlin-conventions") -} diff --git a/fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/Dummy.java b/build-logic/settings-plugins/build.gradle.kts similarity index 77% rename from fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/Dummy.java rename to build-logic/settings-plugins/build.gradle.kts index bee3588a9..3ef382347 100644 --- a/fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/Dummy.java +++ b/build-logic/settings-plugins/build.gradle.kts @@ -14,6 +14,15 @@ * limitations under the License. */ -package com.hedera.fullstack.gradle.plugin; +plugins { + `kotlin-dsl` +} -public class Dummy {} +repositories { + gradlePluginPortal() + mavenCentral() +} + +dependencies { + implementation("com.gradle:gradle-enterprise-gradle-plugin:3.14.1") +} \ No newline at end of file diff --git a/build-logic/settings-plugins/src/main/kotlin/com.hedera.fullstack.settings.settings.gradle.kts b/build-logic/settings-plugins/src/main/kotlin/com.hedera.fullstack.settings.settings.gradle.kts new file mode 100644 index 000000000..68d9408d3 --- /dev/null +++ b/build-logic/settings-plugins/src/main/kotlin/com.hedera.fullstack.settings.settings.gradle.kts @@ -0,0 +1,44 @@ +/* + * 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. + */ + +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} + +plugins { + id("com.gradle.enterprise") +} + +// Enable Gradle Build Scan +gradleEnterprise { + buildScan { + termsOfServiceUrl = "https://gradle.com/terms-of-service" + termsOfServiceAgree = "yes" + + if (!System.getenv("CI").isNullOrEmpty()) { + publishAlways() + tag("CI") + } + } +} + +// Allow projects inside a build to be addressed by dependency coordinates notation. +// https://docs.gradle.org/current/userguide/composite_builds.html#included_build_declaring_substitutions +// Some functionality of the 'java-module-dependencies' plugin relies on this. +includeBuild(".") \ No newline at end of file diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts index ec1812a0c..8f09d6797 100644 --- a/build-logic/settings.gradle.kts +++ b/build-logic/settings.gradle.kts @@ -1,8 +1,19 @@ - -//dependencyResolutionManagement { -// repositories { -// gradlePluginPortal() -// } -//} +/* + * 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. + */ includeBuild("project-plugins") + +includeBuild("settings-plugins") \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index c50450172..cecc1f3ea 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,7 +14,7 @@ * limitations under the License. */ -plugins { id("com.hedera.fullstack.umbrella") } +plugins { id("com.hedera.fullstack.root") } repositories { // mavenLocal() // uncomment to use local maven repository diff --git a/fullstack-examples/build.gradle.kts b/fullstack-examples/build.gradle.kts index 964ebac30..54b447777 100644 --- a/fullstack-examples/build.gradle.kts +++ b/fullstack-examples/build.gradle.kts @@ -14,22 +14,45 @@ * limitations under the License. */ +import com.hedera.fullstack.gradle.plugin.HelmInstallChartTask +import com.hedera.fullstack.gradle.plugin.HelmUninstallChartTask + plugins { + id("java") + id("com.hedera.fullstack.root") id("com.hedera.fullstack.conventions") id("com.hedera.fullstack.jpms-modules") + id("com.hedera.fullstack.fullstack-gradle-plugin") +} + +dependencies { + api(platform("com.hedera.fullstack:fullstack-bom")) + implementation("com.hedera.fullstack:fullstack-readiness-api") + implementation("com.hedera.fullstack:fullstack-monitoring-api") + implementation("com.hedera.fullstack:fullstack-test-toolkit") + implementation("com.hedera.fullstack:fullstack-validator-api") } -dependencies { api(platform(project(":fullstack-bom"))) } +tasks.register("helmInstallFstChart") { + createNamespace.set(true) + namespace.set("fst-ns") + release.set("fst") + chart.set("../charts/hedera-network") +} -testing { - suites { - @Suppress("UnstableApiUsage", "unused") - val fullstack by - registering(JvmTestSuite::class) { - useJUnitJupiter() - dependencies { implementation(project(":fullstack-examples")) } - } - } +tasks.register("helmInstallNginxChart") { + createNamespace.set(true) + namespace.set("nginx-ns") + release.set("nginx-release") + chart.set("oci://ghcr.io/nginxinc/charts/nginx-ingress") } -tasks.assemble.configure { dependsOn(tasks.named("fullstackClasses")) } +tasks.register("helmUninstallNginxChart") { + namespace.set("nginx-ns") + release.set("nginx-release") +} + +tasks.check { + dependsOn("helmInstallNginxChart") + dependsOn("helmUninstallNginxChart") +} diff --git a/fullstack-examples/settings.gradle.kts b/fullstack-examples/settings.gradle.kts new file mode 100644 index 000000000..518cac987 --- /dev/null +++ b/fullstack-examples/settings.gradle.kts @@ -0,0 +1,29 @@ +/* + * 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. + */ + +pluginManagement { + includeBuild("../build-logic") + includeBuild("..") + repositories { + mavenLocal() + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = "fullstack-examples" + +includeBuild("..") diff --git a/fullstack-gradle-plugin/build.gradle.kts b/fullstack-gradle-plugin/build.gradle.kts index f9776d955..6a2dc21fd 100644 --- a/fullstack-gradle-plugin/build.gradle.kts +++ b/fullstack-gradle-plugin/build.gradle.kts @@ -14,6 +14,27 @@ * limitations under the License. */ -plugins { id("com.hedera.fullstack.conventions") } +plugins { + id("java-gradle-plugin") + id("com.gradle.plugin-publish").version("1.2.1") + id("com.hedera.fullstack.conventions") + id("com.hedera.fullstack.maven-publish") +} -dependencies { api(platform(project(":fullstack-bom"))) } +dependencies { + api(platform(project(":fullstack-bom"))) + implementation(project(":fullstack-helm-client")) +} + +gradlePlugin { + plugins { + create("fullstackPlugin") { + id = "com.hedera.fullstack.fullstack-gradle-plugin" + group = "com.hedera.fullstack" + implementationClass = "com.hedera.fullstack.gradle.plugin.FullstackPlugin" + displayName = "Fullstack Plugin" + description = + "The Fullstack Plugin provides tools for working with Fullstack infrastructure." + } + } +} diff --git a/fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/FullstackPlugin.java b/fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/FullstackPlugin.java new file mode 100644 index 000000000..0cf9bcc73 --- /dev/null +++ b/fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/FullstackPlugin.java @@ -0,0 +1,27 @@ +/* + * 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; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; + +public class FullstackPlugin implements Plugin { + @Override + public void apply(Project project) { + // currently no implementation is needed + } +} diff --git a/fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/HelmInstallChartTask.java b/fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/HelmInstallChartTask.java new file mode 100644 index 000000000..e915e9d09 --- /dev/null +++ b/fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/HelmInstallChartTask.java @@ -0,0 +1,98 @@ +/* + * 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; + +import com.hedera.fullstack.helm.client.HelmClient; +import com.hedera.fullstack.helm.client.HelmClientBuilder; +import com.hedera.fullstack.helm.client.model.Chart; +import com.hedera.fullstack.helm.client.model.install.InstallChartOptionsBuilder; +import java.util.ArrayList; +import org.gradle.api.DefaultTask; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.SetProperty; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.options.Option; + +public abstract class HelmInstallChartTask extends DefaultTask { + @Input + @Option(option = "chart", description = "The name of the chart to install") + public abstract Property getChart(); + + @Input + @Optional + @Option(option = "createNamespace", description = "Create the release namespace if not present") + public abstract Property getCreateNamespace(); + + @Input + @Optional + @Option(option = "namespace", description = "The namespace to use when installing the chart") + public abstract Property getNamespace(); + + @Input + @Option(option = "release", description = "The name of the release to install") + public abstract Property getRelease(); + + @Input + @Optional + @Option(option = "repo", description = "The name of the repo to install") + public abstract Property getRepo(); + + @Input + @Optional + @Option( + option = "set", + description = + "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") + public abstract SetProperty getSet(); + + @Input + @Optional + @Option(option = "values", description = "Specify values in a YAML file or a URL (can specify multiple)") + public abstract SetProperty getValues(); + + @TaskAction + void installChart() { + HelmClientBuilder helmClientBuilder = HelmClient.builder(); + if (getNamespace().isPresent()) { + helmClientBuilder.defaultNamespace(getNamespace().get()); + } + HelmClient helmClient = helmClientBuilder.build(); + InstallChartOptionsBuilder optionsBuilder = InstallChartOptionsBuilder.builder(); + if (getCreateNamespace().isPresent()) { + optionsBuilder.createNamespace(getCreateNamespace().get()); + } + if (getSet().isPresent()) { + optionsBuilder.set(new ArrayList<>(getSet().get())); + } + if (getValues().isPresent()) { + optionsBuilder.values(new ArrayList<>(getValues().get())); + } + try { + helmClient.installChart( + getRelease().getOrNull(), + new Chart(getChart().getOrNull(), getRepo().getOrNull()), + optionsBuilder.build()); + } catch (Exception e) { + this.getProject() + .getLogger() + .error("HelmInstallChartTask.installChart() An ERROR occurred while installing the chart: ", e); + throw e; + } + } +} diff --git a/fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/HelmUninstallChartTask.java b/fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/HelmUninstallChartTask.java new file mode 100644 index 000000000..860d0e8a9 --- /dev/null +++ b/fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/HelmUninstallChartTask.java @@ -0,0 +1,56 @@ +/* + * 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; + +import com.hedera.fullstack.helm.client.HelmClient; +import com.hedera.fullstack.helm.client.HelmClientBuilder; +import org.gradle.api.DefaultTask; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.options.Option; + +public abstract class HelmUninstallChartTask extends DefaultTask { + @Input + @Optional + @Option(option = "namespace", description = "The namespace to use when installing the chart") + public abstract Property getNamespace(); + + @Input + @Option(option = "release", description = "The name of the release to install") + public abstract Property getRelease(); + + @TaskAction + void uninstallChart() { + HelmClientBuilder helmClientBuilder = HelmClient.builder(); + if (getNamespace().isPresent()) { + helmClientBuilder.defaultNamespace(getNamespace().get()); + } + HelmClient helmClient = helmClientBuilder.build(); + try { + helmClient.uninstallChart(getRelease().getOrNull()); + } catch (Exception e) { + this.getProject() + .getLogger() + .error( + "HelmUninstallChartTask.uninstallChart() An ERROR occurred while uninstalling the chart: ", + e); + throw e; + } + } +} diff --git a/fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/package-info.java b/fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/package-info.java deleted file mode 100644 index 47a10352d..000000000 --- a/fullstack-gradle-plugin/src/main/java/com/hedera/fullstack/gradle/plugin/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package com.hedera.fullstack.gradle.plugin; diff --git a/fullstack-gradle-plugin/src/main/java/module-info.java b/fullstack-gradle-plugin/src/main/java/module-info.java deleted file mode 100644 index 37ceb29db..000000000 --- a/fullstack-gradle-plugin/src/main/java/module-info.java +++ /dev/null @@ -1 +0,0 @@ -module com.hedera.fullstack.gradle.plugin {} diff --git a/fullstack-gradle-plugin/src/test/java/com/hedera/fullstack/gradle/plugin/HelmInstallChartTaskTest.java b/fullstack-gradle-plugin/src/test/java/com/hedera/fullstack/gradle/plugin/HelmInstallChartTaskTest.java new file mode 100644 index 000000000..101eb4a15 --- /dev/null +++ b/fullstack-gradle-plugin/src/test/java/com/hedera/fullstack/gradle/plugin/HelmInstallChartTaskTest.java @@ -0,0 +1,127 @@ +/* + * 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; + +import static com.hedera.fullstack.base.api.util.ExceptionUtils.suppressExceptions; +import static org.junit.jupiter.api.Assertions.*; + +import com.hedera.fullstack.helm.client.HelmClient; +import com.hedera.fullstack.helm.client.HelmExecutionException; +import com.hedera.fullstack.helm.client.model.Chart; +import com.hedera.fullstack.helm.client.model.Repository; +import java.io.File; +import java.io.IOException; +import java.util.List; +import org.gradle.api.Project; +import org.gradle.testfixtures.ProjectBuilder; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class HelmInstallChartTaskTest { + private static final Repository REPOSITORY = new Repository("stable", "https://charts.helm.sh/stable"); + private static final Chart CHART = new Chart("mysql", "stable"); + + private static final String RELEASE_NAME = "mysql-release"; + + private static Project project; + + @BeforeAll + static void beforeAll() { + project = ProjectBuilder.builder().build(); + } + + @Test + @Disabled("currently this requires manual intervention to run") + // 1. 'make deploy-chart' + // 2. run this test case, assuming it passes and installs fst + // 3. 'make destroy-chart' + @DisplayName("Helm Install Chart Task for Hedera Network Chart") + void testHelmInstallChartTaskForHederaNetworkChart() throws IOException { + HelmClient helmClient = HelmClient.defaultClient(); + suppressExceptions(() -> helmClient.uninstallChart("fst")); + try { + final File hederaNetworkChart = new File("../charts/hedera-network"); + final String hederaNetworkChartPath = hederaNetworkChart.getCanonicalPath(); + final File valuesFile = new File(hederaNetworkChartPath + File.separator + "values.yaml"); + final String valuesFilePath = valuesFile.getCanonicalPath(); + HelmInstallChartTask helmInstallChartTask = project.getTasks() + .create("helmInstallFstChart", HelmInstallChartTask.class, task -> { + task.getChart().set(hederaNetworkChartPath); + task.getRelease().set("fst"); + // set image for nmt-install + task.getSet().add("defaults.root.image.repository=hashgraph/full-stack-testing/ubi8-init-dind"); + task.getValues().add(valuesFilePath); + }); + assertEquals("fst", helmInstallChartTask.getRelease().get()); + helmInstallChartTask.installChart(); + } finally { + // TODO: comment this out as workaround until we no longer need manual use of make command + // suppressExceptions(() -> helmClient.uninstallChart("fst")); + } + } + + @Test + @DisplayName("Simple Helm Install Chart Task") + void testHelmInstallChartTaskSimple() { + HelmClient helmClient = + HelmClient.builder().defaultNamespace("simple-test").build(); + suppressExceptions(() -> helmClient.uninstallChart(RELEASE_NAME)); + suppressExceptions(() -> helmClient.removeRepository(REPOSITORY)); + final List repositories = helmClient.listRepositories(); + if (!repositories.contains(REPOSITORY)) { + helmClient.addRepository(REPOSITORY); + } + try { + HelmInstallChartTask helmInstallChartTask = project.getTasks() + .create("helmInstallChart", HelmInstallChartTask.class, task -> { + task.getChart().set(CHART.name()); + task.getCreateNamespace().set(true); + task.getNamespace().set("simple-test"); + task.getRelease().set(RELEASE_NAME); + task.getRepo().set(CHART.repoName()); + }); + assertEquals(RELEASE_NAME, helmInstallChartTask.getRelease().get()); + helmInstallChartTask.installChart(); + HelmUninstallChartTask helmUninstallChartTask = project.getTasks() + .create("helmUninstallChart", HelmUninstallChartTask.class, task -> { + task.getNamespace().set("simple-test"); + task.getRelease().set(RELEASE_NAME); + }); + helmUninstallChartTask.uninstallChart(); + } finally { + suppressExceptions(() -> helmClient.removeRepository(REPOSITORY)); + } + } + + @Test + @DisplayName("test an error is thrown when the chart is not found") + void testErrorThrownWhenChartNotFound() { + assertThrows(HelmExecutionException.class, () -> { + HelmInstallChartTask helmInstallChartTask = project.getTasks() + .create("helmInstallNonExistingChartChart", HelmInstallChartTask.class, task -> { + task.getChart().set("not-a-chart"); + task.getCreateNamespace().set(true); + task.getNamespace().set("test-failure"); + task.getRelease().set("not-a-release"); + task.getRepo().set("not-a-repo"); + }); + helmInstallChartTask.installChart(); + }); + } +} diff --git a/fullstack-gradle-plugin/src/test/java/com/hedera/fullstack/gradle/plugin/HelmUninstallChartTaskTest.java b/fullstack-gradle-plugin/src/test/java/com/hedera/fullstack/gradle/plugin/HelmUninstallChartTaskTest.java new file mode 100644 index 000000000..4fe613124 --- /dev/null +++ b/fullstack-gradle-plugin/src/test/java/com/hedera/fullstack/gradle/plugin/HelmUninstallChartTaskTest.java @@ -0,0 +1,48 @@ +/* + * 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; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.hedera.fullstack.helm.client.HelmExecutionException; +import org.gradle.api.Project; +import org.gradle.testfixtures.ProjectBuilder; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class HelmUninstallChartTaskTest { + private static Project project; + + @BeforeAll + static void beforeAll() { + project = ProjectBuilder.builder().build(); + } + + @Test + @DisplayName("test an error is thrown when the chart is not found") + void testErrorThrownWhenChartNotFound() { + assertThrows(HelmExecutionException.class, () -> { + HelmUninstallChartTask helmUninstallChartTask = project.getTasks() + .create("helmUninstallNonExistingChartChart", HelmUninstallChartTask.class, task -> { + task.getNamespace().set("test-failure"); + task.getRelease().set("not-a-release"); + }); + helmUninstallChartTask.uninstallChart(); + }); + } +} diff --git a/fullstack-gradle-plugin/src/test/java/module-info.java b/fullstack-gradle-plugin/src/test/java/module-info.java deleted file mode 100644 index 5cdfd426e..000000000 --- a/fullstack-gradle-plugin/src/test/java/module-info.java +++ /dev/null @@ -1 +0,0 @@ -module com.hedera.fullstack.gradle.plugin.test {} diff --git a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/HelmClient.java b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/HelmClient.java index 3afac8067..8c38a2574 100644 --- a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/HelmClient.java +++ b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/HelmClient.java @@ -22,6 +22,7 @@ import com.hedera.fullstack.helm.client.model.Repository; import com.hedera.fullstack.helm.client.model.chart.Release; import com.hedera.fullstack.helm.client.model.install.InstallChartOptions; +import com.hedera.fullstack.helm.client.model.test.TestChartOptions; import java.util.List; /** @@ -92,6 +93,14 @@ default Release installChart(String releaseName, Chart chart) { */ void uninstallChart(String releaseName); + /** + * Executes the Helm CLI {@code test} sub-command and tests the specified Helm chart. + * + * @param releaseName the name of the release to test. + * @param options the options to pass to the Helm CLI command. + */ + void testChart(String releaseName, TestChartOptions options); + /** * Creates a new {@link HelmClientBuilder} instance with the default configuration. * diff --git a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/execution/HelmExecution.java b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/execution/HelmExecution.java index 405896200..1e2d0d22a 100644 --- a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/execution/HelmExecution.java +++ b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/execution/HelmExecution.java @@ -218,30 +218,29 @@ public T responseAs(final Class responseClass, final Duration timeout) { } if (exitCode() != 0) { - throw new HelmExecutionException(exitCode()); + throw new HelmExecutionException( + exitCode(), + StreamUtils.streamToString(suppressExceptions(this::standardOutput)), + StreamUtils.streamToString(suppressExceptions(this::standardError))); } - final String standardOutput = StreamUtils.streamToString(suppressExceptions(this::standardOutput)); final String standardError = StreamUtils.streamToString(suppressExceptions(this::standardError)); - LOGGER.atDebug() - .setMessage( - "ResponseAs exiting with exitCode: {}\n\tResponseClass: {}\n\tstandardOutput: {}\n\tstandardError: {}") - .addArgument(this::exitCode) - .addArgument(responseClass.getName()) - .addArgument(standardOutput) - .addArgument(standardError) - .log(); + LOGGER.debug( + "ResponseAs exiting with exitCode: {}TODO\n\tResponseClass: {}\n\tstandardOutput: {}\n\tstandardError: {}", + exitCode(), + responseClass.getName(), + standardOutput, + standardError); try { return OBJECT_MAPPER.readValue(standardOutput, responseClass); } catch (final Exception e) { - LOGGER.atWarn() - .setMessage("ResponseAs failed to deserialize response into class: {}\n\tresponse: {}") - .addArgument(responseClass.getName()) - .addArgument(standardOutput) - .setCause(e) - .log(); + LOGGER.warn( + String.format( + "ResponseAs failed to deserialize response into class: %s%n\tresponse: %s", + responseClass.getName(), standardOutput), + e); throw new HelmParserException(String.format(MSG_DESERIALIZATION_ERROR, responseClass.getName()), e); } @@ -291,14 +290,12 @@ public List responseAsList(final Class responseClass, final Duration t final String standardOutput = StreamUtils.streamToString(suppressExceptions(this::standardOutput)); final String standardError = StreamUtils.streamToString(suppressExceptions(this::standardError)); - LOGGER.atDebug() - .setMessage( - "ResponseAsList exiting with exitCode: {}\n\tResponseClass: {}\n\tstandardOutput: {}\n\tstandardError: {}") - .addArgument(this::exitCode) - .addArgument(responseClass.getName()) - .addArgument(standardOutput) - .addArgument(standardError) - .log(); + LOGGER.debug( + "ResponseAsList exiting with exitCode: {}\n\tResponseClass: {}\n\tstandardOutput: {}\n\tstandardError: {}", + exitCode(), + responseClass.getName(), + standardOutput, + standardError); try { return OBJECT_MAPPER @@ -306,13 +303,11 @@ public List responseAsList(final Class responseClass, final Duration t .readValues(standardOutput) .readAll(); } catch (final Exception e) { - LOGGER.atWarn() - .setMessage( - "ResponseAsList failed to deserialize the output into a list of the specified class: {}\n\tresponse: {}") - .addArgument(responseClass.getName()) - .addArgument(standardOutput) - .setCause(e) - .log(); + LOGGER.warn( + String.format( + "ResponseAsList failed to deserialize the output into a list of the specified class: %s%n\tresponse: %s", + responseClass.getName(), standardOutput), + e); throw new HelmParserException(String.format(MSG_LIST_DESERIALIZATION_ERROR, responseClass.getName()), e); } @@ -349,20 +344,18 @@ public void call(final Duration timeout) { final String standardOutput = StreamUtils.streamToString(suppressExceptions(this::standardOutput)); final String standardError = StreamUtils.streamToString(suppressExceptions(this::standardError)); - LOGGER.atDebug() - .setMessage("Call exiting with exitCode: {}\n\tstandardOutput: {}\n\tstandardError: {}") - .addArgument(this::exitCode) - .addArgument(standardOutput) - .addArgument(standardError) - .log(); + LOGGER.debug( + "Call exiting with exitCode: {}\n\tstandardOutput: {}\n\tstandardError: {}", + exitCode(), + standardOutput, + standardError); if (exitCode() != 0) { - LOGGER.atWarn() - .setMessage("Call failed with exitCode: {}\n\tstandardOutput: {}\n\tstandardError: {}") - .addArgument(this::exitCode) - .addArgument(standardOutput) - .addArgument(standardError) - .log(); + LOGGER.warn( + "Call failed with exitCode: {}\n\tstandardOutput: {}\n\tstandardError: {}", + exitCode(), + standardOutput, + standardError); throw new HelmExecutionException(exitCode(), standardError, standardOutput); } diff --git a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/execution/HelmExecutionBuilder.java b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/execution/HelmExecutionBuilder.java index 58a4751b7..d4bd2ad85 100644 --- a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/execution/HelmExecutionBuilder.java +++ b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/execution/HelmExecutionBuilder.java @@ -16,7 +16,9 @@ package com.hedera.fullstack.helm.client.execution; +import com.hedera.fullstack.base.api.collections.KeyValuePair; import com.hedera.fullstack.helm.client.HelmConfigurationException; +import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.util.*; @@ -28,7 +30,8 @@ */ public final class HelmExecutionBuilder { private static final Logger LOGGER = LoggerFactory.getLogger(HelmExecutionBuilder.class); - + public static final String NAME_MUST_NOT_BE_NULL = "name must not be null"; + public static final String VALUE_MUST_NOT_BE_NULL = "value must not be null"; /** * The path to the helm executable. */ @@ -42,7 +45,12 @@ public final class HelmExecutionBuilder { /** * The arguments to be passed to the helm command. */ - private final Map arguments; + private final HashMap arguments; + + /** + * The list of options and a list of their one or more values. + */ + private final List>> optionsWithMultipleValues; /** * The flags to be passed to the helm command. @@ -73,10 +81,15 @@ public HelmExecutionBuilder(final Path helmExecutable) { this.helmExecutable = Objects.requireNonNull(helmExecutable, "helmExecutable must not be null"); this.subcommands = new ArrayList<>(); this.arguments = new HashMap<>(); + this.optionsWithMultipleValues = new ArrayList<>(); this.positionals = new ArrayList<>(); this.flags = new ArrayList<>(); this.environmentVariables = new HashMap<>(); - this.workingDirectory = this.helmExecutable.getParent(); + + String workingDirectoryString = System.getenv("PWD"); + this.workingDirectory = (workingDirectoryString == null || workingDirectoryString.isBlank()) + ? this.helmExecutable.getParent() + : new File(workingDirectoryString).toPath(); } /** @@ -100,12 +113,28 @@ public HelmExecutionBuilder subcommands(final String... commands) { * @throws NullPointerException if either {@code name} or {@code value} is {@code null}. */ public HelmExecutionBuilder argument(final String name, final String value) { - Objects.requireNonNull(name, "name must not be null"); - Objects.requireNonNull(value, "value must not be null"); + Objects.requireNonNull(name, NAME_MUST_NOT_BE_NULL); + Objects.requireNonNull(value, VALUE_MUST_NOT_BE_NULL); this.arguments.put(name, value); return this; } + /** + * Adds an option with a provided list of values to the helm command. This is used for options that have can have + * multiple values for a single option. (e.g. --set and --values) + * + * @param name the name of the option. + * @param value the list of values for the given option. + * @return this builder. + * @throws NullPointerException if either {@code name} or {@code value} is {@code null}. + */ + public HelmExecutionBuilder optionsWithMultipleValues(final String name, final List value) { + Objects.requireNonNull(name, NAME_MUST_NOT_BE_NULL); + Objects.requireNonNull(value, VALUE_MUST_NOT_BE_NULL); + this.optionsWithMultipleValues.add(new KeyValuePair<>(name, value)); + return this; + } + /** * Adds a positional argument to the helm command. * @@ -114,7 +143,7 @@ public HelmExecutionBuilder argument(final String name, final String value) { * @throws NullPointerException if {@code value} is {@code null}. */ public HelmExecutionBuilder positional(final String value) { - Objects.requireNonNull(value, "value must not be null"); + Objects.requireNonNull(value, VALUE_MUST_NOT_BE_NULL); this.positionals.add(value); return this; } @@ -128,20 +157,20 @@ public HelmExecutionBuilder positional(final String value) { * @throws NullPointerException if either {@code name} or {@code value} is {@code null}. */ public HelmExecutionBuilder environmentVariable(final String name, final String value) { - Objects.requireNonNull(name, "name must not be null"); - Objects.requireNonNull(value, "value must not be null"); + Objects.requireNonNull(name, NAME_MUST_NOT_BE_NULL); + Objects.requireNonNull(value, VALUE_MUST_NOT_BE_NULL); this.environmentVariables.put(name, value); return this; } /** - * Sets the working directory for the helm process. + * Sets the Path of the working directory for the helm process. * - * @param workingDirectory the working directory. + * @param workingDirectoryPath the Path of the working directory. * @return this builder. */ - public HelmExecutionBuilder workingDirectory(final Path workingDirectory) { - this.workingDirectory = Objects.requireNonNull(workingDirectory, "workingDirectory must not be null"); + public HelmExecutionBuilder workingDirectory(final Path workingDirectoryPath) { + this.workingDirectory = Objects.requireNonNull(workingDirectoryPath, "workingDirectoryPath must not be null"); return this; } @@ -191,18 +220,23 @@ private String[] buildCommand() { command.addAll(subcommands); command.addAll(flags); - for (final Map.Entry entry : arguments.entrySet()) { - command.add(String.format("--%s", entry.getKey())); - command.add(entry.getValue()); - } + arguments.forEach((key, value) -> { + command.add(String.format("--%s", key)); + command.add(value); + }); + + optionsWithMultipleValues.forEach(entry -> entry.value().forEach(value -> { + command.add(String.format("--%s", entry.key())); + command.add(value); + })); command.addAll(positionals); String[] commandArray = command.toArray(new String[0]); - LOGGER.atDebug() - .setMessage("Helm command: {}") - .addArgument(String.join(" ", Arrays.copyOfRange(commandArray, 1, commandArray.length))) - .log(); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "Helm command: {}", String.join(" ", Arrays.copyOfRange(commandArray, 1, commandArray.length))); + } return commandArray; } diff --git a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/impl/DefaultHelmClient.java b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/impl/DefaultHelmClient.java index 1d134c09f..bad753a0c 100644 --- a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/impl/DefaultHelmClient.java +++ b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/impl/DefaultHelmClient.java @@ -25,9 +25,11 @@ import com.hedera.fullstack.helm.client.model.Version; import com.hedera.fullstack.helm.client.model.chart.Release; import com.hedera.fullstack.helm.client.model.install.InstallChartOptions; +import com.hedera.fullstack.helm.client.model.test.TestChartOptions; import com.hedera.fullstack.helm.client.proxy.request.HelmRequest; import com.hedera.fullstack.helm.client.proxy.request.authentication.KubeAuthentication; import com.hedera.fullstack.helm.client.proxy.request.chart.ChartInstallRequest; +import com.hedera.fullstack.helm.client.proxy.request.chart.ChartTestRequest; import com.hedera.fullstack.helm.client.proxy.request.chart.ChartUninstallRequest; import com.hedera.fullstack.helm.client.proxy.request.common.VersionRequest; import com.hedera.fullstack.helm.client.proxy.request.repository.RepositoryAddRequest; @@ -120,6 +122,14 @@ public void uninstallChart(final String releaseName) { }); } + @Override + public void testChart(final String releaseName, final TestChartOptions options) { + executeInternal(new ChartTestRequest(releaseName, options), Void.class, (b, c) -> { + b.call(); + return null; + }); + } + /** * Applies the default namespace and authentication configuration to the given builder. * diff --git a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/Chart.java b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/Chart.java index ab42ca286..1f199a5b8 100644 --- a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/Chart.java +++ b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/Chart.java @@ -16,19 +16,12 @@ package com.hedera.fullstack.helm.client.model; -import java.util.Objects; - /** * Represents a chart and is used to interact with the Helm install and uninstall commands. * @param repoName the name of repository which contains the Helm chart. * @param name the name of the Helm chart. */ public record Chart(String name, String repoName) { - - public Chart { - Objects.requireNonNull(repoName, "repoName must not be null"); - } - public Chart(String name) { this(name, null); } diff --git a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/install/InstallChartOptions.java b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/install/InstallChartOptions.java index 802687861..2a3ac661e 100644 --- a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/install/InstallChartOptions.java +++ b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/install/InstallChartOptions.java @@ -18,6 +18,7 @@ import com.hedera.fullstack.helm.client.execution.HelmExecutionBuilder; import com.hedera.fullstack.helm.client.model.Options; +import java.util.List; /** * The options to be supplied to the helm install command. @@ -32,6 +33,7 @@ * @param passCredentials - pass credentials to all domains. * @param password - chart repository password where to locate the requested chart. * @param repo - chart repository url where to locate the requested chart. + * @param set - set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) * @param skipCrds - if set, no CRDs will be installed. By default, CRDs are installed if not already present. * @param timeout - time to wait for any individual Kubernetes operation (like Jobs for hooks) (default 5m0s). * @param username - chart repository username where to locate the requested chart. @@ -54,10 +56,11 @@ public record InstallChartOptions( boolean passCredentials, String password, String repo, + List set, boolean skipCrds, String timeout, String username, - String values, + List values, boolean verify, String version, boolean waitFor) @@ -94,6 +97,10 @@ public void apply(final HelmExecutionBuilder builder) { builder.argument("repo", repo()); } + if (set() != null) { + builder.optionsWithMultipleValues("set", set()); + } + if (timeout() != null) { builder.argument("timeout", timeout()); } @@ -103,7 +110,7 @@ public void apply(final HelmExecutionBuilder builder) { } if (values() != null) { - builder.argument("values", values()); + builder.optionsWithMultipleValues("values", values()); } if (version() != null) { diff --git a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/install/InstallChartOptionsBuilder.java b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/install/InstallChartOptionsBuilder.java index afec8d648..524eca9f9 100644 --- a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/install/InstallChartOptionsBuilder.java +++ b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/install/InstallChartOptionsBuilder.java @@ -16,6 +16,8 @@ package com.hedera.fullstack.helm.client.model.install; +import java.util.List; + /** * The builder for the {@link InstallChartOptions}. */ @@ -29,10 +31,11 @@ public final class InstallChartOptionsBuilder { private boolean passCredentials; private String password; private String repo; + private List set; private boolean skipCrds; private String timeout; private String username; - private String values; + private List values; private boolean verify; private String version; private boolean waitFor; @@ -149,6 +152,17 @@ public InstallChartOptionsBuilder repo(String repo) { return this; } + /** + * set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) + * + * @param valueOverride set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) + * @return the current InstallChartOptionsBuilder. + */ + public InstallChartOptionsBuilder set(List valueOverride) { + this.set = valueOverride; + return this; + } + /** * if set, no CRDs will be installed. By default, CRDs are installed if not already present. * @@ -188,7 +202,7 @@ public InstallChartOptionsBuilder username(String username) { * @param values specify values in a YAML file or a URL (can specify multiple). * @return the current InstallChartOptionsBuilder. */ - public InstallChartOptionsBuilder values(String values) { + public InstallChartOptionsBuilder values(List values) { this.values = values; return this; } @@ -247,6 +261,7 @@ public InstallChartOptions build() { passCredentials, password, repo, + set, skipCrds, timeout, username, diff --git a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/test/TestChartOptions.java b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/test/TestChartOptions.java new file mode 100644 index 000000000..184440e70 --- /dev/null +++ b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/test/TestChartOptions.java @@ -0,0 +1,58 @@ +/* + * 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.helm.client.model.test; + +import com.hedera.fullstack.helm.client.execution.HelmExecutionBuilder; +import com.hedera.fullstack.helm.client.model.Options; + +/** + * Represents the options to use when testing a chart. + * + * @param filter specify tests by attribute (currently "name") using attribute=value syntax or '!attribute=value' to + * exclude a test (can specify multiple or separate values with commas: name=test1,name=test2) + * @param timeout Time to wait for any individual Kubernetes operation (like Jobs for hooks) (default 5m0s). + */ +public record TestChartOptions(String filter, String timeout) implements Options { + /** + * Returns an instance of the TestChartOptionsBuilder. + * + * @return the TestChartOptionsBuilder. + */ + public static TestChartOptionsBuilder builder() { + return TestChartOptionsBuilder.builder(); + } + + /** + * Returns an instance of the default TestChartOptions. + * + * @return the default TestChartOptions. + */ + public static TestChartOptions defaults() { + return builder().build(); + } + + @Override + public void apply(final HelmExecutionBuilder builder) { + if (filter() != null) { + builder.argument("filter", filter()); + } + + if (timeout() != null) { + builder.argument("timeout", timeout()); + } + } +} diff --git a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/test/TestChartOptionsBuilder.java b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/test/TestChartOptionsBuilder.java new file mode 100644 index 000000000..899dba9f1 --- /dev/null +++ b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/model/test/TestChartOptionsBuilder.java @@ -0,0 +1,68 @@ +/* + * 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.helm.client.model.test; + +/** + * The builder for the {@link TestChartOptions}. + */ +public class TestChartOptionsBuilder { + private String filter; + private String timeout; + + /** + * Returns an instance of the TestChartOptionsBuilder. + * + * @return the TestChartOptionsBuilder. + */ + public static TestChartOptionsBuilder builder() { + return new TestChartOptionsBuilder(); + } + + /** + * Specify tests by attribute (currently "name") using attribute=value syntax or '!attribute=value' to exclude a + * test (can specify multiple or separate values with commas: name=test1,name=test2) + * + * @param filter Specify tests by attribute (currently "name") using attribute=value syntax or '!attribute=value' to + * exclude a test (can specify multiple or separate values with commas: name=test1,name=test2) + * @return the current TestChartOptionsBuilder. + */ + public TestChartOptionsBuilder filter(String filter) { + this.filter = filter; + return this; + } + + /** + * Time to wait for any individual Kubernetes operation (like Jobs for hooks) (default 5m0s). + * + * @param timeout Time to wait for any individual Kubernetes operation (like Jobs for hooks) (default 5m0s). + *

+ * return the current TestChartOptionsBuilder. + */ + public TestChartOptionsBuilder timeout(String timeout) { + this.timeout = timeout; + return this; + } + + /** + * builds the {@link TestChartOptions} instance. + * + * @return the {@link TestChartOptions} instance. + */ + public TestChartOptions build() { + return new TestChartOptions(filter, timeout); + } +} diff --git a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/proxy/request/chart/ChartTestRequest.java b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/proxy/request/chart/ChartTestRequest.java new file mode 100644 index 000000000..c691d0642 --- /dev/null +++ b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/proxy/request/chart/ChartTestRequest.java @@ -0,0 +1,58 @@ +/* + * 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.helm.client.proxy.request.chart; + +import com.hedera.fullstack.helm.client.execution.HelmExecutionBuilder; +import com.hedera.fullstack.helm.client.model.test.TestChartOptions; +import com.hedera.fullstack.helm.client.proxy.request.HelmRequest; +import java.util.Objects; + +/** + * A request to uninstall a chart. + * + * @param releaseName the name of the release to uninstall. + */ +public record ChartTestRequest(String releaseName, TestChartOptions options) implements HelmRequest { + + public ChartTestRequest { + Objects.requireNonNull(releaseName, "releaseName must not be null"); + Objects.requireNonNull(options, "options must not be null"); + + if (releaseName.isBlank()) { + throw new IllegalArgumentException("releaseName must not be null or blank"); + } + } + + /** + * Creates a new install request with the given chart and default options. + * @param releaseName The name of the release. + */ + public ChartTestRequest(String releaseName) { + this(releaseName, TestChartOptions.defaults()); + } + + @Override + public void apply(HelmExecutionBuilder builder) { + builder.subcommands("test"); + + if (options != null) { + options.apply(builder); + } + + builder.positional(releaseName); + } +} diff --git a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/resource/HelmSoftwareLoader.java b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/resource/HelmSoftwareLoader.java index 74e270c6e..cfd0063fd 100644 --- a/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/resource/HelmSoftwareLoader.java +++ b/fullstack-helm-client/src/main/java/com/hedera/fullstack/helm/client/resource/HelmSoftwareLoader.java @@ -63,9 +63,10 @@ private HelmSoftwareLoader() { * Unpacks the Helm executable contained in the JAR file into a temporary directory. * * @return the path to the Helm executable. - * @throws HelmConfigurationException if the Helm executable cannot be unpacked or the - * operating system/architecture combination is not supported. - * @implNote This method expects the executable to be present at the following location in the JAR file: {@code /software///helm}. + * @throws HelmConfigurationException if the Helm executable cannot be unpacked or the operating system/architecture + * combination is not supported. + * @implNote This method expects the executable to be present at the following location in the JAR file: + * {@code /software///helm}. */ public static Path installSupportedVersion() { try { @@ -86,12 +87,11 @@ public static Path installSupportedVersion() { pathBuilder.append(".exe"); } - LOGGER.atDebug() - .setMessage("Loading Helm executable from JAR file. [os={}, arch={}, path={}]") - .addArgument(os.name()) - .addArgument(arch.name()) - .addArgument(pathBuilder.toString()) - .log(); + LOGGER.debug( + "Loading Helm executable from JAR file. [os={}, arch={}, path={}]", + os.name(), + arch.name(), + pathBuilder); return RESOURCE_LOADER.load(pathBuilder.toString()); } catch (IOException | SecurityException | IllegalStateException e) { diff --git a/fullstack-helm-client/src/main/java/module-info.java b/fullstack-helm-client/src/main/java/module-info.java index 51d202231..6e97afa97 100644 --- a/fullstack-helm-client/src/main/java/module-info.java +++ b/fullstack-helm-client/src/main/java/module-info.java @@ -3,6 +3,7 @@ exports com.hedera.fullstack.helm.client.model; exports com.hedera.fullstack.helm.client.model.chart; exports com.hedera.fullstack.helm.client.model.install; + exports com.hedera.fullstack.helm.client.model.test; exports com.hedera.fullstack.helm.client.execution; exports com.hedera.fullstack.helm.client.proxy.request.chart to com.hedera.fullstack.helm.client.test; diff --git a/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/HelmClientTest.java b/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/HelmClientTest.java index c634e3cbf..b0023216e 100644 --- a/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/HelmClientTest.java +++ b/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/HelmClientTest.java @@ -26,6 +26,7 @@ import com.hedera.fullstack.helm.client.model.Repository; import com.hedera.fullstack.helm.client.model.chart.Release; import com.hedera.fullstack.helm.client.model.install.InstallChartOptions; +import com.hedera.fullstack.helm.client.model.test.TestChartOptions; import com.jcovalent.junit.logging.JCovalentLoggingSupport; import com.jcovalent.junit.logging.LogEntry; import com.jcovalent.junit.logging.LogEntryBuilder; @@ -54,7 +55,11 @@ class HelmClientTest { private static final String HAPROXY_RELEASE_NAME = "haproxy-release"; - private static HelmClient defaultClient; + private static final Repository INCUBATOR_REPOSITORY = + new Repository("incubator", "https://charts.helm.sh/incubator"); + + private static final Repository JETSTACK_REPOSITORY = new Repository("jetstack", "https://charts.jetstack.io"); + private static HelmClient helmClient; private static final int INSTALL_TIMEOUT = 10; private static final List EXPECTED_LOG_ENTRIES = List.of( @@ -83,8 +88,9 @@ private record ChartInstallOptionsTestParameters(InstallChartOptions options, Li @BeforeAll static void beforeAll() { - defaultClient = HelmClient.defaultClient(); - assertThat(defaultClient).isNotNull(); + helmClient = + HelmClient.builder().defaultNamespace("helm-client-test-ns").build(); + assertThat(helmClient).isNotNull(); } void removeRepoIfPresent(HelmClient client, Repository repo) { @@ -94,10 +100,17 @@ void removeRepoIfPresent(HelmClient client, Repository repo) { } } + void addRepoIfMissing(HelmClient client, Repository repo) { + final List repositories = client.listRepositories(); + if (!repositories.contains(repo)) { + client.addRepository(repo); + } + } + @Test @DisplayName("Version Command Executes Successfully") void testVersionCommand() { - final SemanticVersion helmVersion = defaultClient.version(); + final SemanticVersion helmVersion = helmClient.version(); assertThat(helmVersion).isNotNull().isNotEqualTo(SemanticVersion.ZERO); assertThat(helmVersion.major()).isGreaterThanOrEqualTo(3); @@ -108,27 +121,27 @@ void testVersionCommand() { @Test @DisplayName("Repository List Executes Successfully") void testRepositoryListCommand() { - final List repositories = defaultClient.listRepositories(); + final List repositories = helmClient.listRepositories(); assertThat(repositories).isNotNull(); } @Test @DisplayName("Repository Add Executes Successfully") void testRepositoryAddCommand() { - final int originalRepoListSize = defaultClient.listRepositories().size(); - removeRepoIfPresent(defaultClient, INGRESS_REPOSITORY); + final int originalRepoListSize = helmClient.listRepositories().size(); + removeRepoIfPresent(helmClient, INCUBATOR_REPOSITORY); try { - assertThatNoException().isThrownBy(() -> defaultClient.addRepository(INGRESS_REPOSITORY)); - final List repositories = defaultClient.listRepositories(); + assertThatNoException().isThrownBy(() -> helmClient.addRepository(INCUBATOR_REPOSITORY)); + final List repositories = helmClient.listRepositories(); assertThat(repositories) .isNotNull() .isNotEmpty() - .contains(INGRESS_REPOSITORY) + .contains(INCUBATOR_REPOSITORY) .hasSize(originalRepoListSize + 1); } finally { - assertThatNoException().isThrownBy(() -> defaultClient.removeRepository(INGRESS_REPOSITORY)); - final List repositories = defaultClient.listRepositories(); + assertThatNoException().isThrownBy(() -> helmClient.removeRepository(INCUBATOR_REPOSITORY)); + final List repositories = helmClient.listRepositories(); assertThat(repositories).isNotNull().hasSize(originalRepoListSize); } } @@ -136,19 +149,19 @@ void testRepositoryAddCommand() { @Test @DisplayName("Repository Remove Executes With Error") void testRepositoryRemoveCommand_WithError(final LoggingOutput loggingOutput) { - removeRepoIfPresent(defaultClient, INGRESS_REPOSITORY); + removeRepoIfPresent(helmClient, JETSTACK_REPOSITORY); - int existingRepoCount = defaultClient.listRepositories().size(); + int existingRepoCount = helmClient.listRepositories().size(); final String expectedMessage; if (existingRepoCount == 0) { expectedMessage = "Error: no repositories configured"; } else { - expectedMessage = String.format("Error: no repo named \"%s\" found", INGRESS_REPOSITORY.name()); + expectedMessage = String.format("Error: no repo named \"%s\" found", JETSTACK_REPOSITORY.name()); } assertThatException() - .isThrownBy(() -> defaultClient.removeRepository(INGRESS_REPOSITORY)) + .isThrownBy(() -> helmClient.removeRepository(JETSTACK_REPOSITORY)) .withStackTraceContaining(expectedMessage); LoggingOutputAssert.assertThat(loggingOutput) .hasAtLeastOneEntry(List.of( @@ -166,19 +179,17 @@ void testRepositoryRemoveCommand_WithError(final LoggingOutput loggingOutput) { @DisplayName("Install Chart Executes Successfully") @Timeout(INSTALL_TIMEOUT) void testInstallChartCommand(final LoggingOutput loggingOutput) { - removeRepoIfPresent(defaultClient, HAPROXYTECH_REPOSITORY); + addRepoIfMissing(helmClient, HAPROXYTECH_REPOSITORY); try { - assertThatNoException().isThrownBy(() -> defaultClient.addRepository(HAPROXYTECH_REPOSITORY)); - suppressExceptions(() -> defaultClient.uninstallChart(HAPROXY_RELEASE_NAME)); - Release release = defaultClient.installChart(HAPROXY_RELEASE_NAME, HAPROXY_CHART); + suppressExceptions(() -> helmClient.uninstallChart(HAPROXY_RELEASE_NAME)); + Release release = helmClient.installChart(HAPROXY_RELEASE_NAME, HAPROXY_CHART); assertThat(release).isNotNull(); assertThat(release.name()).isEqualTo(HAPROXY_RELEASE_NAME); assertThat(release.info().description()).isEqualTo("Install complete"); assertThat(release.info().status()).isEqualTo("deployed"); } finally { - suppressExceptions(() -> defaultClient.uninstallChart(HAPROXY_RELEASE_NAME)); - suppressExceptions(() -> defaultClient.removeRepository(HAPROXYTECH_REPOSITORY)); + suppressExceptions(() -> helmClient.uninstallChart(HAPROXY_RELEASE_NAME)); } LoggingOutputAssert.assertThat(loggingOutput).hasAtLeastOneEntry(EXPECTED_LOG_ENTRIES); } @@ -186,16 +197,14 @@ void testInstallChartCommand(final LoggingOutput loggingOutput) { private static void testChartInstallWithCleanup( InstallChartOptions options, List expectedLogEntries, final LoggingOutput loggingOutput) { try { - assertThatNoException().isThrownBy(() -> defaultClient.addRepository(HAPROXYTECH_REPOSITORY)); - suppressExceptions(() -> defaultClient.uninstallChart(HAPROXY_RELEASE_NAME)); - Release release = defaultClient.installChart(HAPROXY_RELEASE_NAME, HAPROXY_CHART, options); + suppressExceptions(() -> helmClient.uninstallChart(HAPROXY_RELEASE_NAME)); + Release release = helmClient.installChart(HAPROXY_RELEASE_NAME, HAPROXY_CHART, options); assertThat(release).isNotNull(); assertThat(release.name()).isEqualTo(HAPROXY_RELEASE_NAME); assertThat(release.info().description()).isEqualTo("Install complete"); assertThat(release.info().status()).isEqualTo("deployed"); } finally { - suppressExceptions(() -> defaultClient.uninstallChart(HAPROXY_RELEASE_NAME)); - suppressExceptions(() -> defaultClient.removeRepository(HAPROXYTECH_REPOSITORY)); + suppressExceptions(() -> helmClient.uninstallChart(HAPROXY_RELEASE_NAME)); } LoggingOutputAssert.assertThat(loggingOutput).hasAtLeastOneEntry(expectedLogEntries); } @@ -205,7 +214,7 @@ private static void testChartInstallWithCleanup( @MethodSource @DisplayName("Parameterized Chart Installation with Options Executes Successfully") void testChartInstallOptions(ChartInstallOptionsTestParameters parameters, final LoggingOutput loggingOutput) { - removeRepoIfPresent(defaultClient, HAPROXYTECH_REPOSITORY); + addRepoIfMissing(helmClient, HAPROXYTECH_REPOSITORY); testChartInstallWithCleanup(parameters.options(), parameters.expectedLogEntries(), loggingOutput); } @@ -329,11 +338,25 @@ static Stream> testChartInstallOptions( @DisplayName("Install Chart with Provenance Validation") @Disabled("Provenance validation is not supported in our unit tests due to lack of signed charts.") void testInstallChartWithProvenanceValidation(final LoggingOutput loggingOutput) { - removeRepoIfPresent(defaultClient, HAPROXYTECH_REPOSITORY); + addRepoIfMissing(helmClient, HAPROXYTECH_REPOSITORY); final InstallChartOptions options = InstallChartOptions.builder().createNamespace(true).verify(true).build(); testChartInstallWithCleanup(options, EXPECTED_LOG_ENTRIES, loggingOutput); } + + @Test + @DisplayName("Helm Test subcommand with options") + void testTestChartWithOptions() { + addRepoIfMissing(helmClient, HAPROXYTECH_REPOSITORY); + final TestChartOptions options = + TestChartOptions.builder().timeout("60s").filter("haproxy").build(); + suppressExceptions(() -> helmClient.installChart(HAPROXY_RELEASE_NAME, HAPROXY_CHART)); + try { + helmClient.testChart(HAPROXY_RELEASE_NAME, options); + } finally { + suppressExceptions(() -> helmClient.uninstallChart(HAPROXY_RELEASE_NAME)); + } + } } diff --git a/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/execution/HelmExecutionBuilderTest.java b/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/execution/HelmExecutionBuilderTest.java new file mode 100644 index 000000000..ee443fc5e --- /dev/null +++ b/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/execution/HelmExecutionBuilderTest.java @@ -0,0 +1,59 @@ +/* + * 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.helm.client.test.execution; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.hedera.fullstack.helm.client.execution.HelmExecutionBuilder; +import java.io.File; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class HelmExecutionBuilderTest { + @Test + @DisplayName("Test optionsWithMultipleValues null checks") + void testOptionsWithMultipleValuesNullChecks() { + HelmExecutionBuilder builder = new HelmExecutionBuilder(new File(".").toPath()); + assertThrows(NullPointerException.class, () -> { + builder.optionsWithMultipleValues(null, null); + }); + assertThrows(NullPointerException.class, () -> { + builder.optionsWithMultipleValues("test string", null); + }); + } + + @Test + @DisplayName("Test environmentVariable null checks") + void testEnvironmentVariableNullChecks() { + HelmExecutionBuilder builder = new HelmExecutionBuilder(new File(".").toPath()); + assertThrows(NullPointerException.class, () -> { + builder.environmentVariable(null, null); + }); + assertThrows(NullPointerException.class, () -> { + builder.environmentVariable("test string", null); + }); + } + + @Test + @DisplayName("Test workingDirectory null checks") + void testWorkingDirectoryNullChecks() { + HelmExecutionBuilder builder = new HelmExecutionBuilder(new File(".").toPath()); + assertThrows(NullPointerException.class, () -> { + builder.workingDirectory(null); + }); + } +} diff --git a/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/execution/HelmExecutionTest.java b/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/execution/HelmExecutionTest.java index 53f7d74f3..a11bddd80 100644 --- a/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/execution/HelmExecutionTest.java +++ b/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/execution/HelmExecutionTest.java @@ -29,6 +29,7 @@ import com.jcovalent.junit.logging.LogEntryBuilder; import com.jcovalent.junit.logging.LoggingOutput; import com.jcovalent.junit.logging.assertj.LoggingOutputAssert; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.time.Duration; @@ -138,4 +139,31 @@ void testResponseAsWarnMessage(final LoggingOutput loggingOutput) throws Interru .message("ResponseAs exiting with exitCode: 0") .build())); } + + @Test + @DisplayName("Test response as throws HelmExecutionException with standard error and standard out") + void testResponseAsThrowsHelmExecutionException() throws InterruptedException, IOException { + doReturn(inputStreamMock).when(processMock).getInputStream(); + doReturn(inputStreamMock).when(processMock).getErrorStream(); + final HelmExecution helmExecution = Mockito.spy(new HelmExecution(processMock)); + final Duration timeout = Duration.ofSeconds(1); + doReturn(1).when(helmExecution).exitCode(); + doReturn(true).when(helmExecution).waitFor(any(Duration.class)); + String standardOutputMessage = "standardOutput Message"; + doReturn(new ByteArrayInputStream(standardOutputMessage.getBytes())) + .when(helmExecution) + .standardOutput(); + String standardErrorMessage = "standardError Message"; + doReturn(new ByteArrayInputStream(standardErrorMessage.getBytes())) + .when(helmExecution) + .standardError(); + + HelmExecutionException exception = assertThrows(HelmExecutionException.class, () -> { + helmExecution.responseAs(Repository.class, timeout); + }); + + assertThat(exception.getMessage()).contains("Execution of the Helm command failed with exit code: 1"); + assertThat(exception.getStdOut()).contains(standardOutputMessage); + assertThat(exception.getStdErr()).contains(standardErrorMessage); + } } diff --git a/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/model/InstallChartOptionsBuilderTest.java b/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/model/InstallChartOptionsBuilderTest.java index 0589985c2..09442bb2c 100644 --- a/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/model/InstallChartOptionsBuilderTest.java +++ b/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/model/InstallChartOptionsBuilderTest.java @@ -17,12 +17,24 @@ package com.hedera.fullstack.helm.client.test.model; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import com.hedera.fullstack.helm.client.execution.HelmExecutionBuilder; import com.hedera.fullstack.helm.client.model.install.InstallChartOptions; +import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +@ExtendWith(MockitoExtension.class) class InstallChartOptionsBuilderTest { + @Mock + private HelmExecutionBuilder builderMock; @Test @DisplayName("Test InstallChartOptionsBuilder") @@ -37,10 +49,11 @@ void testInstallChartOptionsBuilder() { .passCredentials(true) .password("password") .repo("repo") + .set(List.of("set", "livenessProbe.exec.command=[cat,docroot/CHANGELOG.txt]")) .skipCrds(true) .timeout("timeout") .username("username") - .values("values") + .values(List.of("values1", "values2")) .verify(true) .version("version") .waitFor(true) @@ -55,12 +68,19 @@ void testInstallChartOptionsBuilder() { assertTrue(options.passCredentials()); assertEquals("password", options.password()); assertEquals("repo", options.repo()); + assertTrue(options.set().stream().anyMatch("livenessProbe.exec.command=[cat,docroot/CHANGELOG.txt]"::equals)); + assertTrue(options.set().stream().anyMatch("set"::equals)); assertTrue(options.skipCrds()); assertEquals("timeout", options.timeout()); assertEquals("username", options.username()); - assertEquals("values", options.values()); + assertTrue(options.values().stream().anyMatch("values1"::equals)); + assertTrue(options.values().stream().anyMatch("values2"::equals)); assertTrue(options.verify()); assertEquals("version", options.version()); assertTrue(options.waitFor()); + + options.apply(builderMock); + + verify(builderMock, times(2)).optionsWithMultipleValues(anyString(), anyList()); } } diff --git a/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/model/TestChartOptionsBuilderTest.java b/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/model/TestChartOptionsBuilderTest.java new file mode 100644 index 000000000..350d8d5c6 --- /dev/null +++ b/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/model/TestChartOptionsBuilderTest.java @@ -0,0 +1,51 @@ +/* + * 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.helm.client.test.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.hedera.fullstack.helm.client.execution.HelmExecutionBuilder; +import com.hedera.fullstack.helm.client.model.test.TestChartOptions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class TestChartOptionsBuilderTest { + @Mock + private HelmExecutionBuilder builderMock; + + @Test + @DisplayName("Test TestChartOptionsBuilder") + void testTestChartOptionsBuilder() { + TestChartOptions options = + TestChartOptions.builder().filter("filter").timeout("timeout").build(); + assertNotNull(options); + assertEquals("timeout", options.timeout()); + assertEquals("filter", options.filter()); + + options.apply(builderMock); + + verify(builderMock, times(2)).argument(anyString(), anyString()); + } +} diff --git a/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/proxy/request/chart/ChartInstallRequestTest.java b/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/proxy/request/chart/ChartInstallRequestTest.java index a25a464cb..2b176a5f8 100644 --- a/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/proxy/request/chart/ChartInstallRequestTest.java +++ b/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/proxy/request/chart/ChartInstallRequestTest.java @@ -17,15 +17,32 @@ package com.hedera.fullstack.helm.client.test.proxy.request.chart; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import com.hedera.fullstack.helm.client.execution.HelmExecutionBuilder; import com.hedera.fullstack.helm.client.model.Chart; import com.hedera.fullstack.helm.client.model.install.InstallChartOptions; import com.hedera.fullstack.helm.client.proxy.request.chart.ChartInstallRequest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +@ExtendWith(MockitoExtension.class) class ChartInstallRequestTest { + @Mock + InstallChartOptions installChartOptionsMock; + + @Mock + Chart chartMock; + + @Mock + HelmExecutionBuilder helmExecutionBuilderMock; + @Test @DisplayName("Test ChartInstallRequest Chart constructor") void testChartInstallRequestChartConstructor() { @@ -44,4 +61,48 @@ void testChartInstallRequestChartConstructor() { .isEqualTo(opts) .isNotEqualTo(InstallChartOptions.defaults()); } + + @Test + @DisplayName("Test ChartInstallRequest apply with unqualified chart") + void testChartInstallRequestApplyUnqualifiedChart() { + final ChartInstallRequest chartInstallRequest = + new ChartInstallRequest("mocked", chartMock, installChartOptionsMock); + assertThat(chartInstallRequest).isNotNull(); + assertThat(chartInstallRequest.chart()).isNotNull().isEqualTo(chartMock); + assertThat(chartInstallRequest.releaseName()).isEqualTo("mocked"); + assertThat(chartInstallRequest.options()).isNotNull().isEqualTo(installChartOptionsMock); + + when(installChartOptionsMock.repo()).thenReturn("mockedRepo"); + when(chartMock.unqualified()).thenReturn("mockedUnqualified"); + when(helmExecutionBuilderMock.positional("mocked")).thenReturn(helmExecutionBuilderMock); + when(helmExecutionBuilderMock.positional("mockedUnqualified")).thenReturn(helmExecutionBuilderMock); + chartInstallRequest.apply(helmExecutionBuilderMock); + verify(helmExecutionBuilderMock, times(1)).subcommands("install"); + verify(installChartOptionsMock, times(1)).apply(helmExecutionBuilderMock); + verify(installChartOptionsMock, times(2)).repo(); + verify(chartMock, times(1)).unqualified(); + verify(helmExecutionBuilderMock, times(2)).positional(anyString()); + } + + @Test + @DisplayName("Test ChartInstallRequest apply with qualified chart") + void testChartInstallRequestApplyQualifiedChart() { + final ChartInstallRequest chartInstallRequest = + new ChartInstallRequest("mocked", chartMock, installChartOptionsMock); + assertThat(chartInstallRequest).isNotNull(); + assertThat(chartInstallRequest.chart()).isNotNull().isEqualTo(chartMock); + assertThat(chartInstallRequest.releaseName()).isEqualTo("mocked"); + assertThat(chartInstallRequest.options()).isNotNull().isEqualTo(installChartOptionsMock); + + when(installChartOptionsMock.repo()).thenReturn(null); + when(chartMock.qualified()).thenReturn("mockedQualified"); + when(helmExecutionBuilderMock.positional("mocked")).thenReturn(helmExecutionBuilderMock); + when(helmExecutionBuilderMock.positional("mockedQualified")).thenReturn(helmExecutionBuilderMock); + chartInstallRequest.apply(helmExecutionBuilderMock); + verify(helmExecutionBuilderMock, times(1)).subcommands("install"); + verify(installChartOptionsMock, times(1)).apply(helmExecutionBuilderMock); + verify(installChartOptionsMock, times(1)).repo(); + verify(chartMock, times(1)).qualified(); + verify(helmExecutionBuilderMock, times(2)).positional(anyString()); + } } diff --git a/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/proxy/request/chart/ChartTestRequestTest.java b/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/proxy/request/chart/ChartTestRequestTest.java new file mode 100644 index 000000000..ff17dbb6c --- /dev/null +++ b/fullstack-helm-client/src/test/java/com/hedera/fullstack/helm/client/test/proxy/request/chart/ChartTestRequestTest.java @@ -0,0 +1,46 @@ +/* + * 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.helm.client.test.proxy.request.chart; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.hedera.fullstack.helm.client.model.test.TestChartOptions; +import com.hedera.fullstack.helm.client.proxy.request.chart.ChartTestRequest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ChartTestRequestTest { + @Test + @DisplayName("Test ChartTestRequest Chart constructor") + void testChartTestRequestChartConstructor() { + final ChartTestRequest chartTestRequest = new ChartTestRequest("apache"); + assertThat(chartTestRequest.options()).isNotNull().isEqualTo(TestChartOptions.defaults()); + assertThat(chartTestRequest.releaseName()).isEqualTo("apache"); + + final TestChartOptions opts = + TestChartOptions.builder().timeout("9m0s").filter("filter").build(); + final ChartTestRequest nonDefaultOptRequest = new ChartTestRequest("apache", opts); + + assertThat(nonDefaultOptRequest.options()) + .isNotNull() + .isEqualTo(opts) + .isNotEqualTo(TestChartOptions.defaults()); + } +} diff --git a/fullstack-service-locator/src/main/java/com/hedera/fullstack/service/locator/api/ArtifactLoader.java b/fullstack-service-locator/src/main/java/com/hedera/fullstack/service/locator/api/ArtifactLoader.java index 1380a3d8f..7c99984e7 100644 --- a/fullstack-service-locator/src/main/java/com/hedera/fullstack/service/locator/api/ArtifactLoader.java +++ b/fullstack-service-locator/src/main/java/com/hedera/fullstack/service/locator/api/ArtifactLoader.java @@ -240,13 +240,14 @@ private void identifyArtifacts(final boolean recursive) { .map(Path::toAbsolutePath) .forEach(this::addArtifact); } catch (final IOException e) { - LOGGER.atWarn() - .setCause(e) - .log("Failed to walk directory, skipping artifact identification [ path = '{}' ]", current); + LOGGER.warn( + String.format( + "Failed to walk directory, skipping artifact identification [ path = '%s' ]", + current), + e); } } else { - LOGGER.atWarn() - .log("Skipping artifact identification, file is not a JAR archive [ path = '{}' ]", current); + LOGGER.warn("Skipping artifact identification, file is not a JAR archive [ path = '{}' ]", current); } } } @@ -266,12 +267,11 @@ private void addArtifact(final Path artifact) { classPath.add(artifact); } } catch (final IOException e) { - LOGGER.atWarn() - .setCause(e) - .log( - "Failed to identify artifact, an I/O error occurred [ fileName = '{}', path = '{}' ]", - artifact.getFileName(), - artifact); + LOGGER.warn( + String.format( + "Failed to identify artifact, an I/O error occurred [ fileName = '%s', path = '%s' ]", + artifact.getFileName(), artifact), + e); } } @@ -308,11 +308,11 @@ private void loadClassPath() { try { return uri.toURL(); } catch (final MalformedURLException e) { - LOGGER.atWarn() - .setCause(e) - .log( - "Failed to convert path to URL, unable to load class path entry [ path = '{}' ]", - uri.getPath()); + LOGGER.warn( + String.format( + "Failed to convert path to URL, unable to load class path entry [ path = '%s' ]", + uri.getPath()), + e); return null; } }) @@ -331,7 +331,7 @@ private void loadModules(final ModuleLayer parentLayer) { Objects.requireNonNull(parentLayer, "parentLayer must not be null"); if (modulePath.isEmpty()) { - LOGGER.atDebug().log("No module path entries found, skipping module layer creation"); + LOGGER.debug("No module path entries found, skipping module layer creation"); return; } @@ -341,10 +341,10 @@ private void loadModules(final ModuleLayer parentLayer) { parentLayer.configuration().resolveAndBind(finder, ModuleFinder.of(), Collections.emptySet()); moduleLayer = parentLayer.defineModulesWithOneLoader(cfg, classLoader); } catch (LayerInstantiationException | SecurityException e) { - LOGGER.atError().setCause(e).log("Failed to instantiate module layer, unable to load module path entries"); + LOGGER.error("Failed to instantiate module layer, unable to load module path entries", e); throw new ArtifactLoadingException(e); } catch (FindException | ResolutionException e) { - LOGGER.atError().setCause(e).log("Failed to resolve modules, unable to load module path entries"); + LOGGER.error("Failed to resolve modules, unable to load module path entries", e); throw new ArtifactLoadingException(e); } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 07befff7a..bdaa1997b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,7 +16,10 @@ pluginManagement { includeBuild("build-logic") } -plugins { id("com.gradle.enterprise").version("3.13.2") } +plugins { + id("com.gradle.enterprise").version("3.14.1") + id("com.hedera.fullstack.settings") +} rootProject.name = "full-stack-testing" @@ -45,7 +48,8 @@ include(":fullstack-datasource-api") include(":fullstack-datasource-core") -include(":fullstack-examples") +// TODO: re-enable once we have a way to run the *-examples without IntelliJ and Sonar issues +// includeBuild("fullstack-examples") include(":fullstack-gradle-plugin")