@rulebricks/cli 1.9.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lib/dns.js CHANGED
@@ -1,9 +1,19 @@
1
- import dns from "dns";
2
- import { promisify } from "util";
1
+ import * as dns from "dns";
3
2
  import { execa } from "execa";
4
3
  import { DEFAULT_NAMESPACE } from "../types/index.js";
5
- const resolve4 = promisify(dns.resolve4);
6
- const resolveCname = promisify(dns.resolveCname);
4
+ /**
5
+ * DNS resolvers to try in order:
6
+ * - null = system default
7
+ * - Google Public DNS
8
+ * - Cloudflare DNS
9
+ */
10
+ const DNS_RESOLVERS = [
11
+ null, // System default
12
+ ["8.8.8.8", "8.8.4.4"], // Google
13
+ ["1.1.1.1", "1.0.0.1"], // Cloudflare
14
+ ];
15
+ /** Timeout for each DNS lookup attempt (ms) */
16
+ const DNS_TIMEOUT_MS = 5000;
7
17
  /**
8
18
  * Helper to detect if a string is an IP address
9
19
  */
@@ -19,81 +29,153 @@ function cnameMatchesTarget(cnameRecords, expectedTarget) {
19
29
  r.replace(/\.$/, "") === expectedTarget.replace(/\.$/, ""));
20
30
  }
21
31
  /**
22
- * Checks if a DNS record resolves
32
+ * Wrap a promise with a timeout
23
33
  */
