diff --git a/solutions/virtualization/README.md b/solutions/virtualization/README.md new file mode 100644 index 00000000..ad658a93 --- /dev/null +++ b/solutions/virtualization/README.md @@ -0,0 +1,70 @@ +# OpenShift Virtualization on OCP VPC cluster + +This architecture help setting up OpenShift Virtualization on OCP VPC cluster. Also the outbound traffic is allowed, which is required for accessing the Operator Hub. + +Prerequisites: +- A OCP VPC cluster. +- Outbound traffic protection disabled. + +The following resources are provisioned by this example: + +- Install `openshift-data-foundation` and `vpc-file-csi-driver` addons. +- Setup OperatorHub + + + +### Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [helm](#requirement\_helm) | 2.17.0 | +| [ibm](#requirement\_ibm) | 1.75.1 | +| [kubernetes](#requirement\_kubernetes) | 2.35.1 | +| [null](#requirement\_null) | 3.2.3 | +| [time](#requirement\_time) | 0.12.1 | + +### Modules + +No modules. + +### Resources + +| Name | Type | +|------|------| +| [helm_release.operator](https://registry.terraform.io/providers/hashicorp/helm/2.17.0/docs/resources/release) | resource | +| [helm_release.subscription](https://registry.terraform.io/providers/hashicorp/helm/2.17.0/docs/resources/release) | resource | +| [ibm_container_addons.addons](https://registry.terraform.io/providers/IBM-Cloud/ibm/1.75.1/docs/resources/container_addons) | resource | +| [kubernetes_config_map_v1_data.disable_default_storageclass](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/config_map_v1_data) | resource | +| [kubernetes_config_map_v1_data.set_vpc_file_default_storage_class](https://registry.terraform.io/providers/hashicorp/kubernetes/2.35.1/docs/resources/config_map_v1_data) | resource | +| [null_resource.config_map_status](https://registry.terraform.io/providers/hashicorp/null/3.2.3/docs/resources/resource) | resource | +| [null_resource.enable_catalog_source](https://registry.terraform.io/providers/hashicorp/null/3.2.3/docs/resources/resource) | resource | +| [null_resource.update_storage_profile](https://registry.terraform.io/providers/hashicorp/null/3.2.3/docs/resources/resource) | resource | +| [time_sleep.wait_for_default_storage](https://registry.terraform.io/providers/hashicorp/time/0.12.1/docs/resources/sleep) | resource | +| [time_sleep.wait_for_storage_profile](https://registry.terraform.io/providers/hashicorp/time/0.12.1/docs/resources/sleep) | resource | +| [time_sleep.wait_for_subscription](https://registry.terraform.io/providers/hashicorp/time/0.12.1/docs/resources/sleep) | resource | +| [ibm_container_cluster_config.cluster_config](https://registry.terraform.io/providers/IBM-Cloud/ibm/1.75.1/docs/data-sources/container_cluster_config) | data source | +| [ibm_container_vpc_cluster.cluster](https://registry.terraform.io/providers/IBM-Cloud/ibm/1.75.1/docs/data-sources/container_vpc_cluster) | data source | + +### Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [cluster\_config\_endpoint\_type](#input\_cluster\_config\_endpoint\_type) | Specify the type of endpoint to use to access the cluster configuration. Possible values: `default`, `private`, `vpe`, `link`. The `default` value uses the default endpoint of the cluster. | `string` | `"default"` | no | +| [cluster\_id](#input\_cluster\_id) | The ID of the cluster to deploy the agents in. | `string` | n/a | yes | +| [cluster\_resource\_group\_id](#input\_cluster\_resource\_group\_id) | The resource group ID of the cluster. | `string` | n/a | yes | +| [ibmcloud\_api\_key](#input\_ibmcloud\_api\_key) | The IBM Cloud API key. | `string` | n/a | yes | +| [infra\_node\_selectors](#input\_infra\_node\_selectors) | List of infra node selectors to apply to HyperConverged pods. |
list(object({
label = string
values = list(string)
}))
|
[
{
"label": "ibm-cloud.kubernetes.io/server-type",
"values": [
"virtual",
"physical"
]
}
]
| no | +| [provider\_visibility](#input\_provider\_visibility) | Set the visibility value for the IBM terraform provider. Supported values are `public`, `private`, `public-and-private`. [Learn more](https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/guides/custom-service-endpoints). | `string` | `"public"` | no | +| [provision\_odf\_addon](#input\_provision\_odf\_addon) | Set this variable to true to install OpenShift Data Foundation addon in your existing cluster. | `bool` | `false` | no | +| [provision\_vpc\_file\_addon](#input\_provision\_vpc\_file\_addon) | Set this variable to true to install File Storage for VPC addon in your existing cluster. | `bool` | `false` | no | +| [region](#input\_region) | The region in which to provision all resources created by this solution. | `string` | `"us-south"` | no | +| [vpc\_file\_default\_storage\_class](#input\_vpc\_file\_default\_storage\_class) | The name of the VPC File storage class which will be set as the default storage class. | `string` | `"ibmc-vpc-file-metro-1000-iops"` | no | +| [wait\_till](#input\_wait\_till) | To avoid long wait times when you run your Terraform code, you can specify the stage when you want Terraform to mark the cluster resource creation as completed. Depending on what stage you choose, the cluster creation might not be fully completed and continues to run in the background. However, your Terraform code can continue to run without waiting for the cluster to be fully created. Supported args are `MasterNodeReady`, `OneWorkerNodeReady`, `IngressReady` and `Normal` | `string` | `"Normal"` | no | +| [wait\_till\_timeout](#input\_wait\_till\_timeout) | Timeout for wait\_till in minutes. | `number` | `90` | no | +| [workloads\_node\_selectors](#input\_workloads\_node\_selectors) | List of workload node selectors to apply to HyperConverged pods. |
list(object({
label = string
values = list(string)
}))
|
[
{
"label": "ibm-cloud.kubernetes.io/server-type",
"values": [
"physical"
]
}
]
| no | + +### Outputs + +No outputs. + diff --git a/solutions/virtualization/catalogValidationValues.json.template b/solutions/virtualization/catalogValidationValues.json.template new file mode 100644 index 00000000..f48a7e33 --- /dev/null +++ b/solutions/virtualization/catalogValidationValues.json.template @@ -0,0 +1,3 @@ +{ + "ibmcloud_api_key": $VALIDATION_APIKEY +} diff --git a/solutions/virtualization/chart/operator/.helmignore b/solutions/virtualization/chart/operator/.helmignore new file mode 100644 index 00000000..0e8a0eb3 --- /dev/null +++ b/solutions/virtualization/chart/operator/.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/solutions/virtualization/chart/operator/Chart.yaml b/solutions/virtualization/chart/operator/Chart.yaml new file mode 100644 index 00000000..13d2b5ef --- /dev/null +++ b/solutions/virtualization/chart/operator/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: virtualization +description: A Helm chart for Openshft Virtualization + +# 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.1.2 + +# 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: "1.16.0" diff --git a/solutions/virtualization/chart/operator/templates/hyperconverged.yaml b/solutions/virtualization/chart/operator/templates/hyperconverged.yaml new file mode 100644 index 00000000..5830dbc8 --- /dev/null +++ b/solutions/virtualization/chart/operator/templates/hyperconverged.yaml @@ -0,0 +1,56 @@ +apiVersion: hco.kubevirt.io/v1beta1 +kind: HyperConverged +metadata: + annotations: + deployOVS: 'false' + name: kubevirt-hyperconverged + namespace: {{ .Release.Namespace }} +spec: + virtualMachineOptions: + disableFreePageReporting: false + disableSerialConsoleLog: true + higherWorkloadDensity: + memoryOvercommitPercentage: 100 + liveMigrationConfig: + allowAutoConverge: false + allowPostCopy: false + completionTimeoutPerGiB: 800 + parallelMigrationsPerCluster: 5 + parallelOutboundMigrationsPerNode: 2 + progressTimeout: 150 + infra: + nodePlacement: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + {{- range $node_selector := .Values.infra_node_selectors }} + - key: {{ $node_selector.label | quote }} + operator: In + values: + {{- range $val := $node_selector.values }} + - {{ $val | quote }} + {{- end}} + {{- end}} + workloadUpdateStrategy: + batchEvictionInterval: 1m0s + batchEvictionSize: 10 + workloadUpdateMethods: + - LiveMigrate + uninstallStrategy: BlockUninstallIfWorkloadsExist + workloads: + nodePlacement: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + {{- range $node_selector := .Values.workloads_node_selectors }} + - key: {{ $node_selector.label | quote }} + operator: In + values: + {{- range $val := $node_selector.values }} + - {{ $val | quote }} + {{- end}} + {{- end}} diff --git a/solutions/virtualization/chart/operator/values.yaml b/solutions/virtualization/chart/operator/values.yaml new file mode 100755 index 00000000..78320b4b --- /dev/null +++ b/solutions/virtualization/chart/operator/values.yaml @@ -0,0 +1,14 @@ +# NOTE: Mock values added here for helm linter to pass. Actual values are set in main.tf +operator: + # renovate: datasource=docker depName=icr.io/ext/logdna-agent versioning=regex:^(?\d+)\.(?\d+)\.(?\d+)-(?\d{8}).\w+?$ + version: "v4.17.4" + +infra_node_selectors: + - label: ibm-cloud.kubernetes.io/server-type + values: + - virtual + +workloads_node_selectors: + - label: ibm-cloud.kubernetes.io/server-type + values: + - physical diff --git a/solutions/virtualization/chart/subscription/.helmignore b/solutions/virtualization/chart/subscription/.helmignore new file mode 100644 index 00000000..0e8a0eb3 --- /dev/null +++ b/solutions/virtualization/chart/subscription/.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/solutions/virtualization/chart/subscription/Chart.yaml b/solutions/virtualization/chart/subscription/Chart.yaml new file mode 100644 index 00000000..13d2b5ef --- /dev/null +++ b/solutions/virtualization/chart/subscription/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: virtualization +description: A Helm chart for Openshft Virtualization + +# 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.1.2 + +# 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: "1.16.0" diff --git a/solutions/virtualization/chart/subscription/templates/operator-group.yaml b/solutions/virtualization/chart/subscription/templates/operator-group.yaml new file mode 100644 index 00000000..23ce9a46 --- /dev/null +++ b/solutions/virtualization/chart/subscription/templates/operator-group.yaml @@ -0,0 +1,8 @@ +apiVersion: operators.coreos.com/v1 +kind: OperatorGroup +metadata: + name: kubevirt-hyperconverged-group + namespace: {{ .Release.Namespace }} +spec: + targetNamespaces: + - {{ .Release.Namespace }} diff --git a/solutions/virtualization/chart/subscription/templates/subscription.yaml b/solutions/virtualization/chart/subscription/templates/subscription.yaml new file mode 100644 index 00000000..7a5463ae --- /dev/null +++ b/solutions/virtualization/chart/subscription/templates/subscription.yaml @@ -0,0 +1,11 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: Subscription +metadata: + name: hco-operatorhub + namespace: {{ .Release.Namespace }} +spec: + source: redhat-operators + sourceNamespace: openshift-marketplace + name: kubevirt-hyperconverged + startingCSV: kubevirt-hyperconverged-operator.{{ .Values.subscription.version }} + channel: "stable" diff --git a/solutions/virtualization/chart/subscription/values.yaml b/solutions/virtualization/chart/subscription/values.yaml new file mode 100755 index 00000000..9ca49658 --- /dev/null +++ b/solutions/virtualization/chart/subscription/values.yaml @@ -0,0 +1,4 @@ +# NOTE: Mock values added here for helm linter to pass. Actual values are set in main.tf +subscription: + # renovate: datasource=docker depName=icr.io/ext/logdna-agent versioning=regex:^(?\d+)\.(?\d+)\.(?\d+)-(?\d{8}).\w+?$ + version: "v4.17.4" diff --git a/solutions/virtualization/kubeconfig/.gitignore b/solutions/virtualization/kubeconfig/.gitignore new file mode 100644 index 00000000..632a28fb --- /dev/null +++ b/solutions/virtualization/kubeconfig/.gitignore @@ -0,0 +1,6 @@ +# Ignore everything +* + +# But not these files... +!.gitignore +!README.md diff --git a/solutions/virtualization/kubeconfig/README.md b/solutions/virtualization/kubeconfig/README.md new file mode 100644 index 00000000..e85afee8 --- /dev/null +++ b/solutions/virtualization/kubeconfig/README.md @@ -0,0 +1,2 @@ +This directory must exist in source control so the `ibm_container_cluster_config` data lookup can use it to place the +config.yml used to connect to a kubernetes cluster. diff --git a/solutions/virtualization/main.tf b/solutions/virtualization/main.tf new file mode 100644 index 00000000..93fc0f0d --- /dev/null +++ b/solutions/virtualization/main.tf @@ -0,0 +1,184 @@ +######################################################################################################################## +# Addon +######################################################################################################################## + +locals { + odf_version = replace(data.ibm_container_vpc_cluster.cluster.kube_version, "/(\\d+\\.\\d+)\\.\\d+.*/", "$1.0") + vpc_file_version = "2.0" + addons = var.provision_odf_addon && var.provision_vpc_file_addon ? { + "openshift-data-foundation" = local.odf_version, + "vpc-file-csi-driver" = local.vpc_file_version + } : var.provision_odf_addon ? { + "openshift-data-foundation" = local.odf_version, + } : var.provision_vpc_file_addon ? { + "vpc-file-csi-driver" = local.vpc_file_version + } : {} +} + +resource "ibm_container_addons" "addons" { + count = var.provision_odf_addon || var.provision_vpc_file_addon ? 1 : 0 + cluster = var.cluster_id + resource_group_id = var.cluster_resource_group_id + + # setting to false means we do not want Terraform to manage addons that are managed elsewhere + manage_all_addons = false + + dynamic "addons" { + for_each = local.addons + content { + name = addons.key + version = addons.value + parameters_json = addons.key != "openshift-data-foundation" ? null : < /dev/null; then + echo "Error: OpenShift CLI (oc) is not installed. Exiting." + exit 1 + fi +} + +function apply_oc_patch() { + + local attempt=0 + local retry_wait_time=5 + + while [ $attempt -lt $MAX_ATTEMPTS ]; do + echo "Attempt $((attempt+1)) of $MAX_ATTEMPTS: Applying OpenShift Console patch..." + + if eval "$PATCH_APPLY"; then + echo "Patch applied successfully." + return 0 + else + echo "Failed to apply patch. Retrying in ${retry_wait_time}s..." + sleep $retry_wait_time + ((attempt++)) + fi + done + + echo "Maximum retry attempts reached. Could not apply patch." + exit 1 +} + +echo "=========================================" + +check_oc_cli + + echo "Enabling default catalog source" + apply_oc_patch + +echo "=========================================" diff --git a/solutions/virtualization/scripts/get_config_map_status.sh b/solutions/virtualization/scripts/get_config_map_status.sh new file mode 100755 index 00000000..cb53e429 --- /dev/null +++ b/solutions/virtualization/scripts/get_config_map_status.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -e + +CONFIGMAP_NAME="addon-vpc-file-csi-driver-configmap" +NAMESPACE="kube-system" +COUNTER=0 +MAX_ATTEMPTS=40 + +while [[ $COUNTER -lt $MAX_ATTEMPTS ]] && ! kubectl get configmap $CONFIGMAP_NAME -n $NAMESPACE &>/dev/null; do + COUNTER=$((COUNTER + 1)) + echo "Attempt $COUNTER: ConfigMap '$CONFIGMAP_NAME' not found in namespace '$NAMESPACE', retrying..." + sleep 60 +done + +if [[ $COUNTER -eq $MAX_ATTEMPTS ]]; then + echo "ConfigMap '$CONFIGMAP_NAME' did not become available within $MAX_ATTEMPTS attempts." + # Output for debugging + kubectl get configmaps -n $NAMESPACE + exit 1 +else + echo "ConfigMap '$CONFIGMAP_NAME' is now available." >&2 +fi diff --git a/solutions/virtualization/scripts/update_storage_profile.sh b/solutions/virtualization/scripts/update_storage_profile.sh new file mode 100755 index 00000000..410342c4 --- /dev/null +++ b/solutions/virtualization/scripts/update_storage_profile.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +set -euo pipefail + +STORAGE_CLASS="$1" + +STORAGE_PROFILE="oc patch storageprofile $STORAGE_CLASS -p '{\"spec\": {\"claimPropertySets\": [{\"accessModes\": [\"ReadWriteMany\"], \"volumeMode\": \"Filesystem\"}]}}' --type merge" +MAX_ATTEMPTS=10 +RETRY_WAIT=5 + +function check_oc_cli() { + if ! command -v oc &>/dev/null; then + echo "Error: OpenShift CLI (oc) is not installed. Exiting." + exit 1 + fi +} + +function apply_oc_patch() { + + local attempt=0 + while [ $attempt -lt $MAX_ATTEMPTS ]; do + echo "Attempt $((attempt + 1)) of $MAX_ATTEMPTS: Applying OpenShift Console patch..." + + if eval "$STORAGE_PROFILE"; then + echo "Patch applied successfully." + return 0 + else + echo "Failed to apply patch. Retrying in ${RETRY_WAIT}s..." + sleep $RETRY_WAIT + ((attempt++)) + RETRY_WAIT=$((RETRY_WAIT * 2)) + fi + done + + echo "Maximum retry attempts reached. Could not apply patch." + exit 1 +} + +echo "=========================================" + +check_oc_cli +apply_oc_patch +echo "=========================================" diff --git a/solutions/virtualization/variables.tf b/solutions/virtualization/variables.tf new file mode 100644 index 00000000..c6f84f2c --- /dev/null +++ b/solutions/virtualization/variables.tf @@ -0,0 +1,117 @@ +######################################################################################################################## +# Input variables +######################################################################################################################## + +variable "ibmcloud_api_key" { + type = string + description = "The IBM Cloud API key." + sensitive = true +} + +variable "provider_visibility" { + description = "Set the visibility value for the IBM terraform provider. Supported values are `public`, `private`, `public-and-private`. [Learn more](https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/guides/custom-service-endpoints)." + type = string + default = "public" + + validation { + condition = contains(["public", "private", "public-and-private"], var.provider_visibility) + error_message = "Invalid visibility option. Allowed values are 'public', 'private', or 'public-and-private'." + } +} + +variable "region" { + type = string + description = "The region in which to provision all resources created by this solution." + default = "us-south" +} + +############################################################################## +# Cluster variables +############################################################################## + +variable "cluster_id" { + type = string + description = "The ID of the cluster to deploy the agents in." +} + +variable "cluster_resource_group_id" { + type = string + description = "The resource group ID of the cluster." +} + +variable "cluster_config_endpoint_type" { + description = "Specify the type of endpoint to use to access the cluster configuration. Possible values: `default`, `private`, `vpe`, `link`. The `default` value uses the default endpoint of the cluster." + type = string + default = "default" + nullable = false # use default if null is passed in + validation { + error_message = "The specified endpoint type is not valid. Specify one of the following types of endpoints: `default`, `private`, `vpe`, or `link`." + condition = contains(["default", "private", "vpe", "link"], var.cluster_config_endpoint_type) + } +} + +variable "wait_till" { + description = "To avoid long wait times when you run your Terraform code, you can specify the stage when you want Terraform to mark the cluster resource creation as completed. Depending on what stage you choose, the cluster creation might not be fully completed and continues to run in the background. However, your Terraform code can continue to run without waiting for the cluster to be fully created. Supported args are `MasterNodeReady`, `OneWorkerNodeReady`, `IngressReady` and `Normal`" + type = string + default = "Normal" + + validation { + error_message = "`wait_till` value must be one of `MasterNodeReady`, `OneWorkerNodeReady`, `IngressReady` or `Normal`." + condition = contains([ + "MasterNodeReady", + "OneWorkerNodeReady", + "IngressReady", + "Normal" + ], var.wait_till) + } +} + +variable "wait_till_timeout" { + description = "Timeout for wait_till in minutes." + type = number + default = 90 +} + +variable "provision_odf_addon" { + type = bool + default = false + nullable = false # null values are set to default value + description = "Set this variable to true to install OpenShift Data Foundation addon in your existing cluster." +} + +variable "provision_vpc_file_addon" { + type = bool + default = false + nullable = false # null values are set to default value + description = "Set this variable to true to install File Storage for VPC addon in your existing cluster." +} + +variable "vpc_file_default_storage_class" { + description = "The name of the VPC File storage class which will be set as the default storage class." + type = string + default = "ibmc-vpc-file-metro-1000-iops" +} + +variable "infra_node_selectors" { + type = list(object({ + label = string + values = list(string) + })) + description = "List of infra node selectors to apply to HyperConverged pods." + default = [{ + label = "ibm-cloud.kubernetes.io/server-type" + values = ["virtual", "physical"] + }] +} + +variable "workloads_node_selectors" { + type = list(object({ + label = string + values = list(string) + })) + description = "List of workload node selectors to apply to HyperConverged pods." + default = [{ + label = "ibm-cloud.kubernetes.io/server-type" + values = ["physical"] + }] +} diff --git a/solutions/virtualization/version.tf b/solutions/virtualization/version.tf new file mode 100644 index 00000000..3efaa766 --- /dev/null +++ b/solutions/virtualization/version.tf @@ -0,0 +1,26 @@ + +terraform { + required_version = ">= 1.3.0" + required_providers { + ibm = { + source = "IBM-Cloud/ibm" + version = "1.75.1" + } + helm = { + source = "hashicorp/helm" + version = "2.17.0" + } + time = { + source = "hashicorp/time" + version = "0.12.1" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "2.35.1" + } + null = { + source = "hashicorp/null" + version = "3.2.3" + } + } +}