diff --git a/.github/workflows/zxc-compile-code.yaml b/.github/workflows/zxc-compile-code.yaml index 71e51e372..cce9070b1 100644 --- a/.github/workflows/zxc-compile-code.yaml +++ b/.github/workflows/zxc-compile-code.yaml @@ -102,7 +102,7 @@ jobs: uses: helm/kind-action@dda0770415bac9fc20092cacbc54aa298604d140 # v1.8.0 if: ${{ inputs.enable-unit-tests && !cancelled() }} with: - cluster_name: fst + config: dev/dev-cluster.yaml version: v0.19.0 verbosity: 3 wait: 120s @@ -125,6 +125,7 @@ jobs: run: | kubectl config get-contexts kubectl get crd + kubectl get node --show-labels # This step is currently required because the Hedera Services artifacts are not publicly accessible. # May be removed once the artifacts are publicly accessible. @@ -162,6 +163,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 +179,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 +196,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/build.gradle.kts b/build-logic/project-plugins/build.gradle.kts index 630338539..15c36518d 100644 --- a/build-logic/project-plugins/build.gradle.kts +++ b/build-logic/project-plugins/build.gradle.kts @@ -12,7 +12,7 @@ dependencies { implementation("com.autonomousapps:dependency-analysis-gradle-plugin:1.20.0") implementation("org.gradlex:extra-java-module-info:1.4") implementation("org.gradlex:java-ecosystem-capabilities:1.1") - implementation("org.gradlex:java-module-dependencies:1.3") + implementation("org.gradlex:java-module-dependencies:1.4.1") implementation("com.diffplug.spotless:spotless-plugin-gradle:6.18.0") implementation("org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.0.0.2929") diff --git a/build-logic/project-plugins/src/main/kotlin/Utils.kt b/build-logic/project-plugins/src/main/kotlin/Utils.kt index 1fcc3c6b1..4d367c0f1 100644 --- a/build-logic/project-plugins/src/main/kotlin/Utils.kt +++ b/build-logic/project-plugins/src/main/kotlin/Utils.kt @@ -39,6 +39,30 @@ class Utils { updateStringInFile(manifestFile, "appVersion:", "appVersion: \"${newVersion}\"") } + @JvmStatic + fun updateHelmChartVersion(project: Project, newVersion: SemVer) { + updateHelmCharts(project) {chart -> + updateHelmChartVersion(project, chart.name, newVersion) + } + } + + @JvmStatic + fun updateHelmChartAppVersion(project: Project, newVersion: SemVer) { + updateHelmCharts(project) {chart -> + updateHelmChartAppVersion(project, chart.name, newVersion) + } + } + + @JvmStatic + fun updateHelmCharts(project: Project, fn: (File) -> Unit) { + val chartDir = File(project.rootProject.projectDir, "charts") + chartDir.listFiles()?.forEach { chart -> + if (chart.isDirectory && File(chart, "Chart.yaml").exists()) { + fn(chart) + } + } + } + private fun updateStringInFile(file: File, startsWith: String, newString: String, ignoreLeadingSpace: Boolean = true) { var lines: List = mutableListOf() diff --git a/build-logic/project-plugins/src/main/kotlin/com.hedera.fullstack.aggregate-reports.gradle.kts b/build-logic/project-plugins/src/main/kotlin/com.hedera.fullstack.aggregate-reports.gradle.kts index d87ef35a2..4ceffc4c0 100644 --- a/build-logic/project-plugins/src/main/kotlin/com.hedera.fullstack.aggregate-reports.gradle.kts +++ b/build-logic/project-plugins/src/main/kotlin/com.hedera.fullstack.aggregate-reports.gradle.kts @@ -50,8 +50,6 @@ tasks.register("githubVersionSummary") { } } -val HEDERA_NETWORK_CHART = "hedera-network" - tasks.register("versionAsSpecified") { group = "versioning" doLast { @@ -59,8 +57,8 @@ tasks.register("versionAsSpecified") { ?: throw IllegalArgumentException("No newVersion property provided! Please add the parameter -PnewVersion= when running this task.") val newVer = SemVer.parse(verStr) - Utils.updateHelmChartVersion(project, HEDERA_NETWORK_CHART, newVer) - Utils.updateHelmChartAppVersion(project, HEDERA_NETWORK_CHART, newVer) + Utils.updateHelmChartVersion(project, newVer) + Utils.updateHelmChartAppVersion(project, newVer) Utils.updateVersion(project, newVer) } } @@ -71,8 +69,8 @@ tasks.register("versionAsSnapshot") { val currVer = SemVer.parse(project.version.toString()) val newVer = SemVer(currVer.major, currVer.minor, currVer.patch, "SNAPSHOT") - Utils.updateHelmChartVersion(project, HEDERA_NETWORK_CHART, newVer) - Utils.updateHelmChartAppVersion(project, HEDERA_NETWORK_CHART, newVer) + Utils.updateHelmChartVersion(project, newVer) + Utils.updateHelmChartAppVersion(project, newVer) Utils.updateVersion(project, newVer) } } 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/charts/fullstack-cluster-setup/.helmignore b/charts/fullstack-cluster-setup/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/charts/fullstack-cluster-setup/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/fullstack-cluster-setup/Chart.yaml b/charts/fullstack-cluster-setup/Chart.yaml new file mode 100644 index 000000000..7a8f796e9 --- /dev/null +++ b/charts/fullstack-cluster-setup/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: fullstack-cluster-setup +description: A Helm chart to setup shared resources for fullstack-testing + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.8.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.8.0" diff --git a/charts/fullstack-cluster-setup/templates/NOTES.txt b/charts/fullstack-cluster-setup/templates/NOTES.txt new file mode 100644 index 000000000..e69de29bb diff --git a/charts/fullstack-cluster-setup/templates/_helpers.tpl b/charts/fullstack-cluster-setup/templates/_helpers.tpl new file mode 100644 index 000000000..fb8bd8fe3 --- /dev/null +++ b/charts/fullstack-cluster-setup/templates/_helpers.tpl @@ -0,0 +1,51 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "fullstack-cluster-setup.name" -}} +{{- default .Chart.Name .Values.global.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "fullstack-cluster-setup.fullname" -}} +{{- if .Values.global.fullnameOverride }} +{{- .Values.global.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.global.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "fullstack-cluster-setup.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "fullstack-cluster-setup.labels" -}} +helm.sh/chart: {{ include "fullstack-cluster-setup.chart" . }} +{{ include "fullstack-cluster-setup.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "fullstack-cluster-setup.selectorLabels" -}} +app.kubernetes.io/name: {{ include "fullstack-cluster-setup.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/charts/fullstack-cluster-setup/templates/gateway-api/fst-gateway.yaml b/charts/fullstack-cluster-setup/templates/gateway-api/fst-gateway.yaml new file mode 100644 index 000000000..f869dd944 --- /dev/null +++ b/charts/fullstack-cluster-setup/templates/gateway-api/fst-gateway.yaml @@ -0,0 +1,9 @@ +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: GatewayClass +metadata: + name: fst-gateway-class + labels: + fullstack.hedera.com/type: gateway-class +spec: + controllerName: "gateway.envoyproxy.io/gatewayclass-controller" + #controllerName: "haproxy-ingress.github.io/controller" diff --git a/charts/fullstack-cluster-setup/templates/rbac/pod-monitor-role.yaml b/charts/fullstack-cluster-setup/templates/rbac/pod-monitor-role.yaml new file mode 100644 index 000000000..a2328539e --- /dev/null +++ b/charts/fullstack-cluster-setup/templates/rbac/pod-monitor-role.yaml @@ -0,0 +1,31 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: pod-monitor-role + labels: + fullstack.hedera.com/type: cluster-role +rules: + - apiGroups: [ "" ] + resources: + - pods + - services + - clusterroles + - pods/log + - secrets + verbs: + - get + - list + - apiGroups: [ "" ] + resources: + - pods/exec + verbs: + - create + - apiGroups: [ "gateway.networking.k8s.io" ] + resources: + - gatewayclasses + - gateways + - httproutes + - tcproutes + verbs: + - get + - list \ No newline at end of file diff --git a/charts/fullstack-cluster-setup/values.yaml b/charts/fullstack-cluster-setup/values.yaml new file mode 100644 index 000000000..d22bdbb64 --- /dev/null +++ b/charts/fullstack-cluster-setup/values.yaml @@ -0,0 +1,8 @@ +# Default values for fullstack-cluster-setup. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +global: + namespaceOverride: "" + nameOverride: "" + fullnameOverride: "" \ No newline at end of file diff --git a/charts/hedera-network/Chart.lock b/charts/hedera-network/Chart.lock index 21ec42f86..b32b63749 100644 --- a/charts/hedera-network/Chart.lock +++ b/charts/hedera-network/Chart.lock @@ -8,5 +8,5 @@ dependencies: - name: tenant repository: https://operator.min.io/ version: 5.0.7 -digest: sha256:cf355b295abceb5814ef57d3e146ec9d4e8db7365a700079d683bd5f766ad374 -generated: "2023-09-16T13:47:19.087992+10:00" +digest: sha256:5dbc1a4af8f2b057dbd7730b6308e1a2954f3f95f86e8484bb232e64ed12e923 +generated: "2023-10-04T15:47:44.747012+11:00" diff --git a/charts/hedera-network/Chart.yaml b/charts/hedera-network/Chart.yaml index 02fb107a1..a55d561ba 100644 --- a/charts/hedera-network/Chart.yaml +++ b/charts/hedera-network/Chart.yaml @@ -24,18 +24,20 @@ version: 0.8.0 appVersion: "0.8.0" # This is range of versions of Kubernetes server that is supported by this chart. -kubeVersion: ">=1.25.0" +# Note we need to use -0 suffix to support GKE version +# Reference: https://github.com/helm/helm/issues/3810#issuecomment-379877753 +kubeVersion: ">=1.25.0-0" dependencies: - name: hedera-explorer version: 0.2.0 - condition: cloud.minio.enable + condition: hedera-explorer.enable - name: hedera-mirror alias: hedera-mirror-node version: 0.86.0 repository: https://hashgraph.github.io/hedera-mirror-node/charts - condition: cloud.minio.enable + condition: hedera-mirror-node.enable - name: tenant alias: minio-server diff --git a/charts/hedera-network/templates/gateway-api/gateway.yaml b/charts/hedera-network/templates/gateway-api/gateway.yaml index a041c1cac..4bea8d1c5 100644 --- a/charts/hedera-network/templates/gateway-api/gateway.yaml +++ b/charts/hedera-network/templates/gateway-api/gateway.yaml @@ -1,16 +1,4 @@ -{{- if $.Values.gatewayApi.gatewayClass.enable | eq "true" }} -apiVersion: gateway.networking.k8s.io/v1beta1 -kind: GatewayClass -metadata: - name: {{ $.Values.gatewayApi.gatewayClass.name }} - namespace: {{ default $.Release.Namespace $.Values.global.namespaceOverride }} - labels: - fullstack.hedera.com/type: gateway-class -spec: - controllerName: {{ $.Values.gatewayApi.gatewayClass.controllerName }} -{{- end }} {{- if $.Values.gatewayApi.gateway.enable | eq "true" }} ---- apiVersion: gateway.networking.k8s.io/v1beta1 kind: Gateway metadata: diff --git a/charts/hedera-network/templates/network-node-statefulset.yaml b/charts/hedera-network/templates/network-node-statefulset.yaml index 50f00ef4d..ac90a2b9f 100644 --- a/charts/hedera-network/templates/network-node-statefulset.yaml +++ b/charts/hedera-network/templates/network-node-statefulset.yaml @@ -17,6 +17,13 @@ metadata: namespace: {{ default $.Release.Namespace $.Values.global.namespaceOverride }} labels: app: network-{{ $node.name }} + {{- if $.Values.deployment.podLabels }} + {{- $.Values.deployment.podLabels | toYaml | nindent 4 }} + {{- end }} + {{- if $.Values.deployment.podAnnotations }} + annotations: + {{- $.Values.deployment.podAnnotations | toYaml | nindent 4 }} + {{- end }} spec: replicas: 1 serviceName: "network-{{ $node.name }}" @@ -30,6 +37,21 @@ spec: fullstack.hedera.com/type: network-node fullstack.hedera.com/node-name: {{ $node.name }} spec: + {{- if $.Values.deployment.nodeSelectors }} + nodeSelector: + {{- $.Values.deployment.nodeSelectors | toYaml | nindent 8 }} + {{- end }} + {{- if $.Values.deployment.tolerations }} + tolerations: + {{- $.Values.deployment.tolerations | toYaml | nindent 8 }} + {{- end }} + {{- if $.Values.deployment.affinity }} + affinity: + {{- $.Values.deployment.affinity | toYaml | nindent 8 }} + {{- end }} + {{- if $.Values.deployment.priorityClassName }} + priorityClassName: {{ $.Values.deployment.priorityClassName }} + {{- end }} terminationGracePeriodSeconds: {{ $.Values.terminationGracePeriodSeconds }} volumes: - name: hgcapp-storage # change me diff --git a/charts/hedera-network/templates/pdb.yaml b/charts/hedera-network/templates/pdb.yaml new file mode 100644 index 000000000..643b1f170 --- /dev/null +++ b/charts/hedera-network/templates/pdb.yaml @@ -0,0 +1,24 @@ +{{- if $.Values.deployment.podDisruptionBudget.create }} +{{ range $index, $node := $.Values.hedera.nodes }} +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: network-node-pdb-{{ $node.name }} + namespace: {{ default $.Release.Namespace $.Values.global.namespaceOverride }} + labels: + fullstack.hedera.com/type: pod-disruption-budget + fullstack.hedera.com/node-name: {{ $node.name }} +spec: + selector: + matchLabels: + fullstack.hedera.com/type: network-node + fullstack.hedera.com/node-name: {{ $node.name }} + {{- if $.Values.deployment.podDisruptionBudget.minAvailable }} + minAvailable: {{ $.Values.deployment.podDisruptionBudget.minAvailable }} + {{- end }} + {{- if $.Values.deployment.podDisruptionBudget.maxUnavailable }} + maxUnavailable: {{ $.Values.deployment.podDisruptionBudget.maxUnavailable }} + {{- end }} +{{- end }} +{{- end }} diff --git a/charts/hedera-network/templates/rbac/pod-monitor-role.yaml b/charts/hedera-network/templates/rbac/pod-monitor-role.yaml deleted file mode 100644 index dd8ccbd8e..000000000 --- a/charts/hedera-network/templates/rbac/pod-monitor-role.yaml +++ /dev/null @@ -1,42 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: pod-monitoring-role - namespace: {{ default $.Release.Namespace $.Values.global.namespaceOverride }} -rules: - - apiGroups: [ "" ] - resources: - - pods - - pods/log - - secrets - verbs: - - get - - list - - apiGroups: [ "" ] - resources: - - pods/exec - verbs: - - create - - apiGroups: [ "gateway.networking.k8s.io" ] - resources: - - gatewayclasses - - gateways - - httproutes - - tcproutes - verbs: - - get - - list ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: pod-monitoring-role-binding - namespace: {{ default $.Release.Namespace $.Values.global.namespaceOverride }} -subjects: - - kind: ServiceAccount - name: pod-monitor - namespace: {{ default $.Release.Namespace $.Values.global.namespaceOverride }} -roleRef: - kind: ClusterRole - name: pod-monitoring-role - apiGroup: rbac.authorization.k8s.io diff --git a/charts/hedera-network/templates/rbac/pod-monitor.yaml b/charts/hedera-network/templates/rbac/pod-monitor.yaml new file mode 100644 index 000000000..d6a2ec5e7 --- /dev/null +++ b/charts/hedera-network/templates/rbac/pod-monitor.yaml @@ -0,0 +1,21 @@ +{{- if $.Values.tester.deployPodMonitor | eq "true" }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: pod-monitor + namespace: {{ default $.Release.Namespace $.Values.global.namespaceOverride }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: pod-monitor-role-binding + namespace: {{ default $.Release.Namespace $.Values.global.namespaceOverride }} +subjects: + - kind: ServiceAccount + name: pod-monitor + namespace: {{ default $.Release.Namespace $.Values.global.namespaceOverride }} +roleRef: + kind: ClusterRole + name: {{ $.Values.tester.clusterRoleName }} + apiGroup: rbac.authorization.k8s.io +{{- end }} \ No newline at end of file diff --git a/charts/hedera-network/templates/rbac/service-accounts.yaml b/charts/hedera-network/templates/rbac/service-accounts.yaml deleted file mode 100644 index 0e4b26311..000000000 --- a/charts/hedera-network/templates/rbac/service-accounts.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - name: pod-monitor - namespace: {{ default $.Release.Namespace $.Values.global.namespaceOverride }} diff --git a/charts/hedera-network/templates/tests/test-deployment.yaml b/charts/hedera-network/templates/tests/test-deployment.yaml index b69a5a628..18a4226c5 100644 --- a/charts/hedera-network/templates/tests/test-deployment.yaml +++ b/charts/hedera-network/templates/tests/test-deployment.yaml @@ -28,8 +28,16 @@ spec: env: - name: TESTS_DIR value: "/tests" # should be same as mountPath + - name: LOG_DIR + value: "/tmp/fullstack-testing-logs" + - name: LOG_FILE + value: "helm-test.log" - name: OUTPUT_LOG # outputs the logs from the tests value: "true" + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace command: - "/bin/bash" - "-c" diff --git a/charts/hedera-network/tests/env.sh b/charts/hedera-network/tests/env.sh index 76644eda3..050672dce 100755 --- a/charts/hedera-network/tests/env.sh +++ b/charts/hedera-network/tests/env.sh @@ -3,27 +3,35 @@ # Every script must load (source) this in the beginning # Warning: avoid making these variables readonly since it can be sourced multiple times +CUR_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" + # load .env file if it exists in order to load variables with custom values -ENV_FILE="$(dirname "${BASH_SOURCE[0]}")/.env" +ENV_FILE="${CUR_DIR}/.env" if [[ -f "${ENV_FILE}" ]]; then - export $(cat "${ENV_FILE}" | xargs) + set -a + # shellcheck source=./../temp/.env + source "${ENV_FILE}" + set +a fi - # set global env variables if not set -BATS_HOME="${BATS_HOME:-../../../dev/bats}" -TESTS_DIR="${TESTS_DIR:-.}" +BATS_HOME="${BATS_HOME:-${CUR_DIR}/../../../dev/bats}" +TESTS_DIR="${TESTS_DIR:-${CUR_DIR}}" -OUTPUT_LOG="${OUTPUT_LOG}" -LOG_DIR="${LOG_DIR:-/tmp/bats-test-logs}" -LOG_FILE="test.log" +TOTAL_NODES="${TOTAL_NODES:-3}" +USER="${USER:-changeme}" +NAMESPACE="${NAMESPACE:-fst-${USER}}" +LOG_DIR="${LOG_DIR:-${CUR_DIR}/logs}" +LOG_FILE="${LOG_FILE:-helm-test.log}" +OUTPUT_LOG="${OUTPUT_LOG:-false}" [ ! -d "${LOG_DIR}" ] && mkdir "${LOG_DIR}" -echo "" -echo "Env variables" -echo "==============================================" +echo "--------------------------Env Setup: fullstack-testing Helm Test------------------------------------------------" +echo "NAMESPACE: ${NAMESPACE}" echo "ENV_FILE: ${ENV_FILE}" echo "BATS_HOME: ${BATS_HOME}" echo "TESTS_DIR: ${TESTS_DIR}" +echo "LOG: ${LOG_DIR}/${LOG_FILE}" echo "OUTPUT_LOG: ${OUTPUT_LOG}" - +echo "-----------------------------------------------------------------------------------------------------" +echo "" diff --git a/charts/hedera-network/tests/env.template b/charts/hedera-network/tests/env.template index 3349c2874..5ed4d1fb1 100644 --- a/charts/hedera-network/tests/env.template +++ b/charts/hedera-network/tests/env.template @@ -1 +1,8 @@ +USER="${USER:-changeme}" +NAMESPACE="${NAMESPACE:-fst-${USER}}" + TOTAL_NODES=3 + +LOG_DIR="${LOG_DIR:-/tmp/fullstack-testing-logs}" +LOG_FILE="${LOG_FILE:-helm-test.log}" +OUTPUT_LOG="${OUTPUT_LOG:-false}" diff --git a/charts/hedera-network/tests/helper.sh b/charts/hedera-network/tests/helper.sh index 386195f3d..b86255dfa 100644 --- a/charts/hedera-network/tests/helper.sh +++ b/charts/hedera-network/tests/helper.sh @@ -32,7 +32,7 @@ function import { function get_pod_list() { local pattern=$1 - local resp=$(kubectl get pods -o=jsonpath='{range .items[*]}{.metadata.name}{"\n"}' | grep "${pattern}") + local resp=$(kubectl get pods -o=jsonpath='{range .items[*]}{.metadata.name}{"\n"}' -n "${NAMESPACE}" | grep "${pattern}") echo "${resp}" } @@ -82,8 +82,8 @@ function check_test_status() { function get_config_val() { local config_path=$1 - log_debug "Get config command: helm get values fst -a | yq '${config_path}'" - ret=$(helm get values fst -a | yq "${config_path}" ) + log_debug "Get config command: helm get values fst -a -n ${NAMESPACE} | yq '${config_path}'" + ret=$(helm get values fst -a -n "${NAMESPACE}" | yq "${config_path}" ) echo "${ret}" log_debug "${config_path} => ${ret}" } @@ -101,7 +101,7 @@ function is_enabled_for_node() { local config_path=$2 [[ -z "${config_path}" ]] && echo "ERROR: Config path is needed" && return "${EX_ERR}" - log_debug "Checking config '${config_path}' for node '${node_name}" + log_debug "Checking config '${config_path}' for node '${node_name} in namespace ${NAMESPACE} " local default_config_path=".defaults${config_path}" local node_config_path=".hedera.nodes[] | select(.name==\"${node_name}\") | ${config_path}" @@ -134,7 +134,7 @@ function get_sidecar_status() { [[ -z "${pod}" ]] && echo "ERROR: Pod name is needed (is_sidecar_ready)" && return "${EX_ERR}" [[ -z "${sidecar_name}" ]] && echo "ERROR: Sidecar name is needed (is_sidecar_ready)" && return "${EX_ERR}" - local sidecar_status=$(kubectl get pod "${pod}" -o jsonpath="{.status.containerStatuses[?(@.name=='${sidecar_name}')].ready}" | xargs) + local sidecar_status=$(kubectl get pod "${pod}" -o jsonpath="{.status.containerStatuses[?(@.name=='${sidecar_name}')].ready}" -n "${NAMESPACE}" | xargs) echo "${sidecar_status}" } @@ -144,9 +144,9 @@ function is_sidecar_ready() { [[ -z "${pod}" ]] && echo "ERROR: Pod name is needed (is_sidecar_ready)" && return "${EX_ERR}" [[ -z "${sidecar_name}" ]] && echo "ERROR: Sidecar name is needed (is_sidecar_ready)" && return "${EX_ERR}" - local sidecar_status=$(kubectl get pod "${pod}" -o jsonpath="{.status.containerStatuses[?(@.name=='${sidecar_name}')].ready}" | tr '[:lower:]' '[:upper:]') + local sidecar_status=$(kubectl get pod "${pod}" -o jsonpath="{.status.containerStatuses[?(@.name=='${sidecar_name}')].ready}" -n "${NAMESPACE}" | tr '[:lower:]' '[:upper:]') [ -z "${sidecar_status}" ] && sidecar_status="FALSE" - log_debug "${sidecar_name} in pod ${pod} is ready: ${sidecar_status}" + log_debug "${sidecar_name} in pod ${pod} is ready in namespace ${NAMESPACE} : ${sidecar_status}" [[ "${sidecar_status}" = "TRUE" ]] && return "${EX_OK}" return "${EX_ERR}" @@ -158,8 +158,8 @@ function has_sidecar() { [[ -z "${pod}" ]] && echo "ERROR: Pod name is needed (is_sidecar_ready)" && return "${EX_ERR}" [[ -z "${sidecar_name}" ]] && echo "ERROR: Sidecar name is needed (is_sidecar_ready)" && return "${EX_ERR}" - local sidecars=$(kubectl get pods "${pod}" -o jsonpath='{.spec.containers[*].name}') - log_debug "Sidecar list in pod ${pod}: ${sidecars}" + local sidecars=$(kubectl get pods "${pod}" -o jsonpath='{.spec.containers[*].name}' -n "${NAMESPACE}") + log_debug "Sidecar list in pod ${pod} in namespace ${NAMESPACE} : ${sidecars}" local found="FALSE" if [[ "${sidecars}" =~ ${sidecar_name} ]]; then @@ -173,10 +173,10 @@ function is_pod_ready() { local pod=$1 [[ -z "${pod}" ]] && echo "ERROR: Pod name is needed (is_pod_ready)" && return "${EX_ERR}" - local pod_status=$(kubectl get pod "${pod}" -o jsonpath="{.status.conditions[?(@.type=='Ready')].status}" | tr '[:lower:]' '[:upper:]') + local pod_status=$(kubectl get pod "${pod}" -o jsonpath="{.status.conditions[?(@.type=='Ready')].status}" -n "${NAMESPACE}" | tr '[:lower:]' '[:upper:]') [ -z "${pod_status}" ] && pod_status="FALSE" - log_debug "Pod '${pod}' is ready: ${pod_status}" + log_debug "Pod '${pod}' is ready in namespace ${NAMESPACE} : ${pod_status}" [[ "${pod_status}" = "TRUE" ]] && return "${EX_OK}" return "${EX_ERR}" @@ -190,15 +190,15 @@ function get_pod_label() { [[ -z "${pod}" ]] && echo "ERROR: Label name is needed" && return "${EX_ERR}" - log_debug "Checking for pod '${pod}'(timeout 300s)..." - $(kubectl wait --for=condition=Initialized pods "${pod}" --timeout 300s) > /dev/null 2>&1 + log_debug "Checking for pod '${pod}' in namespace ${NAMESPACE} (timeout 300s)..." + $(kubectl wait --for=condition=Initialized pods "${pod}" --timeout 300s -n "${NAMESPACE}") > /dev/null 2>&1 if [ $? = 1 ]; then log_debug "ERROR: Pod ${pod} is not available" && return "${EX_ERR}" fi - log_debug "Checking label '${label}' for pod '${pod}'" + log_debug "Checking label '${label}' for pod '${pod}' in namespace ${NAMESPACE} " local escaped_label="${label//./\\.}" - local label_val=$(kubectl get pod "${pod}" -o jsonpath="{.metadata.labels.${escaped_label}}" | xargs) + local label_val=$(kubectl get pod "${pod}" -o jsonpath="{.metadata.labels.${escaped_label}}" -n "${NAMESPACE}" | xargs) log_debug "Pod '${pod}' label '${label}': ${label_val}" echo "${label_val}" @@ -208,9 +208,9 @@ function get_pod_by_label() { local label=$1 [[ -z "${pod}" ]] && echo "ERROR: Label name is needed" && return "${EX_ERR}" - log_debug "Getting pod by label '${label}'" + log_debug "Getting pod by label '${label}' in namespace ${NAMESPACE} " local escaped_label="${label//./\\.}" - local pod_name=$(kubectl get pods -l "${label}" -o jsonpath="{.items[0].metadata.name}") + local pod_name=$(kubectl get pods -l "${label}" -o jsonpath="{.items[0].metadata.name}" -n "${NAMESPACE}") echo "${pod_name}" } @@ -221,10 +221,10 @@ function is_route_accepted() { local route_name=$2 [[ -z "${route_name}" ]] && echo "ERROR: Route name is needed" && return "${EX_ERR}" - local route_status=$(kubectl get "${route_type}" "${route_name}" -o jsonpath="{.status.parents[*].conditions[?(@.type=='Accepted')].status}" | tr '[:lower:]' '[:upper:]') + local route_status=$(kubectl get "${route_type}" "${route_name}" -o jsonpath="{.status.parents[*].conditions[?(@.type=='Accepted')].status}" -n "${NAMESPACE}" | tr '[:lower:]' '[:upper:]') [ -z "${route_status}" ] && route_status="FALSE" - log_debug "${route_type} '${route_name}' is accepted: ${route_status}" + log_debug "${route_type} '${route_name}' in namespace ${NAMESPACE} is accepted: ${route_status}" [[ "${route_status}" = "TRUE" ]] && return "${EX_OK}" return "${EX_ERR}" diff --git a/charts/hedera-network/tests/run.sh b/charts/hedera-network/tests/run.sh index 382c00699..33e55969d 100755 --- a/charts/hedera-network/tests/run.sh +++ b/charts/hedera-network/tests/run.sh @@ -1,16 +1,33 @@ #!/usr/bin/env bash -source "$(dirname "${BASH_SOURCE[0]}")/env.sh" -source "$(dirname "${BASH_SOURCE[0]}")/logging.sh" +CUR_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" +source "${CUR_DIR}/env.sh" +source "${CUR_DIR}/logging.sh" clear_log +echo "Cluster Resources" +echo "NAMESPACE: ${NAMESPACE}" +echo "=============================================================" +echo "------------------------------------------- Namespaces ----------------------------------------------------------" +kubectl get ns +kubectl config get-contexts +echo "------------------------------------------- GatewayClass ---------------------------------------------------------" +kubectl get GatewayClass +echo "------------------------------------------- ClusterRole ----------------------------------------------------------" +kubectl get ClusterRole +echo "------------------------------------------- Pods -----------------------------------------------------------------" +kubectl get pods +echo "------------------------------------------- Services -------------------------------------------------------------" +kubectl get svc +echo "------------------------------------------------------------------------------------------------------------------" + echo "" -echo "BATS directory: $BATS_HOME" +echo "File list in 'BATS_HOME': $BATS_HOME" echo "=============================================================" ls -la "${BATS_HOME}" echo "" -echo "Tests directory: $TESTS_DIR" +echo "File list in 'TEST_DIR': $TESTS_DIR" echo "=============================================================" ls -la "${TESTS_DIR}" @@ -24,8 +41,17 @@ if [[ -z "${test_file}" ]]; then else "${BATS_HOME}/bats-core/bin/bats" "${TESTS_DIR}/${test_file}" fi + readonly bats_exec_status=$? +# print test status in the log file +log_debug "Exit code: ${bats_exec_status}" +if [[ $bats_exec_status -eq 0 ]];then + log_debug "Test status: PASS" +else + log_debug "Test status: FAIL" +fi + # uncomment in order to inspect tmpdir #"${BATS_HOME}/bats-core/bin/bats" --no-tempdir-cleanup . diff --git a/charts/hedera-network/tests/test_basic_deployment.bats b/charts/hedera-network/tests/test_basic_deployment.bats index 1459a9e5c..69f0b31b0 100644 --- a/charts/hedera-network/tests/test_basic_deployment.bats +++ b/charts/hedera-network/tests/test_basic_deployment.bats @@ -10,7 +10,7 @@ setup() { log_debug "Expected total nodes: ${TOTAL_NODES}" log_debug "----------------------------------------------------------------------------" - kubectl wait --for=jsonpath='{.status.phase}'=Running pod -l fullstack.hedera.com/type=network-node --timeout=300s || return "${EX_ERR}" + kubectl wait --for=jsonpath='{.status.phase}'=Running pod -l fullstack.hedera.com/type=network-node --timeout=300s -n "${NAMESPACE}" || return "${EX_ERR}" local resp="$(get_pod_list network-node)" local nodes=(${resp}) # convert into an array @@ -53,7 +53,7 @@ setup() { # make few attempts to check systemctl status while [[ "${attempts}" -lt "${MAX_ATTEMPTS}" && "${systemctl_status}" -ne "${EX_OK}" ]]; do attempts=$((attempts + 1)) - kubectl exec "${node}" -c root-container -- systemctl status --no-pager + kubectl exec "${node}" -c root-container -n "${NAMESPACE}" -- systemctl status --no-pager systemctl_status="${?}" log_debug "Checked systemctl status in ${node} (Attempt #${attempts}/${MAX_ATTEMPTS})... >>>>> status: ${systemctl_status} <<<<<" if [[ "${systemctl_status}" -ne "${EX_OK}" ]]; then diff --git a/charts/hedera-network/tests/test_sidecar_deployment.bats b/charts/hedera-network/tests/test_sidecar_deployment.bats index 399da3d44..e45558b41 100644 --- a/charts/hedera-network/tests/test_sidecar_deployment.bats +++ b/charts/hedera-network/tests/test_sidecar_deployment.bats @@ -14,30 +14,34 @@ function run_default_sidecar_check() { local resp="$(get_pod_list network-node)" local pods=(${resp}) # convert into an array - local test_status="${PASS}" + log_debug "Network node: ${pods[*]}" + + local test_status="${FAIL}" local status_val="${EX_ERR}" - for pod in "${pods[@]}"; do - log_debug "" - log_debug "Checking pod ${pod} for sidecar ${sidecar_name}" + if [[ "${#pods[@]}" -gt 0 ]]; then + test_status="${PASS}" + for pod in "${pods[@]}"; do + log_debug "" + log_debug "Checking pod ${pod} for sidecar ${sidecar_name}" - local should_enable=$(get_config_val_upper "${enable_config_path}") - log_debug "${sidecar_name} is enabled in pod ${pod}: ${should_enable}" + local should_enable=$(get_config_val_upper "${enable_config_path}") + log_debug "${sidecar_name} is enabled in pod ${pod}: ${should_enable}" - local sidecar_exists=$(has_sidecar "${pod}" "${sidecar_name}" ) - log_debug "${sidecar_name} exists in pod ${pod}: ${sidecar_exists} " + local sidecar_exists=$(has_sidecar "${pod}" "${sidecar_name}" ) + log_debug "${sidecar_name} exists in pod ${pod}: ${sidecar_exists} " - log_debug "${should_enable} ${sidecar_exists}" - if [ "${should_enable}" = "TRUE" ] && [ "${sidecar_exists}" = "TRUE" ]; then - is_sidecar_ready "${pod}" "${sidecar_name}" || test_status="${FAIL}" - elif [[ "${should_enable}" != "${sidecar_exists}" ]]; then - test_status="${FAIL}" - fi + if [ "${should_enable}" = "TRUE" ] && [ "${sidecar_exists}" = "TRUE" ]; then + is_sidecar_ready "${pod}" "${sidecar_name}" || test_status="${FAIL}" + elif [[ "${should_enable}" != "${sidecar_exists}" ]]; then + test_status="${FAIL}" + fi - [ "${test_status}" = "FAIL" ] && break - done + [ "${test_status}" = "FAIL" ] && break + done + fi log_debug "" - log_debug "[${test_status}] ${sidecar_name} sidecar is running in all network-node pods" + log_debug "[${test_status}] ${sidecar_name} sidecar is running in all network-node pods in namespace ${NAMESPACE}" log_debug "" # assert success diff --git a/charts/hedera-network/values.yaml b/charts/hedera-network/values.yaml index a54169b2a..426cfe8d2 100644 --- a/charts/hedera-network/values.yaml +++ b/charts/hedera-network/values.yaml @@ -5,8 +5,6 @@ global: # cloud configuration cloud: - minio: - enable: true buckets: streamBucket: "fst-streams" backupBucket: "fst-backups" @@ -14,6 +12,8 @@ cloud: enable: "true" gcs: enable: "true" + minio: + enable: true # telemetry configurations telemetry: @@ -26,6 +26,8 @@ terminationGracePeriodSeconds: 10 # helm test container tester: + deployPodMonitor: "true" + clusterRoleName: "pod-monitor-role" # this is a shared cluster role for all namespaces image: registry: "ghcr.io" repository: "hashgraph/full-stack-testing/kubectl-bats" @@ -36,10 +38,7 @@ tester: # gateway-api configuration gatewayApi: gatewayClass: - name: "fst" - enable: "true" - controllerName: "gateway.envoyproxy.io/gatewayclass-controller" -# controllerName: "haproxy-ingress.github.io/controller" + name: "fst-gateway-class" # this is a shared gateway class for all namespaces gateway: name: "fst" enable: "true" @@ -65,14 +64,13 @@ gatewayApi: route: hostname: "{{ .node.name }}.fst.local" - # default settings for a single node # This default configurations can be overridden for each node in the hedera.nodes section. defaults: resources: requests: - cpu: 1 - memory: 2G + cpu: 100m + memory: 100Mi limits: cpu: 1 memory: 2G @@ -224,7 +222,9 @@ minio-server: certificate: requestAutoCert: false +# hedera mirror node configuration hedera-mirror-node: + enable: true global: namespaceOverride: "{{ tpl (.Values.global.namespaceOverride | toString) }}" # importer is a component of the hedera mirror node @@ -252,7 +252,9 @@ hedera-mirror-node: bucketName: "fst-streams" # for s3 configuration of mirror node look at uploader-mirror-secrets.yaml +# hedera explorer configuration hedera-explorer: + enable: true global: namespaceOverride: "{{ tpl (.Values.global.namespaceOverride | toString) }}" # The hedera explorer UI /api url will proxy all request to mirror node @@ -274,6 +276,39 @@ hedera-explorer: } ] +# common deployment configuration +deployment: + podAnnotations: {} + podLabels: {} + nodeSelectors: + fullstack-scheduling.io/os: linux + fullstack-scheduling.io/role: network + tolerations: + - key: "fullstack-scheduling.io/os" + operator: "Equal" + value: "linux" + effect: "NoSchedule" + - key: "fullstack-scheduling.io/role" + operator: "Equal" + value: "network" + effect: "NoSchedule" + # Specify pod affinity + # Use complete affinity spec starting with key "nodeAffinity:" + # Ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#node-affinity + affinity: {} + priorityClassName: {} + ## PodDisruptionBudget for fullstack testing pods + ## Default backend Pod Disruption Budget configuration + ## ref: https://kubernetes.io/docs/tasks/run-application/configure-pdb/ + ## @param deployment.podDisruptionBudget.create Enable Pod Disruption Budget configuration + ## @param deployment.podDisruptionBudget.minAvailable Minimum number/percentage of pods that should remain scheduled + ## @param deployment.podDisruptionBudget.maxUnavailable Maximum number/percentage of pods that should remain scheduled + ## + podDisruptionBudget: + create: true + minAvailable: 1 + maxUnavailable: "" + # hedera node configuration # Only the name of the node is required. The rest of the configuration will be inherited from `defaults` section hedera: diff --git a/dev/Makefile b/dev/Makefile index e6f5a839a..f54cfb98f 100644 --- a/dev/Makefile +++ b/dev/Makefile @@ -59,20 +59,26 @@ uninstall-chart: update-helm-dependencies: helm dependency update ../charts/hedera-network +.PHONY: deploy-shared +deploy-shared: update-helm-dependencies deploy-gateway-api deploy-prometheus-operator deploy-minio-operator-if-required + source "${SCRIPTS_DIR}/main.sh" && deploy_shared # run only after gateway-api CRDs are available + +.PHONY: destroy-shared +destroy-shared: + -$(MAKE) source "${SCRIPTS_DIR}/main.sh" && destroy_shared + -$(MAKE) undeploy-minio-operator + -$(MAKE) destroy-prometheus-operator + -$(MAKE) destroy-gateway-api # should be destroyed at the end when no more gateway-api CRDs are required + .PHONY: deploy-chart deploy-chart: - $(MAKE) update-helm-dependencies - $(MAKE) deploy-minio-operator-if-required - $(MAKE) deploy-prometheus-operator - $(MAKE) deploy-gateway-api + $(MAKE) deploy-shared $(MAKE) install-chart .PHONY: destroy-chart destroy-chart: -$(MAKE) uninstall-chart - -$(MAKE) destroy-gateway-api - -$(MAKE) destroy-prometheus-operator - -$(MAKE) undeploy-minio-operator + -$(MAKE) destroy-shared .PHONY: deploy-network deploy-network: deploy-chart @@ -94,7 +100,7 @@ deploy-network: deploy-chart kubectl wait --for=jsonpath='{.status.phase}'=Running pod -l fullstack.hedera.com/type=network-node --timeout=600s .PHONY: destroy-network -destroy-network: destroy-test-container destroy-chart +destroy-network: destroy-test-container uninstall-chart .PHONY: setup-nodes setup-nodes: setup @@ -132,7 +138,10 @@ run-func: source "${SCRIPTS_DIR}/${SCRIPT_NAME}" && ${FUNC} .PHONY: start -start: deploy-minio-operator-if-required update-helm-dependencies deploy-network setup-nodes start-nodes +start: ci-deploy-network setup-nodes start-nodes + +.PHONY: stop +stop: stop-nodes destroy-network .PHONY: restart restart: stop-nodes start-nodes @@ -220,37 +229,24 @@ helm-test: destroy-test-container: echo "" && \ echo ">> Deleting test container..." && \ - kubectl delete pod network-test || true + kubectl delete pod network-test -n "${NAMESPACE}" || true ######################################### CI ################################# .PHONY: local-kubectl-bats local-kubectl-bats: source "${SCRIPTS_DIR}/${DOCKER_SCRIPT}" && build_kubectl_bats "${CLUSTER_NAME}" -# Here we run all steps in sequence, if any step fails, deploy-all trap the EXIT and run cleanup -.PHONY: run-deploy-seq -run-deploy-seq: setup deploy-network helm-test setup-nodes start-nodes - -.PHONY: deploy-all -deploy-all: +.PHONY: ci-test +ci-test: # Enable cleanup_test function so that even if test fails, we cleanup the cluster. # We are only enabling this in this make target, however if necessary, similar pattern can be used in other targets. # Ref: https://stackoverflow.com/questions/28597794/how-can-i-clean-up-after-an-error-in-a-makefile - function cleanup_test { + # NOTE: It needs latest make (version ~=4.3) + function cleanup_test () { $(MAKE) destroy-network } trap cleanup_test EXIT # always destroy-network on exit - $(MAKE) run-deploy-seq - -.PHONY: destroy-all -destroy-all: - -$(MAKE) destroy-network - -$(MAKE) undeploy-minio-operator - -$(MAKE) destroy-prometheus-operator - -.PHONY: ci-test -ci-test: setup-cluster local-kubectl-bats - $(MAKE) deploy-all CHART_VALUES_FILES="$(PWD)/ci/ci-values.yaml" + $(MAKE) ci-deploy-network setup-nodes start-nodes .PHONY: ci-deploy-network ci-deploy-network: setup-cluster local-kubectl-bats diff --git a/dev/dev-cluster.yaml b/dev/dev-cluster.yaml new file mode 100644 index 000000000..df235cf65 --- /dev/null +++ b/dev/dev-cluster.yaml @@ -0,0 +1,8 @@ +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +name: fst # this is overridden if CLUSTER_NAME env var is set. Check .env file +nodes: + - role: control-plane + labels: + fullstack-scheduling.io/os: linux + fullstack-scheduling.io/role: network \ No newline at end of file diff --git a/dev/gateway-api/Makefile b/dev/gateway-api/Makefile index fe8a9cd6e..b2d82c5dd 100644 --- a/dev/gateway-api/Makefile +++ b/dev/gateway-api/Makefile @@ -1,3 +1,6 @@ +# Force the use of bash as the shell for more features +SHELL=/bin/bash + SHELLOPTS:=$(if $(SHELLOPTS),$(SHELLOPTS):)pipefail:errexit .ONESHELL: diff --git a/dev/scripts/docker.sh b/dev/scripts/docker.sh index 1f69ee151..1caf83667 100644 --- a/dev/scripts/docker.sh +++ b/dev/scripts/docker.sh @@ -10,8 +10,10 @@ function build_kubectl_bats() { [[ -z "${CLUSTER_NAME}" ]] && echo "ERROR: [build_kubectl_bats] Cluster name is required" && return 1 echo "" - echo "Building kubectl-bats image" + echo "Building kubectl-bats image" echo "-----------------------------------------------------------------------------------------------------" - cd "${DOCKERFILE_DIR}/kubectl-bats" && docker build -t "${KUBECTL_BATS_IMAGE}" . + cd "${DOCKERFILE_DIR}/kubectl-bats" && docker build -t "${KUBECTL_BATS_IMAGE}" . kind load docker-image "${KUBECTL_BATS_IMAGE}" -n "${CLUSTER_NAME}" + + log_time "build_kubectl_bats" } \ No newline at end of file diff --git a/dev/scripts/env.sh b/dev/scripts/env.sh index 81838d7d4..ba7b11325 100644 --- a/dev/scripts/env.sh +++ b/dev/scripts/env.sh @@ -1,13 +1,19 @@ #!/usr/bin/env bash +start_time=$(date +%s) + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" readonly SCRIPT_DIR readonly TMP_DIR="${SCRIPT_DIR}/../temp" +readonly SETUP_CHART_DIR="${SCRIPT_DIR}/../../charts/fullstack-cluster-setup" readonly CHART_DIR="${SCRIPT_DIR}/../../charts/hedera-network" +POD_MONITOR_ROLE="${POD_MONITOR_ROLE:-pod-monitor-role}" +GATEWAY_CLASS_NAME="${GATEWAY_CLASS_NAME:-fst-gateway-class}" # telemetry related env variables +readonly COMMON_RESOURCES="${SCRIPT_DIR}/../common-resources" readonly GATEWAY_API_DIR="${SCRIPT_DIR}/../gateway-api" readonly TELEMETRY_DIR="${SCRIPT_DIR}/../telemetry" readonly PROMETHEUS_DIR="${TELEMETRY_DIR}/prometheus" @@ -54,7 +60,11 @@ function setup_kubectl_context() { kubectl get ns echo "Setting kubectl context..." - kubectl config use-context "kind-${CLUSTER_NAME}" + local count + count=$(kubectl config get-contexts --no-headers | grep -c "kind-${CLUSTER_NAME}") + if [[ $count -ne 0 ]]; then + kubectl config use-context "kind-${CLUSTER_NAME}" + fi kubectl config set-context --current --namespace="${NAMESPACE}" kubectl config get-contexts } @@ -64,4 +74,27 @@ function setup() { load_env_file } +function log_time() { + local end_time duration execution_time + + local func_name=$1 + + end_time=$(date +%s) + duration=$((end_time - start_time)) + execution_time=$(printf "%.2f seconds" "${duration}") + echo "-----------------------------------------------------------------------------------------------------" + echo "<<< ${func_name} execution took: ${execution_time} >>>" + echo "-----------------------------------------------------------------------------------------------------" +} + setup + +echo "--------------------------Env Setup: fullstack-testing ------------------------------------------------" +echo "CLUSTER_NAME: ${CLUSTER_NAME}" +echo "RELEASE_NAME: ${HELM_RELEASE_NAME}" +echo "USER: ${USER}" +echo "NAMESPACE: ${NAMESPACE}" +echo "SCRIPT_DIR: ${SCRIPT_DIR}" +echo "TMP_DIR: ${TMP_DIR}" +echo "-----------------------------------------------------------------------------------------------------" +echo "" diff --git a/dev/scripts/gateway.sh b/dev/scripts/gateway.sh index 7004dbfb6..0817a2c10 100644 --- a/dev/scripts/gateway.sh +++ b/dev/scripts/gateway.sh @@ -21,6 +21,8 @@ function deploy_haproxy_ingress() { echo "HAProxy Ingress Controller is already installed" echo "" fi + + log_time "deploy_haproxy_ingress" } function destroy_haproxy_ingress() { @@ -40,6 +42,8 @@ function destroy_haproxy_ingress() { echo "HAProxy Ingress Controller is uninstalled" echo "" + + log_time "destroy_haproxy_ingress" } function deploy_gateway_api_crd() { @@ -79,22 +83,27 @@ function deploy_envoy_gateway_api() { echo "Envoy Gateway API is already installed" echo "" fi + + get_gateway_status + + log_time "deploy_envoy_gateway_api" } function get_gateway_status() { echo "" helm list --all-namespaces | grep envoy-gateway - echo "-----------------------------------------------------------------------------------------------------" + echo "-----------------------Gateway CRDs------------------------------------------------------------------------------" kubectl get crd - echo "-----------------------------------------------------------------------------------------------------" + echo "-----------------------Gateway Class------------------------------------------------------------------------------" kubectl get gatewayclass - echo "-----------------------------------------------------------------------------------------------------" + echo "-----------------------Gateway------------------------------------------------------------------------------" kubectl get gateway - echo "-----------------------------------------------------------------------------------------------------" + echo "-----------------------HTTPRoute------------------------------------------------------------------------------" kubectl get httproute - echo "-----------------------------------------------------------------------------------------------------" + echo "-----------------------GRPCRoute------------------------------------------------------------------------------" kubectl get grpcroute - echo "-----------------------------------------------------------------------------------------------------" + echo "-----------------------TCPRoute------------------------------------------------------------------------------" + kubectl get tcproute } function destroy_envoy_gateway_api() { @@ -111,11 +120,10 @@ function destroy_envoy_gateway_api() { kubectl delete ns gateway-system fi - uninstall_crd "gateway.networking.k8s.io" - uninstall_crd "gateway.envoyproxy.io" - echo "Envoy Gateway API is uninstalled" echo "" + + log_time "destroy_envoy_gateway_api" } function uninstall_crd() { @@ -129,6 +137,8 @@ function uninstall_crd() { kubectl delete crd "${name}" done fi + + log_time "uninstall_crd" } function expose_envoy_gateway_svc() { @@ -140,15 +150,17 @@ function expose_envoy_gateway_svc() { unexpose_envoy_gateway_svc || true - ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=fst -o jsonpath="{.items[0].metadata.name}" ) + ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace="${NAMESPACE}",gateway.envoyproxy.io/owning-gateway-name=fst -o jsonpath="{.items[0].metadata.name}" ) echo "" echo "Exposing Envoy Gateway Service: ${ENVOY_SERVICE} on ${local_port}:${gateway_port}" echo "-----------------------------------------------------------------------------------------------------" kubectl port-forward "svc/${ENVOY_SERVICE}" -n envoy-gateway-system "${local_port}":"${gateway_port}" & + + log_time "expose_envoy_gateway_svc" } function unexpose_envoy_gateway_svc() { - export GATEWAY_SVC_PID=$(ps aux | grep "kubectl port-forward svc/${ENVOY_SERVICE}" | sed -n 2p | awk '{ print $2 }') + export GATEWAY_SVC_PID=$(ps aux | grep "kubectl port-forward svc/${ENVOY_SERVICE}" | grep -v "grep" | sed -n 1p | awk '{ print $2 }') [[ -z "${GATEWAY_SVC_PID}" ]] && echo "No Envoy Gateway Service PID is found" && return 0 if [[ "${GATEWAY_SVC_PID}" ]]; then @@ -157,6 +169,8 @@ function unexpose_envoy_gateway_svc() { echo "-----------------------------------------------------------------------------------------------------" kill "${GATEWAY_SVC_PID}" &>/dev/null || true fi + + log_time "unexpose_envoy_gateway_svc" } function test_http_route() { @@ -195,6 +209,8 @@ function test_http_route() { echo "-----------------------------------------------------------------------------------------------------" unexpose_envoy_gateway_svc || true kubectl delete -f "${GATEWAY_API_DIR}/http-debug.yaml" + + log_time "test_http_route" } function test_grpc_route() { @@ -232,6 +248,8 @@ function test_grpc_route() { echo "-----------------------------------------------------------------------------------------------------" unexpose_envoy_gateway_svc || true kubectl delete -f "${GATEWAY_API_DIR}/grpc-debug.yaml" + + log_time "test_grpc_route" } function test_tcp_route() { @@ -266,4 +284,6 @@ function test_tcp_route() { rm deleteme.txt unexpose_envoy_gateway_svc || true kubectl delete -f "${GATEWAY_API_DIR}/tcp-debug.yaml" + + log_time "test_tcp_route" } diff --git a/dev/scripts/main.sh b/dev/scripts/main.sh index 3baaa6a8d..a7ee713a2 100644 --- a/dev/scripts/main.sh +++ b/dev/scripts/main.sh @@ -8,16 +8,19 @@ function setup_cluster() { [[ -z "${NAMESPACE}" ]] && echo "ERROR: [setup_cluster] Namespace name is required" && return 1 echo "Cluster name: ${CLUSTER_NAME}" - local count=$(kind get clusters -q | grep -c -sw "${CLUSTER_NAME}") + local count + + count=$(kind get clusters -q | grep -c -sw "${CLUSTER_NAME}") if [[ $count -eq 0 ]]; then echo "Cluster '${CLUSTER_NAME}' not found" - kind create cluster -n "${CLUSTER_NAME}" - kubectl create ns "${NAMESPACE}" + kind create cluster -n "${CLUSTER_NAME}" --config="${CUR_DIR}/../dev-cluster.yaml" else echo "Cluster '${CLUSTER_NAME}' found" fi - setup_kubectl_context + setup_kubectl_context + + log_time "setup_cluster" } function destroy_cluster() { @@ -28,6 +31,56 @@ function destroy_cluster() { kubectl delete ns "${NAMESPACE}" || true } +function deploy_shared() { + deploy_fullstack_cluster_setup_chart +} + +function destroy_shared() { + destroy_fullstack_cluster_setup_chart +} + +function deploy_fullstack_cluster_setup_chart() { + setup_kubectl_context + + echo "Installing fullstack-cluster-setup chart" + echo "-----------------------------------------------------------------------------------------------------" + local count=$(helm list --all-namespaces -q | grep -c "fullstack-cluster-setup") + if [[ $count -eq 0 ]]; then + helm install -n "${NAMESPACE}" "fullstack-cluster-setup" "${SETUP_CHART_DIR}" + else + echo "fullstack-cluster-setup chart is already installed" + echo "" + fi + + echo "-----------------------Shared Resources------------------------------------------------------------------------------" + kubectl get clusterrole "${POD_MONITOR_ROLE}" -o wide + kubectl get gatewayclass + echo "" + + log_time "deploy_fullstack_cluster_setup_chart" +} + +function destroy_fullstack_cluster_setup_chart() { + setup_kubectl_context + + echo "Uninstalling fullstack-cluster-setup chart" + echo "-----------------------------------------------------------------------------------------------------" + local count=$(helm list --all-namespaces -q | grep -c "fullstack-cluster-setup") + if [[ $count -ne 0 ]]; then + helm uninstall -n "${NAMESPACE}" "fullstack-cluster-setup" + else + echo "fullstack-cluster-setup chart is already installed" + echo "" + fi + + echo "-----------------------Shared Resources------------------------------------------------------------------------------" + kubectl get clusterrole "${POD_MONITOR_ROLE}" -o wide + kubectl get gatewayclass + echo "" + + log_time "destroy_fullstack_cluster_setup_chart" +} + function install_chart() { local node_setup_script=$1 [[ -z "${node_setup_script}" ]] && echo "ERROR: [install_chart] Node setup script name is required" && return 1 @@ -37,23 +90,40 @@ function install_chart() { echo "" echo "Installing helm chart... " echo "SCRIPT_NAME: ${node_setup_script}" - echo "Values: -f ${CHART_DIR}/values.yaml --values ${CHART_VALUES_FILES}" + echo "Additional values: ${CHART_VALUES_FILES}" echo "-----------------------------------------------------------------------------------------------------" - if [ "${node_setup_script}" = "nmt-install.sh" ]; then - nmt_install + local count=$(helm list -q -n "${NAMESPACE}" | grep -c "${HELM_RELEASE_NAME}") + if [[ $count -eq 0 ]]; then + if [ "${node_setup_script}" = "nmt-install.sh" ]; then + nmt_install + else + direct_install + fi else - direct_install + echo "${HELM_RELEASE_NAME} is already installed" fi + + log_time "install_chart" } function uninstall_chart() { [[ -z "${HELM_RELEASE_NAME}" ]] && echo "ERROR: [uninstall_chart] Helm release name is required" && return 1 echo "" - echo "Uninstalling helm chart... " - echo "-----------------------------------------------------------------------------------------------------" - helm uninstall "${HELM_RELEASE_NAME}" - sleep 10 + local count=$(helm list -q -n "${NAMESPACE}" | grep -c "${HELM_RELEASE_NAME}") + if [[ $count -ne 0 ]]; then + echo "Uninstalling helm chart ${HELM_RELEASE_NAME} in namespace ${NAMESPACE}... " + echo "-----------------------------------------------------------------------------------------------------" + helm uninstall -n "${NAMESPACE}" "${HELM_RELEASE_NAME}" + sleep 10 + echo "Uninstalled helm chart ${HELM_RELEASE_NAME} in namespace ${NAMESPACE}" + else + echo "Helm chart '${HELM_RELEASE_NAME}' not found in namespace ${NAMESPACE}. Nothing to uninstall. " + fi + + kubectl delete ns "${NAMESPACE}" || true + + log_time "uninstall_chart" } function nmt_install() { @@ -87,10 +157,10 @@ function run_helm_chart_tests() { [[ -z "${test_name}" ]] && echo "ERROR: test name is required" && return 1 echo "" - echo "Running helm chart tests (first run takes ~2m)... " + echo "Running helm chart tests (takes ~5m, timeout 15m)... " echo "-----------------------------------------------------------------------------------------------------" - helm test "${HELM_RELEASE_NAME}" --filter name="${test_name}" + helm test "${HELM_RELEASE_NAME}" --filter name="${test_name}" --timeout 15m local test_status=$(kubectl get pod "${test_name}" -o jsonpath='{.status.phase}' | xargs) echo "Helm test status: ${test_status}" @@ -104,4 +174,6 @@ function run_helm_chart_tests() { echo "Returning exit code 1" return 1 fi + + log_time "run_helm_chart_tests" } \ No newline at end of file diff --git a/dev/scripts/telemetry.sh b/dev/scripts/telemetry.sh index 15a258300..310be5d24 100644 --- a/dev/scripts/telemetry.sh +++ b/dev/scripts/telemetry.sh @@ -18,6 +18,8 @@ function fetch-prometheus-operator-bundle() { [[ "${status}" != 0 ]] && rm "${PROMETHEUS_OPERATOR_YAML}" && echo "ERROR: Failed to fetch prometheus bundle" return "${status}" fi + + log_time "fetch-prometheus-operator-bundle" } function deploy-prometheus-operator() { @@ -36,6 +38,8 @@ function deploy-prometheus-operator() { echo "Prometheus operator CRD is already installed" echo "" fi + + log_time "deploy-prometheus-operator" } function destroy-prometheus-operator() { @@ -45,6 +49,8 @@ function destroy-prometheus-operator() { echo "-----------------------------------------------------------------------------------------------------" kubectl delete -f "${PROMETHEUS_OPERATOR_YAML}" sleep 10 + + log_time "destroy-prometheus-operator" } function deploy-prometheus() { @@ -77,6 +83,8 @@ function deploy-prometheus() { echo "Waiting for prometheus to be active (timeout 300s)..." kubectl wait --for=condition=Ready pods -l app.kubernetes.io/name=prometheus --timeout 300s -n "${NAMESPACE}" + + log_time "deploy-prometheus" } function destroy-prometheus() { @@ -89,6 +97,8 @@ function destroy-prometheus() { kubectl delete -f "${PROMETHEUS_RBAC_YAML}" || true kubectl delete clusterrolebindings prometheus || true sleep 5 + + log_time "destroy-prometheus" } function deploy-prometheus-example-app() { @@ -143,6 +153,8 @@ function deploy_grafana_tempo() { helm upgrade -f "${TELEMETRY_DIR}/grafana/grafana-values.yaml" --install grafana grafana/grafana echo "Waiting for grafana to be active (timeout 300s)..." kubectl wait --for=jsonpath='{.status.phase}'=Running pod -l "app.kubernetes.io/name=grafana,app.kubernetes.io/instance=grafana" --timeout=300s + + log_time "deploy_grafana_tempo" } function destroy_grafana_tempo() { diff --git a/dev/scripts/template.env b/dev/scripts/template.env index b25b0b7f0..bbeb1109e 100644 --- a/dev/scripts/template.env +++ b/dev/scripts/template.env @@ -1,9 +1,12 @@ USER="${USER:-changeme}" -CLUSTER_NAME="fst" -NAMESPACE="fst-${USER}" -HELM_RELEASE_NAME="fst" +CLUSTER_NAME="${CLUSTER_NAME:-fst}" +NAMESPACE="${NAMESPACE:-fst-${USER}}" +HELM_RELEASE_NAME="${RELEASE_NAME:-fst}" NMT_VERSION=v2.0.0-alpha.0 PLATFORM_VERSION=v0.39.1 +POD_MONITOR_ROLE="${POD_MONITOR_ROLE:-pod-monitor-role}" +GATEWAY_CLASS_NAME="${GATEWAY_CLASS_NAME:-fst-gateway-class}" + #NODE_NAMES=(node0 node1 node2 node3) NODE_NAMES=(node0) diff --git a/dev/telemetry/grafana/example-tracing-app.yaml b/dev/telemetry/grafana/example-tracing-app.yaml index 7231b25e2..00a2b59ce 100644 --- a/dev/telemetry/grafana/example-tracing-app.yaml +++ b/dev/telemetry/grafana/example-tracing-app.yaml @@ -4,7 +4,6 @@ metadata: annotations: ingress.kubernetes.io/ssl-redirect: "false" name: ingress - namespace: default spec: rules: - http: @@ -21,7 +20,6 @@ apiVersion: apps/v1 kind: Deployment metadata: name: xk6-tracing - namespace: default spec: minReadySeconds: 10 replicas: 1 diff --git a/fullstack-examples/build.gradle.kts b/fullstack-examples/build.gradle.kts index 964ebac30..4eabfe398 100644 --- a/fullstack-examples/build.gradle.kts +++ b/fullstack-examples/build.gradle.kts @@ -14,22 +14,41 @@ * limitations under the License. */ +import com.hedera.fullstack.gradle.plugin.HelmInstallChartTask + 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")) } +// TODO: task register helmUninstallNginxChart + +tasks.check { + dependsOn("helmInstallNginxChart") + // TODO: depends on 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/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..e15ca2ee8 --- /dev/null +++ b/fullstack-gradle-plugin/src/test/java/com/hedera/fullstack/gradle/plugin/HelmInstallChartTaskTest.java @@ -0,0 +1,122 @@ +/* + * 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(); + } finally { + suppressExceptions(() -> helmClient.uninstallChart(RELEASE_NAME)); + 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/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/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/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/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/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..475baa18e 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 @@ -54,7 +54,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 +87,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 +99,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 +120,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 +148,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 +178,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 +196,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 +213,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,7 +337,7 @@ 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(); 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/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-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")