24
- export async function checkDNSRecord(hostname, expectedTarget) {
25
- try {
26
- // If expected target is a hostname (not an IP), check CNAME first
27
- if (expectedTarget && !isIPAddress(expectedTarget)) {
34
+ function withTimeout(promise, ms) {
35
+ return new Promise((resolve, reject) => {
36
+ const timer = setTimeout(() => reject(new Error("DNS lookup timeout")), ms);
37
+ promise
38
+ .then((result) => {
39
+ clearTimeout(timer);
40
+ resolve(result);
41
+ })
42
+ .catch((err) => {
43
+ clearTimeout(timer);
44
+ reject(err);
45
+ });
46
+ });
47
+ }
48
+ /**
49
+ * Create promisified resolver functions for a specific DNS server
50
+ */
51
+ function createResolver(servers) {
52
+ const resolver = new dns.Resolver();
53
+ if (servers) {
54
+ resolver.setServers(servers);
55
+ }
56
+ return {
57
+ resolve4: (hostname) => new Promise((resolve, reject) => {
58
+ resolver.resolve4(hostname, (err, addresses) => {
59
+ if (err)
60
+ reject(err);
61
+ else
62
+ resolve(addresses);
63
+ });
64
+ }),
65
+ resolveCname: (hostname) => new Promise((resolve, reject) => {
66
+ resolver.resolveCname(hostname, (err, addresses) => {
67
+ if (err)
68
+ reject(err);
69
+ else
70
+ resolve(addresses);
71
+ });
72
+ }),
73
+ };
74
+ }
75
+ /**
76
+ * Check DNS record with a specific resolver
77
+ */
78
+ async function checkWithResolver(hostname, expectedTarget, servers) {
79
+ const { resolve4, resolveCname } = createResolver(servers);
80
+ // If expected target is a hostname (not an IP), check CNAME first
81
+ if (expectedTarget && !isIPAddress(expectedTarget)) {
82
+ try {
83
+ const cnameRecords = await withTimeout(resolveCname(hostname), DNS_TIMEOUT_MS);
84
+ const matchesTarget = cnameMatchesTarget(cnameRecords, expectedTarget);
85
+ return {
86
+ resolved: true,
87
+ records: cnameRecords,
88
+ type: "CNAME",
89
+ matchesTarget,
90
+ };
91
+ }
92
+ catch {
93
+ // No CNAME record exists - try A record check
94
+ // For hostname targets, we need to resolve both the hostname and target
95
+ // to IPs and compare them
28
96
  try {
29
- const cnameRecords = await resolveCname(hostname);
30
- // CNAME records found - return the comparison result directly
31
- // Don't fall through to A record check, as that would incorrectly
32
- // compare IPs against a hostname target
33
- const matchesTarget = cnameMatchesTarget(cnameRecords, expectedTarget);
97
+ const [hostnameIPs, targetIPs] = await withTimeout(Promise.all([resolve4(hostname), resolve4(expectedTarget)]), DNS_TIMEOUT_MS);
98
+ const matchesTarget = hostnameIPs.some((ip) => targetIPs.includes(ip));
34
99
  return {
35
100
  resolved: true,
36
- records: cnameRecords,
37
- type: "CNAME",
101
+ records: hostnameIPs,
102
+ type: "A",
38
103
  matchesTarget,
39
104
  };
40
105
  }
41
106
  catch {
42
- // No CNAME record exists - try A record check
43
- // For hostname targets, we need to resolve both the hostname and target
44
- // to IPs and compare them
45
- try {
46
- const [hostnameIPs, targetIPs] = await Promise.all([
47
- resolve4(hostname),
48
- resolve4(expectedTarget),
49
- ]);
50
- // Check if any of the hostname's IPs match any of the target's IPs
51
- const matchesTarget = hostnameIPs.some((ip) => targetIPs.includes(ip));
52
- return {
53
- resolved: true,
54
- records: hostnameIPs,
55
- type: "A",
56
- matchesTarget,
57
- };
58
- }
59
- catch {
60
- // Could not resolve - DNS not configured
61
- return {
62
- resolved: false,
63
- records: [],
64
- type: null,
65
- matchesTarget: false,
66
- };
67
- }
107
+ return {
108
+ resolved: false,
109
+ records: [],
110
+ type: null,
111
+ matchesTarget: false,
112
+ };
68
113
  }
69
114
  }
70
- // Expected target is an IP address - check A records directly
115
+ }
116
+ // Expected target is an IP address - check A records directly
117
+ try {
118
+ const aRecords = await withTimeout(resolve4(hostname), DNS_TIMEOUT_MS);
119
+ const matchesTarget = expectedTarget
120
+ ? aRecords.some((r) => r === expectedTarget)
121
+ : true;
122
+ return {
123
+ resolved: true,
124
+ records: aRecords,
125
+ type: "A",
126
+ matchesTarget,
127
+ };
128
+ }
129
+ catch {
130
+ // Try CNAME if A fails (fallback for when no expected target)
71
131
  try {
72
- const aRecords = await resolve4(hostname);
132
+ const cnameRecords = await withTimeout(resolveCname(hostname), DNS_TIMEOUT_MS);
73
133
  const matchesTarget = expectedTarget
74
- ? aRecords.some((r) => r === expectedTarget)
134
+ ? cnameMatchesTarget(cnameRecords, expectedTarget)
75
135
  : true;
76
136
  return {
77
137
  resolved: true,
78
- records: aRecords,
79
- type: "A",
138
+ records: cnameRecords,
139
+ type: "CNAME",
80
140
  matchesTarget,
81
141
  };
82
142
  }
83
143
  catch {
84
- // Try CNAME if A fails (fallback for when no expected target)
85
- const cnameRecords = await resolveCname(hostname);
86
- const matchesTarget = expectedTarget
87
- ? cnameMatchesTarget(cnameRecords, expectedTarget)
88
- : true;
89
144
  return {
90
- resolved: true,
91
- records: cnameRecords,
92
- type: "CNAME",
93
- matchesTarget,
145
+ resolved: false,
146
+ records: [],
147
+ type: null,
148
+ matchesTarget: false,
94
149
  };
95
150
  }
96
151
  }
152
+ }
153
+ /**
154
+ * Checks if a DNS record resolves using multiple DNS resolvers for reliability.
155
+ * Tries system DNS first, then Google (8.8.8.8), then Cloudflare (1.1.1.1).
156
+ * Returns success if ANY resolver confirms the record matches the target.
157
+ */
158
+ export async function checkDNSRecord(hostname, expectedTarget) {
159
+ // Try each resolver in sequence until one succeeds with a matching target
160
+ for (const servers of DNS_RESOLVERS) {
161
+ try {
162
+ const result = await checkWithResolver(hostname, expectedTarget, servers);
163
+ // If we found a matching record, return immediately
164
+ if (result.resolved && result.matchesTarget) {
165
+ return result;
166
+ }
167
+ // If resolved but doesn't match, continue to next resolver
168
+ // (different resolvers might have different cache states)
169
+ }
170
+ catch {
171
+ // This resolver failed entirely, try the next one
172
+ }
173
+ }
174
+ // No resolver found a matching record - do one final check with system DNS
175
+ // to return whatever we can find (even if not matching)
176
+ try {
177
+ return await checkWithResolver(hostname, expectedTarget, null);
178
+ }
97
179
  catch {
98
180
  return {
99
181
  resolved: false,
@@ -16,7 +16,7 @@ export declare function fetchChartVersions(): Promise<ChartVersion[]>;
16
16
  */
17
17
  export declare function getInstalledVersion(releaseName: string, namespace: string): Promise<string | null>;
18
18
  /**
19
- * Installs the Rulebricks Helm chart
19
+ * Installs the Rulebricks Helm chart (use installOrUpgradeChart for idempotent operations)
20
20
  */
21
21
  export declare function installChart(deploymentName: string, options: {
22
22
  releaseName: string;
@@ -26,6 +26,19 @@ export declare function installChart(deploymentName: string, options: {
26
26
  timeout?: string;
27
27
  createNamespace?: boolean;
28
28
  }): Promise<void>;
29
+ /**
30
+ * Installs or upgrades the Rulebricks Helm chart (idempotent operation).
31
+ * Uses `helm upgrade --install` which will install if release doesn't exist,
32
+ * or upgrade if it does. This is safe to run multiple times.
33
+ */
34
+ export declare function installOrUpgradeChart(deploymentName: string, options: {
35
+ releaseName: string;
36
+ namespace: string;
37
+ version?: string;
38
+ wait?: boolean;
39
+ timeout?: string;
40
+ createNamespace?: boolean;
41
+ }): Promise<void>;
29
42
  /**
30
43
  * Upgrades the Rulebricks Helm chart
31
44
  */
package/dist/lib/helm.js CHANGED
@@ -102,7 +102,7 @@ export async function getInstalledVersion(releaseName, namespace) {
102
102
  }
103
103
  }
104
104
  /**
105
- * Installs the Rulebricks Helm chart
105
+ * Installs the Rulebricks Helm chart (use installOrUpgradeChart for idempotent operations)
106
106
  */
107
107
  export async function installChart(deploymentName, options) {
108
108
  const { releaseName, namespace, version, wait = true, timeout = "15m", createNamespace = true, } = options;
@@ -133,6 +133,41 @@ export async function installChart(deploymentName, options) {
133
133
  throw new Error(`Helm install failed:\n${getErrorMessage(error)}`);
134
134
  }
135
135
  }
136
+ /**
137
+ * Installs or upgrades the Rulebricks Helm chart (idempotent operation).
138
+ * Uses `helm upgrade --install` which will install if release doesn't exist,
139
+ * or upgrade if it does. This is safe to run multiple times.
140
+ */
141
+ export async function installOrUpgradeChart(deploymentName, options) {
142
+ const { releaseName, namespace, version, wait = true, timeout = "15m", createNamespace = true, } = options;
143
+ const valuesPath = getHelmValuesPath(deploymentName);
144
+ const args = [
145
+ "upgrade",
146
+ "--install", // This makes it idempotent - install if not exists, upgrade if exists
147
+ releaseName,
148
+ HELM_CHART_OCI,
149
+ "--namespace",
150
+ namespace,
151
+ "--values",
152
+ valuesPath,
153
+ ];
154
+ if (version) {
155
+ args.push("--version", version);
156
+ }
157
+ if (createNamespace) {
158
+ args.push("--create-namespace");
159
+ }
160
+ if (wait) {
161
+ args.push("--wait");
162
+ args.push("--timeout", timeout);
163
+ }
164
+ try {
165
+ await execa("helm", args);
166
+ }
167
+ catch (error) {
168
+ throw new Error(`Helm install/upgrade failed:\n${getErrorMessage(error)}`);
169
+ }
170
+ }
136
171
  /**
137
172
  * Upgrades the Rulebricks Helm chart
138
173
  */
@@ -435,6 +435,7 @@ export const VALID_LOG_COMPONENTS = [
435
435
  "kafka",
436
436
  "supabase",
437
437
  "traefik",
438
+ "redis",
438
439
  ];
439
440
  /**
440
441
  * Pod name patterns for each component.
@@ -447,6 +448,7 @@ const COMPONENT_POD_PATTERNS = {
447
448
  kafka: ["kafka"],
448
449
  supabase: ["supabase", "db", "postgres"],
449
450
  traefik: ["traefik"],
451
+ redis: ["redis", "dragonfly", "keydb"],
450
452
  };
451
453
  /**
452
454
  * Gets pods for a specific component in a deployment.
@@ -716,3 +716,93 @@ export declare function getNamespace(deploymentName: string): string;
716
716
  * Example: rulebricks-prod, rulebricks-staging
717
717
  */
718
718
  export declare function getReleaseName(deploymentName: string): string;
719
+ /** Benchmark test mode - QPS measures requests/sec, throughput measures solutions/sec */
720
+ export type BenchmarkTestMode = "qps" | "throughput";
721
+ /** Benchmark preset intensity level */
722
+ export type BenchmarkPreset = "light" | "medium" | "heavy" | "custom";
723
+ /** Configuration for a benchmark test run */
724
+ export interface BenchmarkConfig {
725
+ /** Name of the deployment being tested */
726
+ deploymentName: string;
727
+ /** Full API URL including flow slug (e.g., https://domain.com/api/v1/flows/benchmark-flow) */
728
+ apiUrl: string;
729
+ /** Rulebricks API key */
730
+ apiKey: string;
731
+ /** Test mode - qps or throughput */
732
+ testMode: BenchmarkTestMode;
733
+ /** Test duration (e.g., "2m", "4m") */
734
+ testDuration: string;
735
+ /** Target requests per second */
736
+ targetRps: number;
737
+ /** Bulk size - only for throughput mode (payloads per request) */
738
+ bulkSize?: number;
739
+ }
740
+ /** Result metrics from a benchmark test */
741
+ export interface BenchmarkMetrics {
742
+ /** Actual requests per second achieved */
743
+ actualRps: number;
744
+ /** Success rate as percentage (0-100) */
745
+ successRate: number;
746
+ /** P50 latency in milliseconds */
747
+ p50Latency: number;
748
+ /** P90 latency in milliseconds */
749
+ p90Latency: number;
750
+ /** P95 latency in milliseconds */
751
+ p95Latency: number;
752
+ /** P99 latency in milliseconds */
753
+ p99Latency: number;
754
+ /** Minimum latency in milliseconds */
755
+ minLatency: number;
756
+ /** Maximum latency in milliseconds */
757
+ maxLatency: number;
758
+ /** Average latency in milliseconds */
759
+ avgLatency: number;
760
+ /** Total requests made */
761
+ totalRequests: number;
762
+ /** Number of failed requests */
763
+ failedRequests: number;
764
+ /** Test duration in seconds */
765
+ testDuration: number;
766
+ /** Total data sent in bytes */
767
+ dataSent: number;
768
+ /** Total data received in bytes */
769
+ dataReceived: number;
770
+ /** Max virtual users used */
771
+ maxVUs: number;
772
+ /** For throughput tests: actual throughput (solutions/sec) */
773
+ actualThroughput?: number;
774
+ /** For throughput tests: total payloads processed */
775
+ totalPayloads?: number;
776
+ }
777
+ /** Result of a benchmark test run */
778
+ export interface BenchmarkResult {
779
+ /** Whether the test completed successfully */
780
+ success: boolean;
781
+ /** Path to the output directory */
782
+ outputDir: string;
783
+ /** Path to the HTML report */
784
+ reportPath: string;
785
+ /** Path to the JSON results */
786
+ resultsPath: string;
787
+ /** Parsed metrics from the test (if successful) */
788
+ metrics?: BenchmarkMetrics;
789
+ /** Error message if the test failed */
790
+ error?: string;
791
+ }
792
+ /** Preset configurations for QPS tests */
793
+ export declare const QPS_PRESETS: Record<Exclude<BenchmarkPreset, "custom">, {
794
+ targetRps: number;
795
+ testDuration: string;
796
+ label: string;
797
+ description: string;
798
+ }>;
799
+ /** Preset configurations for throughput tests */
800
+ export declare const THROUGHPUT_PRESETS: Record<Exclude<BenchmarkPreset, "custom">, {
801
+ targetRps: number;
802
+ bulkSize: number;
803
+ testDuration: string;
804
+ label: string;
805
+ description: string;
806
+ }>;
807
+ /** Cloud URLs that should be blocked from benchmarking */
808
+ export declare const BLOCKED_BENCHMARK_DOMAINS: string[];
@@ -554,3 +554,54 @@ export function getNamespace(deploymentName) {
554
554
  export function getReleaseName(deploymentName) {
555
555
  return `rulebricks-${deploymentName}`;
556
556
  }
557
+ /** Preset configurations for QPS tests */
558
+ export const QPS_PRESETS = {
559
+ light: {
560
+ targetRps: 100,
561
+ testDuration: "2m",
562
+ label: "Light",
563
+ description: "100 RPS for 2 minutes - quick validation",
564
+ },
565
+ medium: {
566
+ targetRps: 500,
567
+ testDuration: "4m",
568
+ label: "Medium",
569
+ description: "500 RPS for 4 minutes - standard load test",
570
+ },
571
+ heavy: {
572
+ targetRps: 1000,
573
+ testDuration: "4m",
574
+ label: "Heavy",
575
+ description: "1000 RPS for 4 minutes - stress test",
576
+ },
577
+ };
578
+ /** Preset configurations for throughput tests */
579
+ export const THROUGHPUT_PRESETS = {
580
+ light: {
581
+ targetRps: 50,
582
+ bulkSize: 25,
583
+ testDuration: "2m",
584
+ label: "Light",
585
+ description: "1,250 solutions/sec for 2 minutes",
586
+ },
587
+ medium: {
588
+ targetRps: 100,
589
+ bulkSize: 50,
590
+ testDuration: "4m",
591
+ label: "Medium",
592
+ description: "5,000 solutions/sec for 4 minutes",
593
+ },
594
+ heavy: {
595
+ targetRps: 200,
596
+ bulkSize: 100,
597
+ testDuration: "4m",
598
+ label: "Heavy",
599
+ description: "20,000 solutions/sec for 4 minutes",
600
+ },
601
+ };
602
+ /** Cloud URLs that should be blocked from benchmarking */
603
+ export const BLOCKED_BENCHMARK_DOMAINS = [
604
+ "api.rulebricks.com",
605
+ "rulebricks.io",
606
+ "app.rulebricks.com",
607
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulebricks/cli",
3
- "version": "1.9.0",
3
+ "version": "2.0.1",
4
4
  "description": "CLI for deploying and managing private Rulebricks instances",
5
5
  "type": "module",
6
6
  "bin": {
@@ -38,19 +38,19 @@
38
38
  "commander": "^12.1.0",
39
39
  "execa": "^8.0.1",
40
40
  "figures": "^6.1.0",
41
- "ink": "^5.0.1",
41
+ "ink": "^6.6.0",
42
42
  "ink-select-input": "^6.0.0",
43
43
  "ink-spinner": "^5.0.0",
44
44
  "ink-text-input": "^6.0.0",
45
45
  "node-fetch": "^3.3.2",
46
46
  "ora": "^8.0.1",
47
- "react": "^18.3.1",
47
+ "react": "^19.2.4",
48
48
  "yaml": "^2.4.5",
49
49
  "zod": "^3.23.8"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/node": "^20.14.10",
53
- "@types/react": "^18.3.3",
53
+ "@types/react": "^19.2.4",
54
54
  "typescript": "^5.5.3"
55
55
  },
56
56
  "engines": {
@@ -73,6 +73,6 @@
73
73
  "files": [
74
74
  "dist",
75
75
  "terraform",
76
- "email-templates"
76
+ "templates"
77
77
  ]
78
78
  }
@@ -185,6 +185,28 @@ module "eks" {
185
185
  }
186
186
  }
187
187
 
188
+ # Allow all node-to-node traffic for intra-cluster communication
189
+ # This ensures services on any port (including port 80 for serverless-redis-http) can communicate
190
+ node_security_group_additional_rules = {
191
+ ingress_self_all = {
192
+ description = "Node to node all ports/protocols"
193
+ protocol = "-1"
194
+ from_port = 0
195
+ to_port = 0
196
+ type = "ingress"
197
+ self = true
198
+ }
199
+ egress_all = {
200
+ description = "Node all egress"
201
+ protocol = "-1"
202
+ from_port = 0
203
+ to_port = 0
204
+ type = "egress"
205
+ cidr_blocks = ["0.0.0.0/0"]
206
+ ipv6_cidr_blocks = ["::/0"]
207
+ }
208
+ }
209
+
188
210
  tags = {
189
211
  Environment = "rulebricks"
190
212
  Terraform = "true"
@@ -138,6 +138,51 @@ resource "azurerm_subnet" "aks" {
138
138
  address_prefixes = ["10.240.0.0/16"]
139
139
  }
140
140
 
141
+ # Network Security Group for AKS subnet
142
+ # Allows all intra-VNet traffic for Kubernetes node-to-node communication
143
+ resource "azurerm_network_security_group" "aks" {
144
+ name = "${var.cluster_name}-nsg"
145
+ location = azurerm_resource_group.rg.location
146
+ resource_group_name = azurerm_resource_group.rg.name
147
+
148
+ # Allow all intra-VNet inbound traffic for Kubernetes
149
+ security_rule {
150
+ name = "AllowVNetInbound"
151
+ priority = 100
152
+ direction = "Inbound"
153
+ access = "Allow"
154
+ protocol = "*"
155
+ source_port_range = "*"
156
+ destination_port_range = "*"
157
+ source_address_prefix = "VirtualNetwork"
158
+ destination_address_prefix = "VirtualNetwork"
159
+ }
160
+
161
+ # Allow all intra-VNet outbound traffic for Kubernetes
162
+ security_rule {
163
+ name = "AllowVNetOutbound"
164
+ priority = 100
165
+ direction = "Outbound"
166
+ access = "Allow"
167
+ protocol = "*"
168
+ source_port_range = "*"
169
+ destination_port_range = "*"
170
+ source_address_prefix = "VirtualNetwork"
171
+ destination_address_prefix = "VirtualNetwork"
172
+ }
173
+
174
+ tags = {
175
+ Environment = "rulebricks"
176
+ Terraform = "true"
177
+ }
178
+ }
179
+
180
+ # Associate NSG with AKS subnet
181
+ resource "azurerm_subnet_network_security_group_association" "aks" {
182
+ subnet_id = azurerm_subnet.aks.id
183
+ network_security_group_id = azurerm_network_security_group.aks.id
184
+ }
185
+
141
186
  # User Assigned Identity for AKS
142
187
  resource "azurerm_user_assigned_identity" "aks" {
143
188
  name = "${var.cluster_name}-identity"
@@ -165,6 +165,37 @@ resource "google_compute_router_nat" "nat" {
165
165
  source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES"
166
166
  }
167
167
 
168
+ # Firewall rule to allow all internal traffic within the VPC
169
+ # This ensures services on any port can communicate between nodes
170
+ resource "google_compute_firewall" "allow_internal" {
171
+ name = "${var.cluster_name}-allow-internal"
172
+ network = google_compute_network.vpc.name
173
+
174
+ allow {
175
+ protocol = "tcp"
176
+ ports = ["0-65535"]
177
+ }
178
+
179
+ allow {
180
+ protocol = "udp"
181
+ ports = ["0-65535"]
182
+ }
183
+
184
+ allow {
185
+ protocol = "icmp"
186
+ }
187
+
188
+ # Allow traffic from nodes, pods, and services in the same VPC
189
+ source_ranges = [
190
+ google_compute_subnetwork.subnet.ip_cidr_range, # Node IPs (10.0.0.0/16)
191
+ google_compute_subnetwork.subnet.secondary_ip_range[0].ip_cidr_range, # Pod IPs (10.1.0.0/16)
192
+ google_compute_subnetwork.subnet.secondary_ip_range[1].ip_cidr_range # Service IPs (10.2.0.0/16)
193
+ ]
194
+
195
+ # Target all instances in the VPC
196
+ target_tags = ["gke-${var.cluster_name}"]
197
+ }
198
+
168
199
  # GKE Cluster
169
200
  resource "google_container_cluster" "cluster" {
170
201
  provider = google-beta
@@ -266,6 +297,9 @@ resource "google_container_node_pool" "primary" {
266
297
  tier = var.tier
267
298
  }
268
299
 
300
+ # Network tags for firewall rules
301
+ tags = ["gke-${var.cluster_name}"]
302
+
269
303
  workload_metadata_config {
270
304
  mode = "GKE_METADATA"
271
305
  }
File without changes
File without changes