Skip to content

Commit 908e7f9

Browse files
authored
refactor: Introduce internal semver compare module (#5411)
1 parent 5c1001d commit 908e7f9

7 files changed

Lines changed: 211 additions & 2 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<!--
2+
Copyright 2026 Google LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
-->
16+
17+
# Semver Compare Module
18+
19+
This internal module securely performs a semantic version comparison (major.minor.patch) between a target version and a required minimum version.
20+
21+
It safely parses inputs using native Terraform `regex()`. It evaluates up to four hierarchical components: `major`, `minor`, `patch`, and an optional GKE build number (`-gke.X`). It explicitly ignores any other suffixes (e.g., `-beta`, `+build123`) that follow the parsed components.
22+
23+
Critically, this module implements **fail-open validation**: if the provided `current_version` cannot be resolved to a standard 3-integer format (for example, if a user specifies a Github branch name like `my-custom-feature` or a commit SHA `sha256-4c4892`), the output `is_greater_than_or_equal` evaluates to `true`. This protects the Cluster Toolkit from inadvertently blocking advanced users running fully custom artifacts.
24+
25+
**Note on GKE Versions:** This module evaluates the `-gke.X` suffix as a post-release build number (where `1.35.0-gke.100` is strictly *greater* than `1.35.0`). This correctly maps to GKE's versioning scheme, but diverges from strict SemVer which treats hyphenated suffixes as pre-releases.
26+
27+
## Usage
28+
29+
You must invoke this inside a module, and usually consume it via a `lifecycle { precondition {} }` block since `module` outputs cannot be natively read from `variable { validation {} }` blocks.
30+
31+
```hcl
32+
module "version_check" {
33+
source = "../../internal/semver_compare"
34+
current_version = "1.35.2-gke.1269001"
35+
minimum_version = "1.35.0"
36+
}
37+
38+
resource "terraform_data" "feature_guard" {
39+
lifecycle {
40+
precondition {
41+
condition = module.version_check.is_greater_than_or_equal
42+
error_message = "Your environment requires version >= 1.35.0."
43+
}
44+
}
45+
}
46+
```
47+
48+
<!-- BEGINNING OF PRE-COMMIT-TERRAFORM DOCS HOOK -->
49+
## Requirements
50+
51+
| Name | Version |
52+
|------|---------|
53+
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.3 |
54+
55+
## Providers
56+
57+
No providers.
58+
59+
## Modules
60+
61+
No modules.
62+
63+
## Resources
64+
65+
No resources.
66+
67+
## Inputs
68+
69+
| Name | Description | Type | Default | Required |
70+
|------|-------------|------|---------|:--------:|
71+
| <a name="input_current_version"></a> [current\_version](#input\_current\_version) | The version string to evaluate (e.g. 1.35.2-gke, v0.15.2, sha256-123). | `string` | n/a | yes |
72+
| <a name="input_minimum_version"></a> [minimum\_version](#input\_minimum\_version) | The minimum required version (e.g. 1.35.0). | `string` | n/a | yes |
73+
74+
## Outputs
75+
76+
| Name | Description |
77+
|------|-------------|
78+
| <a name="output_is_greater_than_or_equal"></a> [is\_greater\_than\_or\_equal](#output\_is\_greater\_than\_or\_equal) | True if the version meets the minimum requirement, or if the version is a non-standard custom string (fail-open). |
79+
| <a name="output_is_valid_semver"></a> [is\_valid\_semver](#output\_is\_valid\_semver) | True if both versions could be parsed into major.minor semantic logic. |
80+
<!-- END OF PRE-COMMIT-TERRAFORM DOCS HOOK -->
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Copyright 2026 "Google LLC"
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
locals {
16+
# Strip leading 'v' if present
17+
clean_version = var.current_version != null ? trimprefix(var.current_version, "v") : ""
18+
clean_min_version = var.minimum_version != null ? trimprefix(var.minimum_version, "v") : ""
19+
20+
# Regex to capture strictly major.minor.patch integers, and optionally a -gke.123 build suffix
21+
version_regex = "^([0-9]+)(?:\\.([0-9]+))?(?:\\.([0-9]+))?(?:-gke\\.([0-9]+))?(?:[-+].*)?$"
22+
23+
# Try to parse. If it fails (e.g., 'sha256-12345'), it returns null.
24+
parsed_ver = try(regex(local.version_regex, local.clean_version), null)
25+
parsed_min = try(regex(local.version_regex, local.clean_min_version), null)
26+
27+
is_valid_current = local.parsed_ver != null
28+
is_valid_min = local.parsed_min != null
29+
30+
is_valid_semver = local.is_valid_current && local.is_valid_min
31+
32+
# Map to integers, defaulting to 0 for missing patch versions or gke build numbers
33+
ver_major = local.is_valid_current ? tonumber(local.parsed_ver[0]) : 0
34+
ver_minor = local.is_valid_current ? tonumber(coalesce(local.parsed_ver[1], "0")) : 0
35+
ver_patch = local.is_valid_current ? tonumber(coalesce(local.parsed_ver[2], "0")) : 0
36+
ver_gke = local.is_valid_current ? tonumber(coalesce(local.parsed_ver[3], "0")) : 0
37+
38+
min_major = local.is_valid_min ? tonumber(local.parsed_min[0]) : 0
39+
min_minor = local.is_valid_min ? tonumber(coalesce(local.parsed_min[1], "0")) : 0
40+
min_patch = local.is_valid_min ? tonumber(coalesce(local.parsed_min[2], "0")) : 0
41+
min_gke = local.is_valid_min ? tonumber(coalesce(local.parsed_min[3], "0")) : 0
42+
43+
# Fail-open logic for custom tags
44+
is_greater_than_or_equal = local.is_valid_min ? (
45+
!local.is_valid_current ||
46+
local.ver_major > local.min_major ||
47+
(local.ver_major == local.min_major && local.ver_minor > local.min_minor) ||
48+
(local.ver_major == local.min_major && local.ver_minor == local.min_minor && local.ver_patch > local.min_patch) ||
49+
(local.ver_major == local.min_major && local.ver_minor == local.min_minor && local.ver_patch == local.min_patch && local.ver_gke >= local.min_gke)
50+
) : false
51+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Copyright 2026 "Google LLC"
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
output "is_valid_semver" {
16+
value = local.is_valid_semver
17+
description = "True if both versions could be parsed into major.minor semantic logic."
18+
}
19+
20+
output "is_greater_than_or_equal" {
21+
value = local.is_greater_than_or_equal
22+
description = "True if the version meets the minimum requirement, or if the version is a non-standard custom string (fail-open)."
23+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Copyright 2026 "Google LLC"
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
variable "current_version" {
16+
type = string
17+
description = "The version string to evaluate (e.g. 1.35.2-gke, v0.15.2, sha256-123)."
18+
}
19+
20+
variable "minimum_version" {
21+
type = string
22+
description = "The minimum required version (e.g. 1.35.0)."
23+
24+
validation {
25+
condition = can(regex("^[vV]?([0-9]+)(?:\\.([0-9]+))?(?:\\.([0-9]+))?(?:-gke\\.([0-9]+))?(?:[-+].*)?$", var.minimum_version))
26+
error_message = "The minimum_version must be a valid major.minor.patch[-gke.X] string."
27+
}
28+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
terraform {
18+
required_version = ">= 1.3"
19+
}

modules/scheduler/gke-cluster/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ limitations under the License.
126126
| Name | Source | Version |
127127
|------|--------|---------|
128128
| <a name="module_kubectl_apply"></a> [kubectl\_apply](#module\_kubectl\_apply) | ../../management/kubectl-apply | n/a |
129+
| <a name="module_slice_controller_version_check"></a> [slice\_controller\_version\_check](#module\_slice\_controller\_version\_check) | ../../internal/semver_compare | n/a |
129130
| <a name="module_workload_identity"></a> [workload\_identity](#module\_workload\_identity) | terraform-google-modules/kubernetes-engine/google//modules/workload-identity | >= 40.0 |
130131

131132
## Resources

modules/scheduler/gke-cluster/main.tf

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ locals {
7979
master_version = var.min_master_version != null ? var.min_master_version : data.google_container_engine_versions.version_prefix_filter.latest_master_version
8080
}
8181

82+
83+
module "slice_controller_version_check" {
84+
source = "../../internal/semver_compare"
85+
current_version = local.master_version
86+
minimum_version = "1.35.0-gke.274500"
87+
}
88+
8289
resource "google_container_cluster" "gke_cluster" {
8390
provider = google-beta
8491

@@ -299,9 +306,9 @@ resource "google_container_cluster" "gke_cluster" {
299306
precondition {
300307
condition = (
301308
!var.enable_slice_controller ||
302-
try(tonumber(split(".", local.master_version)[0]) > 1 || (tonumber(split(".", local.master_version)[0]) == 1 && tonumber(split(".", local.master_version)[1]) >= 35), true)
309+
module.slice_controller_version_check.is_greater_than_or_equal
303310
)
304-
error_message = "The GKE Slice Controller requires a GKE version of 1.35 or higher. Please update 'version_prefix' or 'min_master_version'."
311+
error_message = "The GKE Slice Controller requires a GKE version of 1.35.0-gke.274500 or higher. Please update 'version_prefix' or 'min_master_version'."
305312
}
306313
}
307314

0 commit comments

Comments
 (0)