Skip to content

Commit 48f0742

Browse files
authored
Revert "[Bugfix] Applying K8s manifests to GKE clusters via URL"
1 parent 857aacd commit 48f0742

7 files changed

Lines changed: 24 additions & 132 deletions

File tree

modules/management/kubectl-apply/README.md

Lines changed: 1 addition & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ This module simplifies the following functionality:
1313
* **Raw String:** Specify manifests directly within the module configuration using the `content: manifest_body` format.
1414
* **File/Template/Directory Reference:** Set `source` to the path to:
1515
* A single URL to a manifest file. Ex.: `https://github.com/.../myrepo/manifest.yaml`.
16-
17-
> **Note:** Applying from a URL has important limitations. Please review the [Considerations & Callouts for Applying from URLs](#applying-manifests-from-urls-considerations--callouts) section below.
1816
* A single local YAML manifest file (`.yaml`). Ex.: `./manifest.yaml`.
1917
* A template file (`.tftpl`) to generate a manifest. Ex.: `./template.yaml.tftpl`. You can pass the variables to format the template file in `template_vars`.
2018
* A directory containing multiple YAML or template files. Ex: `./manifests/`. You can pass the variables to format the template files in `template_vars`.
@@ -97,73 +95,6 @@ You can specify a particular kueue version that you would like to use using the
9795
>
9896
> Terraform may apply resources in parallel, leading to potential dependency issues. If a resource's dependencies aren't ready, it will be applied again up to 15 times.
9997

100-
## Callouts
101-
102-
### Applying Manifests from URLs: Considerations & Callouts
103-
104-
While this module supports applying manifests directly from remote `http://` or `https://` URLs, this method introduces complexities not present when using local files. For production environments, we recommend sourcing manifests from local paths or a version-controlled Git repository. Moreover, this method will be deprecated soon. Hence we recommend to use other methods to source manifests.
105-
106-
If you choose to use the URL method, be aware of the following potential issues and their solutions.
107-
108-
#### **1. Apply Order and Race Conditions**
109-
110-
The module applies manifests from the `apply_manifests` list in parallel. This can create a **race condition** if one manifest depends on another. The most common example is applying a manifest with custom resources (like a `ClusterQueue`) at the same time as the manifest that defines it (the `CustomResourceDefinition` or CRD).
111-
112-
There is **no guarantee** that the CRD will be applied before the resource that uses it. This can lead to non-deterministic deployment failures with errors like:
113-
114-
```Error: resource [kueue.x-k8s.io/v1beta1/ClusterQueue] isn't valid for cluster```
115-
116-
##### **Recommended Workaround: Two-Stage Apply**
117-
118-
To ensure a reliable deployment, you must manually enforce the correct order of operations.
119-
120-
1. **Initial Deployment:** In your blueprint, include **only** the manifest(s) containing the `CustomResourceDefinition` (CRD) resources in the `apply_manifests` list.
121-
122-
*Example `settings` for the first run:*
123-
124-
```yaml
125-
settings:
126-
apply_manifests:
127-
# This manifest contains the CRDs for Kueue
128-
- source: "https://raw.githubusercontent.com/GoogleCloudPlatform/cluster-toolkit/refs/heads/develop/modules/management/kubectl-apply/manifests/kueue-v0.11.4.yaml"
129-
server_side_apply: true
130-
```
131-
132-
2. **Run the deployment** (`gcluster deploy` or `terraform apply`).
133-
134-
3. **Second Deployment:** Once the first apply is successful, **add** the manifests containing your custom resources (like `ClusterQueue`, `LocalQueue`) to the list.
135-
136-
*Example `settings` for the second run:*
137-
138-
```yaml
139-
settings:
140-
apply_manifests:
141-
# The CRD manifest is still present
142-
- source: "https://raw.githubusercontent.com/GoogleCloudPlatform/cluster-toolkit/refs/heads/develop/modules/management/kubectl-apply/manifests/kueue-v0.11.4.yaml"
143-
server_side_apply: true
144-
145-
# Now, add your configuration manifest
146-
- source: "https://gist.githubusercontent.com/YourUser/..." # Your configuration URL
147-
server_side_apply: true
148-
```
149-
150-
4. **Run the deployment command again.** Since the CRDs are now guaranteed to exist in the cluster, this second apply will succeed reliably.
151-
152-
#### **2. Large Manifests (CRDs)**
153-
154-
* **Issue:** Applying very large manifests can fail with a `metadata.annotations: Too long` error.
155-
* **Solution:** Enable Server-Side Apply by setting `server_side_apply: true` for the manifest entry.
156-
157-
#### **3. Conflicts on Re-application**
158-
159-
* **Issue:** Re-running a deployment after a partial failure can cause server-side apply field manager `conflicts`.
160-
* **Solution:** Forcibly take ownership of the resource fields by setting `force_conflicts: true`.
161-
162-
#### **4. Terraform Template Files (`.tftpl`)**
163-
164-
* **Limitation:** This module **cannot** render a template file (`.tftpl`) when sourced from a remote URL.
165-
* **Workaround:** You must render the template into a pure YAML file locally, host that rendered file at a URL, and provide the URL of the rendered file in your blueprint.
166-
16798
## License
16899

169100
<!-- BEGINNING OF PRE-COMMIT-TERRAFORM DOCS HOOK -->
@@ -188,15 +119,13 @@ limitations under the License.
188119
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.3 |
189120
| <a name="requirement_google"></a> [google](#requirement\_google) | > 5.0 |
190121
| <a name="requirement_helm"></a> [helm](#requirement\_helm) | ~> 2.17 |
191-
| <a name="requirement_http"></a> [http](#requirement\_http) | ~> 3.0 |
192122
| <a name="requirement_kubectl"></a> [kubectl](#requirement\_kubectl) | >= 1.7.0 |
193123

194124
## Providers
195125

196126
| Name | Version |
197127
|------|---------|
198128
| <a name="provider_google"></a> [google](#provider\_google) | > 5.0 |
199-
| <a name="provider_http"></a> [http](#provider\_http) | ~> 3.0 |
200129
| <a name="provider_terraform"></a> [terraform](#provider\_terraform) | n/a |
201130

202131
## Modules
@@ -221,13 +150,12 @@ limitations under the License.
221150
| [terraform_data.kueue_validations](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource |
222151
| [google_client_config.default](https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/client_config) | data source |
223152
| [google_container_cluster.gke_cluster](https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/container_cluster) | data source |
224-
| [http_http.manifest_from_url](https://registry.terraform.io/providers/hashicorp/http/latest/docs/data-sources/http) | data source |
225153

226154
## Inputs
227155

228156
| Name | Description | Type | Default | Required |
229157
|------|-------------|------|---------|:--------:|
230-
| <a name="input_apply_manifests"></a> [apply\_manifests](#input\_apply\_manifests) | A list of manifests to apply to GKE cluster using kubectl. For more details see [kubectl module's inputs](kubectl/README.md).<br/> NOTE: The `enable` input acts as a FF to apply a manifest or not. By default it is always set to `true`. | <pre>list(object({<br/> enable = optional(bool, true)<br/> content = optional(string, null)<br/> source = optional(string, null)<br/> template_vars = optional(map(any), null)<br/> server_side_apply = optional(bool, false)<br/> wait_for_rollout = optional(bool, true)<br/> }))</pre> | `[]` | no |
158+
| <a name="input_apply_manifests"></a> [apply\_manifests](#input\_apply\_manifests) | A list of manifests to apply to GKE cluster using kubectl. For more details see [kubectl module's inputs](kubectl/README.md). | <pre>list(object({<br/> enable = optional(bool, true)<br/> content = optional(string, null)<br/> source = optional(string, null)<br/> template_vars = optional(map(any), null)<br/> server_side_apply = optional(bool, false)<br/> wait_for_rollout = optional(bool, true)<br/> }))</pre> | `[]` | no |
231159
| <a name="input_cluster_id"></a> [cluster\_id](#input\_cluster\_id) | An identifier for the gke cluster resource with format projects/<project\_id>/locations/<region>/clusters/<name>. | `string` | n/a | yes |
232160
| <a name="input_gib"></a> [gib](#input\_gib) | Install the NCCL gIB plugin | <pre>object({<br/> install = bool<br/> path = string<br/> template_vars = object({<br/> image = optional(string, "us-docker.pkg.dev/gce-ai-infra/gpudirect-gib/nccl-plugin-gib")<br/> version = string<br/> node_affinity = optional(any, {<br/> requiredDuringSchedulingIgnoredDuringExecution = {<br/> nodeSelectorTerms = [{<br/> matchExpressions = [{<br/> key = "cloud.google.com/gke-gpu",<br/> operator = "In",<br/> values = ["true"]<br/> }]<br/> }]<br/> }<br/> })<br/> accelerator_count = number<br/> })<br/> })</pre> | <pre>{<br/> "install": false,<br/> "path": "",<br/> "template_vars": {<br/> "accelerator_count": 0,<br/> "version": ""<br/> }<br/>}</pre> | no |
233161
| <a name="input_gke_cluster_exists"></a> [gke\_cluster\_exists](#input\_gke\_cluster\_exists) | A static flag that signals to downstream modules that a cluster has been created. Needed by community/modules/scripts/kubernetes-operations. | `bool` | `false` | no |

modules/management/kubectl-apply/kubectl/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ limitations under the License.
1818
| Name | Version |
1919
|------|---------|
2020
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.3 |
21+
| <a name="requirement_http"></a> [http](#requirement\_http) | ~> 3.0 |
2122
| <a name="requirement_kubectl"></a> [kubectl](#requirement\_kubectl) | >= 1.7.0 |
2223

2324
## Providers
2425

2526
| Name | Version |
2627
|------|---------|
28+
| <a name="provider_http"></a> [http](#provider\_http) | ~> 3.0 |
2729
| <a name="provider_kubectl"></a> [kubectl](#provider\_kubectl) | >= 1.7.0 |
2830

2931
## Modules
@@ -35,6 +37,7 @@ No modules.
3537
| Name | Type |
3638
|------|------|
3739
| [kubectl_manifest.apply_doc](https://registry.terraform.io/providers/gavinbunney/kubectl/latest/docs/resources/manifest) | resource |
40+
| [http_http.yaml_content](https://registry.terraform.io/providers/hashicorp/http/latest/docs/data-sources/http) | data source |
3841
| [kubectl_path_documents.templates](https://registry.terraform.io/providers/gavinbunney/kubectl/latest/docs/data-sources/path_documents) | data source |
3942
| [kubectl_path_documents.yamls](https://registry.terraform.io/providers/gavinbunney/kubectl/latest/docs/data-sources/path_documents) | data source |
4043

modules/management/kubectl-apply/kubectl/main.tf

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,44 +17,35 @@
1717
locals {
1818
yaml_separator = "\n---"
1919

20-
# This locals block processes manifest inputs from one of four methods,
21-
# evaluated in order of precedence using coalesce.
22-
23-
# --- METHOD 1: Direct Content Input ---
24-
# Used when manifest content is passed directly as a string.
2520
content_yaml_body = var.content
2621

27-
# Fallback for safe path checking in subsequent methods.
2822
null_safe_source = coalesce(var.source_path, " ")
2923

30-
# --- METHOD 2: Single Local YAML File ---
31-
# Used when var.source_path points to a local .yaml file.
32-
yaml_file = length(regexall("\\.yaml(_.*)?$", lower(local.null_safe_source))) == 1 ? abspath(var.source_path) : null
24+
url = startswith(local.null_safe_source, "http://") || startswith(local.null_safe_source, "https://") ? var.source_path : null
25+
url_content = local.url != null ? data.http.yaml_content[0].response_body : null
26+
27+
yaml_file = local.url == null && length(regexall("\\.yaml(_.*)?$", lower(local.null_safe_source))) == 1 ? abspath(var.source_path) : null
3328
yaml_file_content = local.yaml_file != null ? file(local.yaml_file) : null
3429

35-
# --- METHOD 3: Single Local Template File ---
36-
# Used when var.source_path points to a local .tftpl file.
37-
template_file = length(regexall("\\.tftpl(_.*)?$", lower(local.null_safe_source))) == 1 ? abspath(var.source_path) : null
30+
template_file = local.url == null && length(regexall("\\.tftpl(_.*)?$", lower(local.null_safe_source))) == 1 ? abspath(var.source_path) : null
3831
template_file_content = local.template_file != null ? templatefile(local.template_file, var.template_vars) : null
3932

40-
# --- CONSOLIDATE & PROCESS ---
41-
# Coalesce finds the first non-null content from the methods above.
42-
yaml_body = coalesce(local.content_yaml_body, local.yaml_file_content, local.template_file_content, " ")
43-
yaml_body_docs = [for doc in split(local.yaml_separator, local.yaml_body) : trimspace(doc) if length(trimspace(doc)) > 0]
33+
yaml_body = coalesce(local.content_yaml_body, local.url_content, local.yaml_file_content, local.template_file_content, " ")
34+
yaml_body_docs = [for doc in split(local.yaml_separator, local.yaml_body) : trimspace(doc) if length(trimspace(doc)) > 0] # Split yaml to single docs because the kubectl provider only supports single resource
4435

45-
# --- METHOD 4: Directory of Files ---
46-
# If no content was found via the methods above AND the source path looks like a directory,
47-
# we assume this is the desired method. The data blocks below will handle it.
4836
directory = length(local.yaml_body_docs) == 0 && endswith(local.null_safe_source, "/") ? abspath(var.source_path) : null
4937

50-
# --- FINAL AGGREGATION ---
51-
# Combine documents from single-source methods and directory-scan methods into one list.
5238
docs_list = concat(try(local.yaml_body_docs, []), try(data.kubectl_path_documents.yamls[0].documents, []), try(data.kubectl_path_documents.templates[0].documents, []))
5339
docs_map = tomap({
5440
for index, doc in local.docs_list : index => doc
5541
})
5642
}
5743

44+
data "http" "yaml_content" {
45+
count = local.url != null ? 1 : 0
46+
url = local.url
47+
}
48+
5849
data "kubectl_path_documents" "yamls" {
5950
count = local.directory != null ? 1 : 0
6051
pattern = "${local.directory}/*.yaml"

modules/management/kubectl-apply/kubectl/versions.tf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ terraform {
2020
source = "gavinbunney/kubectl"
2121
version = ">= 1.7.0"
2222
}
23+
http = {
24+
source = "hashicorp/http"
25+
version = "~> 3.0"
26+
}
2327
}
2428

2529
required_version = ">= 1.3"

modules/management/kubectl-apply/main.tf

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -20,34 +20,9 @@ locals {
2020
cluster_location = local.cluster_id_parts[3]
2121
project_id = var.project_id != null ? var.project_id : local.cluster_id_parts[1]
2222

23-
# 1. First, Identify manifests that are explicitly enabled.
24-
enabled_manifests = {
23+
apply_manifests_map = tomap({
2524
for index, manifest in var.apply_manifests : index => manifest
26-
if try(manifest.enable, true)
27-
}
28-
29-
# 2. Identify URL-based manifests
30-
url_manifests = {
31-
for index, manifest in local.enabled_manifests : index => manifest
32-
if try(manifest.source, "") != "" && (
33-
startswith(manifest.source, "http://") || startswith(manifest.source, "https://")
34-
)
35-
}
36-
37-
# 3. Rebuild the map by populating the 'content' field for URLs based manifest
38-
processed_apply_manifests_map = tomap({
39-
for index, manifest in local.enabled_manifests : tostring(index) => {
40-
# If this manifest was a URL, its content is the body from the HTTP call.
41-
content = contains(keys(local.url_manifests), tostring(index)) ? data.http.manifest_from_url[tostring(index)].body : manifest.content
42-
43-
# If this was a URL, its source path is now null. Otherwise, use original.
44-
source = contains(keys(local.url_manifests), tostring(index)) ? null : manifest.source
45-
46-
# Pass other vars
47-
template_vars = manifest.template_vars
48-
server_side_apply = manifest.server_side_apply
49-
wait_for_rollout = manifest.wait_for_rollout
50-
}
25+
if manifest.enable
5126
})
5227

5328
install_kueue = try(var.kueue.install, false)
@@ -59,11 +34,6 @@ locals {
5934
jobset_install_source = format("${path.module}/manifests/jobset-%s.yaml", try(var.jobset.version, ""))
6035
}
6136

62-
data "http" "manifest_from_url" {
63-
for_each = local.url_manifests
64-
url = each.value.source
65-
}
66-
6737
data "google_container_cluster" "gke_cluster" {
6838
project = local.project_id
6939
name = local.cluster_name
@@ -73,7 +43,7 @@ data "google_container_cluster" "gke_cluster" {
7343
data "google_client_config" "default" {}
7444

7545
module "kubectl_apply_manifests" {
76-
for_each = local.processed_apply_manifests_map
46+
for_each = local.apply_manifests_map
7747
source = "./kubectl"
7848
depends_on = [var.gke_cluster_exists]
7949

modules/management/kubectl-apply/variables.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ variable "cluster_id" {
7575
}
7676

7777
variable "apply_manifests" {
78-
description = "A list of manifests to apply to GKE cluster using kubectl. For more details see [kubectl module's inputs](kubectl/README.md).\n NOTE: The `enable` input acts as a FF to apply a manifest or not. By default it is always set to `true`. "
78+
description = "A list of manifests to apply to GKE cluster using kubectl. For more details see [kubectl module's inputs](kubectl/README.md)."
7979
type = list(object({
8080
enable = optional(bool, true)
8181
content = optional(string, null)

modules/management/kubectl-apply/versions.tf

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,6 @@ terraform {
2828
source = "hashicorp/helm"
2929
version = "~> 2.17"
3030
}
31-
http = {
32-
source = "hashicorp/http"
33-
version = "~> 3.0"
34-
}
3531
}
3632

3733
provider_meta "google" {

0 commit comments

Comments
 (0)