From e00308d50a8d32b0bc40fde9fda91aea45ba8188 Mon Sep 17 00:00:00 2001 From: Sanmesh Kakade Date: Fri, 30 Jan 2026 14:36:21 +0530 Subject: [PATCH 01/13] add gateway api crd --- intents/gateway_api_crd/facets.yaml | 8 ++ modules/gateway_api_crd/k8s/1.0/facets.yaml | 50 ++++++++ modules/gateway_api_crd/k8s/1.0/main.tf | 119 +++++++++++++++++++ modules/gateway_api_crd/k8s/1.0/outputs.tf | 22 ++++ modules/gateway_api_crd/k8s/1.0/variables.tf | 33 +++++ outputs/gateway_api_crd/outputs.yaml | 19 +++ 6 files changed, 251 insertions(+) create mode 100644 intents/gateway_api_crd/facets.yaml create mode 100644 modules/gateway_api_crd/k8s/1.0/facets.yaml create mode 100644 modules/gateway_api_crd/k8s/1.0/main.tf create mode 100644 modules/gateway_api_crd/k8s/1.0/outputs.tf create mode 100644 modules/gateway_api_crd/k8s/1.0/variables.tf create mode 100644 outputs/gateway_api_crd/outputs.yaml diff --git a/intents/gateway_api_crd/facets.yaml b/intents/gateway_api_crd/facets.yaml new file mode 100644 index 00000000..d7e23c99 --- /dev/null +++ b/intents/gateway_api_crd/facets.yaml @@ -0,0 +1,8 @@ +name: gateway_api_crd +type: K8s +displayName: Gateway API CRD +description: Kubernetes Gateway API Custom Resource Definitions for advanced networking capabilities. +iconUrl: https://uploads-ssl.webflow.com/6252ef50a9f5d4afb6983bc3/669fa0ccb5a2964fe7f4d754_k8s_resource.svg +outputs: + - name: default + type: "@outputs/gateway_api_crd" \ No newline at end of file diff --git a/modules/gateway_api_crd/k8s/1.0/facets.yaml b/modules/gateway_api_crd/k8s/1.0/facets.yaml new file mode 100644 index 00000000..2a465521 --- /dev/null +++ b/modules/gateway_api_crd/k8s/1.0/facets.yaml @@ -0,0 +1,50 @@ +intent: gateway_api_crd +flavor: legacy +version: "1.0" +description: Installs Kubernetes Gateway API CRDs (Legacy module format) +clouds: + - aws + - azure + - gcp + - kubernetes +inputs: + kubernetes_details: + type: "@outputs/kubernetes" + optional: false + default: + resource_type: kubernetes_cluster + resource_name: default + providers: + - kubernetes + - kubernetes-alpha +outputs: + default: + type: "@outputs/gateway_api_crd" +spec: + title: Gateway API CRD Configuration + type: object + properties: + channel: + type: string + default: experimental + description: Gateway API release channel + enum: + - standard + - experimental + version: + type: string + default: v1.4.1 + description: Gateway API version to install + enum: + - v1.4.1 + - v1.4.0 + - v1.3.0 + - v1.2.0 +sample: + kind: gateway_api_crd + version: "1.0" + flavor: legacy + disabled: true + spec: + channel: experimental + version: v1.4.1 diff --git a/modules/gateway_api_crd/k8s/1.0/main.tf b/modules/gateway_api_crd/k8s/1.0/main.tf new file mode 100644 index 00000000..e4fbc8bf --- /dev/null +++ b/modules/gateway_api_crd/k8s/1.0/main.tf @@ -0,0 +1,119 @@ +locals { + name = lower(var.environment.namespace == "default" ? var.instance_name : "${var.environment.namespace}-${var.instance_name}") + namespace = var.environment.namespace + version = lookup(var.instance.spec, "version", "v1.4.1") + channel = lookup(var.instance.spec, "channel", "experimental") + + # Build the install URL based on version and channel + install_file = local.channel == "experimental" ? "experimental-install.yaml" : "standard-install.yaml" + install_url = "https://github.com/kubernetes-sigs/gateway-api/releases/download/${local.version}/${local.install_file}" + + # Tolerations: merge environment defaults with facets dedicated tolerations + tolerations = concat( + lookup(var.environment, "default_tolerations", []), + try(var.inputs.kubernetes_details.attributes.legacy_outputs.facets_dedicated_tolerations, []) + ) + + # Node selector from kubernetes_details legacy outputs + node_selector = try(var.inputs.kubernetes_details.attributes.legacy_outputs.facets_dedicated_node_selectors, {}) +} + +# ServiceAccount for Gateway API CRD installer Job +resource "kubernetes_service_account_v1" "gateway_api_crd_installer" { + metadata { + name = "${local.name}-gateway-api-crd-installer" + namespace = local.namespace + } +} + +# ClusterRole for Gateway API CRD installer +resource "kubernetes_cluster_role_v1" "gateway_api_crd_installer" { + metadata { + name = "${local.name}-gateway-api-crd-installer" + } + + rule { + api_groups = ["apiextensions.k8s.io"] + resources = ["customresourcedefinitions"] + verbs = ["get", "list", "create", "update", "patch"] + } +} + +# ClusterRoleBinding for Gateway API CRD installer +resource "kubernetes_cluster_role_binding_v1" "gateway_api_crd_installer" { + metadata { + name = "${local.name}-gateway-api-crd-installer" + } + + role_ref { + api_group = "rbac.authorization.k8s.io" + kind = "ClusterRole" + name = kubernetes_cluster_role_v1.gateway_api_crd_installer.metadata[0].name + } + + subject { + kind = "ServiceAccount" + name = kubernetes_service_account_v1.gateway_api_crd_installer.metadata[0].name + namespace = local.namespace + } +} + +# Job to install Gateway API CRDs +resource "kubernetes_job_v1" "gateway_api_crd_installer" { + metadata { + name = "${local.name}-gateway-api-crd-installer" + namespace = local.namespace + } + + spec { + template { + metadata { + labels = { + app = "gateway-api-crd-installer" + } + } + + spec { + service_account_name = kubernetes_service_account_v1.gateway_api_crd_installer.metadata[0].name + restart_policy = "OnFailure" + + # Node selector from kubernetes_details legacy outputs + node_selector = local.node_selector + + # Dynamic tolerations from environment and kubernetes_details + dynamic "toleration" { + for_each = local.tolerations + content { + key = toleration.value.key + operator = toleration.value.operator + value = lookup(toleration.value, "value", null) + effect = toleration.value.effect + } + } + + container { + name = "kubectl" + image = "bitnami/kubectl:latest" + command = ["/bin/sh", "-c"] + args = [ + # Using --server-side to avoid annotation size limit (262KB) + "kubectl apply --server-side -f ${local.install_url}" + ] + } + } + } + + backoff_limit = 3 + } + + wait_for_completion = true + + timeouts { + create = "5m" + update = "5m" + } + + depends_on = [ + kubernetes_cluster_role_binding_v1.gateway_api_crd_installer + ] +} diff --git a/modules/gateway_api_crd/k8s/1.0/outputs.tf b/modules/gateway_api_crd/k8s/1.0/outputs.tf new file mode 100644 index 00000000..4b800896 --- /dev/null +++ b/modules/gateway_api_crd/k8s/1.0/outputs.tf @@ -0,0 +1,22 @@ +locals { + output_attributes = { + version = local.version + channel = local.channel + install_url = local.install_url + job_name = kubernetes_job_v1.gateway_api_crd_installer.metadata[0].name + namespace = local.namespace + } + output_interfaces = {} +} + +output "version" { + value = local.version +} + +output "channel" { + value = local.channel +} + +output "install_url" { + value = local.install_url +} diff --git a/modules/gateway_api_crd/k8s/1.0/variables.tf b/modules/gateway_api_crd/k8s/1.0/variables.tf new file mode 100644 index 00000000..a695c7ce --- /dev/null +++ b/modules/gateway_api_crd/k8s/1.0/variables.tf @@ -0,0 +1,33 @@ +variable "cluster" { + type = any + default = {} +} + +variable "baseinfra" { + type = any + default = {} +} + +variable "cc_metadata" { + type = any + default = {} +} + +variable "instance" { + type = any +} + +variable "instance_name" { + type = string + default = "test_instance" +} + +variable "environment" { + type = any + default = {} +} + +variable "inputs" { + type = any + default = {} +} diff --git a/outputs/gateway_api_crd/outputs.yaml b/outputs/gateway_api_crd/outputs.yaml new file mode 100644 index 00000000..43e39d23 --- /dev/null +++ b/outputs/gateway_api_crd/outputs.yaml @@ -0,0 +1,19 @@ +name: "@outputs/gateway_api_crd" +properties: + attributes: + type: object + properties: + version: + type: string + channel: + type: string + install_url: + type: string + job_name: + type: string + namespace: + type: string + interfaces: + type: object + properties: {} +providers: [] From b4593495869d336a6f8a825029a5eda6e98076ea Mon Sep 17 00:00:00 2001 From: Sanmesh Kakade Date: Fri, 30 Jan 2026 14:43:16 +0530 Subject: [PATCH 02/13] add ingress of flavor nginx gateway fabric --- .../nginx_gateway_fabric_legacy/1.0/README.md | 664 ++++++++++ .../1.0/charts/nginx-gateway-fabric-2.3.0.tgz | Bin 0 -> 104025 bytes .../1.0/facets.yaml | 852 ++++++++++++ .../nginx_gateway_fabric_legacy/1.0/main.tf | 1158 +++++++++++++++++ .../1.0/outputs.tf | 102 ++ .../1.0/variables.tf | 37 + 6 files changed, 2813 insertions(+) create mode 100644 modules/ingress/nginx_gateway_fabric_legacy/1.0/README.md create mode 100644 modules/ingress/nginx_gateway_fabric_legacy/1.0/charts/nginx-gateway-fabric-2.3.0.tgz create mode 100644 modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml create mode 100644 modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf create mode 100644 modules/ingress/nginx_gateway_fabric_legacy/1.0/outputs.tf create mode 100644 modules/ingress/nginx_gateway_fabric_legacy/1.0/variables.tf diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/README.md b/modules/ingress/nginx_gateway_fabric_legacy/1.0/README.md new file mode 100644 index 00000000..4eee9bbd --- /dev/null +++ b/modules/ingress/nginx_gateway_fabric_legacy/1.0/README.md @@ -0,0 +1,664 @@ +# NGINX Gateway Fabric + +Kubernetes Gateway API implementation for advanced ingress traffic management. + +## Overview + +This module deploys **NGINX Gateway Fabric**, NGINX's implementation of the Kubernetes Gateway API specification. It provides a modern, declarative approach to configuring ingress traffic with native support for advanced routing features. + +### Features + +- **Gateway API Resources**: GatewayClass, Gateway, HTTPRoute, GRPCRoute +- **Advanced Routing**: Header matching, query parameter matching, HTTP method matching +- **URL Rewriting**: Path and hostname rewriting +- **Traffic Management**: Canary deployments, request mirroring +- **Multi-Domain Support**: Routes work across all configured domains +- **TLS Management**: Automatic SSL certificates via cert-manager (HTTP-01 and DNS-01) +- **Multi-Cloud**: AWS (NLB), Azure (LB), GCP (GCLB) +- **gRPC Support**: Native GRPCRoute resources +- **CORS**: Cross-origin resource sharing configuration +- **Observability**: Prometheus metrics via ServiceMonitor + +--- + +## Configuration + +### Basic Example + +```json +{ + "kind": "ingress", + "flavor": "nginx_gateway_fabric_legacy", + "version": "1.0", + "spec": { + "private": false, + "force_ssl_redirection": true, + "rules": { + "api": { + "service_name": "api-service", + "namespace": "default", + "port": "8080", + "path": "/api", + "path_type": "RegularExpression" + } + } + } +} +``` + +### Required Fields (per rule) + +| Field | Description | +|-------|-------------| +| `service_name` | Kubernetes service name | +| `namespace` | Service namespace | +| `port` | Service port number | +| `path` | URL path (required for HTTP routes) | + +### Path Type Options + +| Type | Default | Description | +|------|---------|-------------| +| `RegularExpression` | Yes | Auto-appends `.*` to path (e.g., `/api` becomes `/api.*`). Ensures longer paths match before shorter ones in NGINX. | +| `PathPrefix` | No | Matches paths starting with the specified prefix | +| `Exact` | No | Matches the exact path only | + +> **Note**: `RegularExpression` is the default because it ensures proper route ordering in NGINX. More specific paths (e.g., `/perform_login.*`) will match before catch-all patterns (e.g., `/.*`). + +--- + +## Routing Options + +### Header-Based Routing + +Route traffic based on HTTP headers: + +```json +{ + "rules": { + "api_v2": { + "service_name": "api-v2", + "namespace": "default", + "port": "8080", + "path": "/", + "path_type": "RegularExpression", + "header_matches": { + "version_header": { + "name": "X-API-Version", + "value": "v2", + "type": "Exact" + }, + "client_header": { + "name": "X-Client-Type", + "value": "mobile.*", + "type": "RegularExpression" + } + } + } + } +} +``` + +### Query Parameter Matching + +Route traffic based on query parameters: + +```json +{ + "rules": { + "api_beta": { + "service_name": "api-beta", + "namespace": "default", + "port": "8080", + "path": "/api", + "path_type": "RegularExpression", + "query_param_matches": { + "version_param": { + "name": "version", + "value": "beta", + "type": "Exact" + } + } + } + } +} +``` + +### HTTP Method Matching + +Route traffic based on HTTP method: + +```json +{ + "rules": { + "api_readonly": { + "service_name": "api-readonly", + "namespace": "default", + "port": "8080", + "path": "/api", + "path_type": "RegularExpression", + "method": "GET" + } + } +} +``` + +Options: `ALL` (default), `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS` + +--- + +## URL Rewriting + +Rewrite request URLs before forwarding to backend: + +```json +{ + "rules": { + "legacy_api": { + "service_name": "new-api-service", + "namespace": "default", + "port": "8080", + "path": "/old-api", + "path_type": "RegularExpression", + "url_rewrite": { + "rewrite_rule": { + "hostname": "internal-api.svc.cluster.local", + "path_type": "ReplacePrefixMatch", + "replace_path": "/new-api" + } + } + } + } +} +``` + +For full path replacement: + +```json +{ + "url_rewrite": { + "rewrite_rule": { + "path_type": "ReplaceFullPath", + "replace_path": "/v2/api" + } + } +} +``` + +--- + +## Header Modification + +### Request Headers + +Modify headers sent to backend: + +```json +{ + "rules": { + "api": { + "service_name": "api", + "namespace": "default", + "port": "8080", + "path": "/", + "path_type": "RegularExpression", + "request_header_modifier": { + "add": { + "custom_header": { + "name": "X-Custom-Header", + "value": "custom-value" + } + }, + "set": { + "source_header": { + "name": "X-Request-Source", + "value": "gateway" + } + }, + "remove": { + "sensitive_header": { + "name": "X-Sensitive-Header" + } + } + } + } + } +} +``` + +### Response Headers + +Modify headers sent to client: + +```json +{ + "response_header_modifier": { + "add": { + "response_id": { + "name": "X-Response-ID", + "value": "unique-id" + } + }, + "set": { + "cache_header": { + "name": "Cache-Control", + "value": "no-store" + } + }, + "remove": { + "server_header": { + "name": "Server" + } + } + } +} +``` + +> **Note**: Security headers (HSTS, X-Frame-Options, X-Content-Type-Options, X-XSS-Protection) are automatically added. + +--- + +## Request Timeouts + +Configure request and backend timeouts: + +```json +{ + "rules": { + "api": { + "service_name": "slow-api", + "namespace": "default", + "port": "8080", + "path": "/api", + "path_type": "RegularExpression", + "timeouts": { + "request": "60s", + "backend_request": "30s" + } + } + } +} +``` + +--- + +## CORS Configuration + +Enable Cross-Origin Resource Sharing: + +```json +{ + "rules": { + "api": { + "service_name": "api", + "namespace": "default", + "port": "8080", + "path": "/", + "path_type": "RegularExpression", + "cors": { + "enabled": true, + "allow_origins": { + "origin1": { + "origin": "https://example.com" + }, + "origin2": { + "origin": "https://app.example.com" + } + }, + "allow_methods": { + "get": { + "method": "GET" + }, + "post": { + "method": "POST" + } + }, + "allow_headers": { + "content_type": { + "header": "Content-Type" + }, + "auth": { + "header": "Authorization" + } + }, + "allow_credentials": true, + "max_age": 86400 + } + } + } +} +``` + +--- + +## gRPC Support + +### Route All gRPC Traffic + +```json +{ + "rules": { + "grpc_service": { + "service_name": "grpc-backend", + "namespace": "default", + "port": "50051", + "grpc_config": { + "enabled": true, + "match_all_methods": true + } + } + } +} +``` + +### Specific Method Matching + +```json +{ + "rules": { + "grpc_service": { + "service_name": "grpc-backend", + "namespace": "default", + "port": "50051", + "grpc_config": { + "enabled": true, + "match_all_methods": false, + "method_match": { + "get_user": { + "service": "myapp.v1.UserService", + "method": "GetUser", + "type": "Exact" + }, + "list_users": { + "service": "myapp.v1.UserService", + "method": "ListUsers", + "type": "Exact" + } + } + } + } + } +} +``` + +--- + +## Canary Deployments + +Split traffic between service versions: + +```json +{ + "rules": { + "api": { + "service_name": "api-v1", + "namespace": "default", + "port": "8080", + "path": "/", + "path_type": "RegularExpression", + "canary_deployment": { + "enabled": true, + "canary_service": "api-v2", + "canary_weight": 20 + } + } + } +} +``` + +This sends 20% of traffic to `api-v2` and 80% to `api-v1`. + +--- + +## Request Mirroring + +Mirror traffic to a secondary service for testing: + +```json +{ + "rules": { + "api": { + "service_name": "api-prod", + "namespace": "default", + "port": "8080", + "path": "/api", + "path_type": "RegularExpression", + "request_mirror": { + "service_name": "api-shadow", + "port": "8080", + "namespace": "testing" + } + } + } +} +``` + +--- + +## Multi-Domain Configuration + +### Custom Domains + +Configure custom domains at the root level: + +```json +{ + "kind": "ingress", + "flavor": "nginx_gateway_fabric_legacy", + "version": "1.0", + "domains": { + "production": { + "domain": "api.example.com", + "alias": "prod" + }, + "staging": { + "domain": "staging-api.example.com", + "alias": "staging", + "certificate_reference": "staging-tls" + } + }, + "spec": { + "private": false, + "disable_base_domain": true, + "force_ssl_redirection": true, + "rules": { + "api": { + "service_name": "api-service", + "namespace": "default", + "port": "8080", + "path": "/", + "path_type": "RegularExpression" + } + } + } +} +``` + +All routes are accessible on all domains: +- `https://api.example.com/` +- `https://staging-api.example.com/` + +### Domain Options + +| Field | Description | +|-------|-------------| +| `domain` | Full domain name | +| `alias` | Short identifier | +| `certificate_reference` | Existing TLS secret name (optional) | + +--- + +## TLS Certificate Management + +### HTTP-01 Validation (Default) + +Used when `disable_endpoint_validation: false` (default): + +- Creates bootstrap self-signed certificates for Gateway startup +- cert-manager replaces them with valid Let's Encrypt certificates +- Requires port 80 accessible from internet + +### DNS-01 Validation + +Used when `disable_endpoint_validation: true` or `private: true`: + +- Uses DNS challenges instead of HTTP +- Required for private/internal load balancers +- Requires cert-manager DNS provider configuration + +### Custom Certificates + +Use existing TLS certificates: + +```json +{ + "domains": { + "custom": { + "domain": "api.example.com", + "alias": "api", + "certificate_reference": "my-existing-tls-secret" + } + } +} +``` + +--- + +## Private Load Balancer + +Deploy with internal/private load balancer: + +```json +{ + "spec": { + "private": true, + "disable_endpoint_validation": true, + "force_ssl_redirection": true, + "rules": { + "api": { + "service_name": "api-service", + "namespace": "default", + "port": "8080", + "path": "/", + "path_type": "RegularExpression" + } + } + } +} +``` + +--- + +## Custom Helm Values + +Override default Helm configuration: + +```json +{ + "spec": { + "helm_values": { + "nginxGateway": { + "replicaCount": 3 + }, + "nginx": { + "config": { + "logging": { + "errorLevel": "debug" + } + } + } + } + } +} +``` + +See available values: https://github.com/nginxinc/nginx-gateway-fabric/blob/main/charts/nginx-gateway-fabric/values.yaml + +--- + +## Spec Options + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `private` | boolean | `false` | Use internal load balancer | +| `force_ssl_redirection` | boolean | `true` | Redirect HTTP to HTTPS | +| `disable_base_domain` | boolean | `false` | Disable auto-generated base domain | +| `disable_endpoint_validation` | boolean | `false` | Use DNS-01 instead of HTTP-01 | +| `domain_prefix_override` | string | - | Override auto-generated domain prefix | +| `renew_cert_before` | string | `720h` | Renew certificate before expiry | +| `helm_wait` | boolean | `true` | Wait for Helm release to be ready | +| `resources` | object | - | Controller resource limits/requests | +| `helm_values` | object | - | Additional Helm values | + +--- + +## Cloud Provider Support + +| Provider | Load Balancer | DNS | Features | +|----------|--------------|-----|----------| +| AWS | Network Load Balancer (NLB) | Route53 | Proxy Protocol v2, private LB | +| Azure | Azure Load Balancer | - | Internal LB support | +| GCP | Google Cloud Load Balancer | - | Internal LB with global access | + +--- + +## Outputs + +| Output | Description | +|--------|-------------| +| `domains` | Map of all configured domains | +| `domain` | Base domain (if not disabled) | +| `secure_endpoint` | HTTPS endpoint for base domain | +| `gateway_class` | GatewayClass name | +| `gateway_name` | Gateway resource name | +| `load_balancer_hostname` | LB hostname (for CNAME records) | +| `load_balancer_ip` | LB IP address (for A records) | + +--- + +## Not Supported + +| Feature | Reason | +|---------|--------| +| Rate Limiting | Not natively supported in NGF | +| IP Whitelisting | Not natively supported in NGF | +| Basic Auth | Not natively supported in NGF | + +--- + +## Troubleshooting + +### Check Gateway Status + +```bash +kubectl get gateway -n +kubectl describe gateway -n +``` + +### Check HTTPRoute Status + +```bash +kubectl get httproute -n +kubectl describe httproute -n +``` + +### Controller Logs + +```bash +kubectl logs -n -l app.kubernetes.io/name=nginx-gateway-fabric -c nginx-gateway +``` + +### Certificate Issues + +```bash +kubectl get certificate -n +kubectl describe certificate -n +``` + +--- + +## Resources + +- [NGINX Gateway Fabric Documentation](https://docs.nginx.com/nginx-gateway-fabric/) +- [Kubernetes Gateway API Specification](https://gateway-api.sigs.k8s.io/) +- [NGINX Gateway Fabric GitHub](https://github.com/nginxinc/nginx-gateway-fabric) diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/charts/nginx-gateway-fabric-2.3.0.tgz b/modules/ingress/nginx_gateway_fabric_legacy/1.0/charts/nginx-gateway-fabric-2.3.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..dbdf7d2cf532d17a59c1ce4adab31c333b25ac91 GIT binary patch literal 104025 zcmV)YK&-zXiwG0|00000|0w_~VMtOiV@ORlOnEsqVl!4SWK%V1T2nbTPgYhoO;>Dc zVQyr3R8em|NM&qo0POv3f7>>)Fb?l)eF_|<`)jhtl;k)MU0ru~FRz_8-c4Lxw%a}D zj&oXwge0sff+avZYLf5%|6%YzkOV18vYm8K^rDSP5Eu*wgPFlRBy*a)3+7mm8@vi; zc$(30c(lNo7_9Iz{=;qjMx)W_*^?*m|Iuhv{r~>x@$*0IKiz-4KiYr#{K@_wM*C0q zpFjBn8r{n1YkoNwIQzqB=dr4j`&HEuruNUrgM-1S7ZDz2G!^jZ z^!t<3|3cp@YC+!twe%KjNyca)M9Rm*;hc&^J{^Q?IRx4o`Y5bNL)o4ACc`%a^9>V{D*@fS{Y6 zd})#J^vje`v=b-KY#vQ^8Y7KpMBx~_wjpxC zi?gKHdw_xfT`UMXK0QZaoO3}k6fwzl5SyVD%QN&3%M(qDK~MkV7+Q|)^O%J=c6tSp zG-nJaq2y0U9txNyT>fK5=XnNf3(Z)D#DaJaCio^}F-l{c5EPL#W~-$ym^U@p!_E_updo`?N7FeJg8pmi# zP?!-cNQ5v)Db9rYfLg!@=*2b3R%LX+C>kZpoC~zT*92jdC-m2xppa!5;VDZZInUD2 z8Br(jz>i;8y)ByateA)G3KsGDm_TD&!WBod$>w$Up%hC)=MpL!RO#>7yAX zab){LvJ=@HI*}_-{Y=XcmPF18peM|^U`vFPgb7$Z-o9N@7D4wZiw=u##_0XW zYR@sAl9)@lrUcPugr4bYZWg-!oOLQ%tk7h0FlQY-|?k}>)p{n
w0$ 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 dvQYx*bxbOBTK;-Q4*;B= 60 ? substr(md5("${var.instance_name}-${var.environment.unique_name}"), 0, 20) : "${var.instance_name}-${var.environment.unique_name}" + check_domain_prefix = coalesce(lookup(var.instance.spec, "domain_prefix_override", null), local.instance_env_name) + base_domain = lower("${local.check_domain_prefix}.${var.cc_metadata.tenant_base_domain}") + base_subdomain = "*.${local.base_domain}" + name = lower(var.environment.namespace == "default" ? "${var.instance_name}" : "${var.environment.namespace}-${var.instance_name}") + dns_validation_secret_name = lower("nginx-gateway-fabric-cert-${local.name}") + gateway_class_name = local.name + + # Conditionally append base domain + # Note: Not setting certificate_reference - the listener uses dns_validation_secret_name as fallback + # This ensures base domain is included in certmanager_managed_domains for DNS-01 + add_base_domain = lookup(var.instance.spec, "disable_base_domain", false) ? {} : { + "facets" = { + "domain" = "${local.base_domain}" + "alias" = "base" + } + } + + domains = merge(lookup(var.instance, "domains", {}), local.add_base_domain) + + # List of all domain hostnames for HTTPRoutes + all_domain_hostnames = [for domain_key, domain in local.domains : domain.domain] + + # Filter rules + rulesFiltered = { + for k, v in local.rulesRaw : length(k) < 175 ? k : md5(k) => merge(v, { + host = lookup(v, "domain_prefix", null) == null || lookup(v, "domain_prefix", null) == "" ? "${local.base_domain}" : "${lookup(v, "domain_prefix", null)}.${local.base_domain}" + domain_key = "facets" + namespace = lookup(v, "namespace", var.environment.namespace) + }) + if( + (lookup(v, "port", null) != null && lookup(v, "port", null) != "") && + (lookup(v, "service_name", null) != null && lookup(v, "service_name", "") != "") && + ( + # gRPC routes don't need path/path_type - they use method matching + lookup(lookup(v, "grpc_config", {}), "enabled", false) || + # HTTP routes require path (path_type defaults to RegularExpression, with .* suffix auto-added) + (lookup(v, "path", null) != null && lookup(v, "path", "") != "") + ) && + (lookup(v, "disable", false) == false) + ) + } + + # Generate all unique hostnames from rules (domain_prefix + domain combinations) + # This is needed to create listeners for each hostname + all_route_hostnames = distinct(flatten([ + for rule_key, rule in local.rulesFiltered : [ + for domain_key, domain in local.domains : + lookup(rule, "domain_prefix", null) == null || lookup(rule, "domain_prefix", null) == "" ? + domain.domain : + "${lookup(rule, "domain_prefix", null)}.${domain.domain}" + ] + ])) + + # Hostnames that need additional listeners (not already covered by base domain listeners) + additional_hostnames = [ + for hostname in local.all_route_hostnames : + hostname if !contains(local.all_domain_hostnames, hostname) + ] + + # Map of additional hostnames to their config for listeners and certs + additional_hostname_configs = { + for hostname in local.additional_hostnames : + replace(replace(hostname, ".", "-"), "*", "wildcard") => { + hostname = hostname + secret_name = "${local.name}-${replace(replace(hostname, ".", "-"), "*", "wildcard")}-tls-cert" + } + } + + # Tolerations: merge environment defaults with facets dedicated tolerations + ingress_tolerations = concat( + lookup(var.environment, "default_tolerations", []), + try(var.inputs.kubernetes_details.attributes.legacy_outputs.facets_dedicated_tolerations, []) + ) + + # Node selector from kubernetes_details legacy outputs + nodepool_labels = try(var.inputs.kubernetes_details.attributes.legacy_outputs.facets_dedicated_node_selectors, {}) + + disable_endpoint_validation = lookup(var.instance.spec, "disable_endpoint_validation", false) || lookup(var.instance.spec, "private", false) + + # Common labels for all resources + # Note: app.kubernetes.io/instance must match the Helm release name for selector compatibility + common_labels = { + "app.kubernetes.io/name" = "nginx-gateway-fabric" + "app.kubernetes.io/instance" = "${local.name}-nginx-fabric" + "app.kubernetes.io/managed-by" = "facets" + "facets.cloud/module" = "nginx_gateway_fabric" + "facets.cloud/instance" = var.instance_name + } + + # Domains that need bootstrap TLS certificates for HTTP-01 validation + # Bootstrap cert is needed when HTTP-01 validation is used (disable_endpoint_validation = false) + # AND domain doesn't have certificate_reference (custom certs don't need bootstrap) + # Bootstrap cert is NOT needed for DNS-01 (uses dns_validation_secret_name) + bootstrap_tls_domains = { + for domain_key, domain in local.domains : + domain_key => domain + if !local.disable_endpoint_validation && can(domain.domain) && lookup(domain, "certificate_reference", "") == "" + } + + # Domains that need cert-manager to issue certificates + # Only domains WITHOUT certificate_reference - cert-manager should NOT manage domains with custom certs + # Applies to both HTTP-01 and DNS-01 validation + certmanager_managed_domains = { + for domain_key, domain in local.domains : + domain_key => domain + if can(domain.domain) && lookup(domain, "certificate_reference", "") == "" + } + + # Use gateway-shim only when ALL domains are managed by cert-manager + # When false (some domains have certificate_reference), we create explicit Certificate resources + use_gateway_shim = length(local.certmanager_managed_domains) == length(local.domains) + + # Cloud-specific service annotations + aws_annotations = merge( + lookup(var.instance.spec, "private", false) ? { + "service.beta.kubernetes.io/aws-load-balancer-scheme" = "internal" + "service.beta.kubernetes.io/aws-load-balancer-internal" = "true" + } : { + "service.beta.kubernetes.io/aws-load-balancer-scheme" = "internet-facing" + }, + { + "service.beta.kubernetes.io/aws-load-balancer-type" = "external" + "service.beta.kubernetes.io/aws-load-balancer-nlb-target-type" = "ip" + "service.beta.kubernetes.io/aws-load-balancer-backend-protocol" = "http" + "service.beta.kubernetes.io/aws-load-balancer-target-group-attributes" = lookup(var.instance.spec, "private", false) ? "proxy_protocol_v2.enabled=true,preserve_client_ip.enabled=false" : "proxy_protocol_v2.enabled=true,preserve_client_ip.enabled=true" + } + ) + + azure_annotations = lookup(var.instance.spec, "private", false) ? { + "service.beta.kubernetes.io/azure-load-balancer-internal" = "true" + } : {} + + gcp_annotations = lookup(var.instance.spec, "private", false) ? { + "cloud.google.com/load-balancer-type" = "Internal" + "networking.gke.io/load-balancer-type" = "Internal" + "networking.gke.io/internal-load-balancer-allow-global-access" = "true" + } : {} + + cloud_provider = upper(try(var.inputs.kubernetes_details.attributes.cloud_provider, "aws")) + + service_annotations = merge( + local.cloud_provider == "AWS" ? local.aws_annotations : {}, + local.cloud_provider == "AZURE" ? local.azure_annotations : {}, + local.cloud_provider == "GCP" ? local.gcp_annotations : {} + ) + + # Get ClusterIssuer names and config from cert-manager + cluster_issuer_dns = lookup(var.instance.spec, "dns_issuer", "gts-production") + cluster_issuer_gateway_http = "${local.name}-gateway-http01" + acme_email = lookup(var.instance.spec, "acme_email", "systems@facets.cloud") + + # Allow override of ClusterIssuer - useful for staging, custom issuers, or rate limit bypass + cluster_issuer_override = lookup(var.instance.spec, "cluster_issuer_override", null) + effective_cluster_issuer = coalesce( + local.cluster_issuer_override, + local.disable_endpoint_validation ? local.cluster_issuer_dns : local.cluster_issuer_gateway_http + ) + + # Security headers (always enabled with sensible defaults) + security_headers = { + "Strict-Transport-Security" = "max-age=31536000; includeSubDomains" + "X-Frame-Options" = "DENY" + "X-Content-Type-Options" = "nosniff" + "X-XSS-Protection" = "1; mode=block" + } + + # CORS headers per route + cors_headers = { + for k, v in local.rulesFiltered : k => merge( + lookup(lookup(v, "cors", {}), "enabled", false) ? { + "Access-Control-Allow-Origin" = join(", ", length(lookup(lookup(v, "cors", {}), "allow_origins", {})) > 0 ? + [for key, origin in lookup(lookup(v, "cors", {}), "allow_origins", {}) : origin.origin] : + ["*"] + ) + "Access-Control-Allow-Methods" = join(", ", length(lookup(lookup(v, "cors", {}), "allow_methods", {})) > 0 ? + [for key, m in lookup(lookup(v, "cors", {}), "allow_methods", {}) : m.method] : + ["GET", "POST", "PUT", "DELETE", "OPTIONS"] + ) + "Access-Control-Allow-Headers" = join(", ", length(lookup(lookup(v, "cors", {}), "allow_headers", {})) > 0 ? + [for key, h in lookup(lookup(v, "cors", {}), "allow_headers", {}) : h.header] : + ["Content-Type", "Authorization"] + ) + "Access-Control-Max-Age" = tostring(lookup(lookup(v, "cors", {}), "max_age", 86400)) + } : {}, + lookup(lookup(v, "cors", {}), "allow_credentials", false) ? { + "Access-Control-Allow-Credentials" = "true" + } : {} + ) + } + + # HTTP to HTTPS Redirect Route (only created when force_ssl_redirection is enabled) + # Single route that handles ALL HTTP (port 80) traffic and redirects to HTTPS + # MUST NOT have backendRefs - only RequestRedirect filter + http_redirect_resources = lookup(var.instance.spec, "force_ssl_redirection", false) ? { + "httproute-redirect-${local.name}" = { + apiVersion = "gateway.networking.k8s.io/v1" + kind = "HTTPRoute" + metadata = { + name = "${local.name}-http-redirect" + namespace = var.environment.namespace + } + spec = { + parentRefs = [{ + name = local.name + namespace = var.environment.namespace + sectionName = "http" # Reference HTTP listener (port 80) + }] + + rules = [{ + matches = [{ + path = { + type = "PathPrefix" + value = "/" + } + }] + filters = [{ + type = "RequestRedirect" + requestRedirect = { + scheme = "https" + statusCode = 301 + } + }] + # No backendRefs - redirect only + }] + } + } + } : {} + + # HTTPRoute Resources (HTTPS traffic - port 443) + # Note: GatewayClass, Gateway, and NginxProxy are created by the Helm chart + httproute_resources = { + for k, v in local.rulesFiltered : "httproute-${lower(var.instance_name)}-${k}" => { + apiVersion = "gateway.networking.k8s.io/v1" + kind = "HTTPRoute" + metadata = { + name = "${lower(var.instance_name)}-${k}" + namespace = var.environment.namespace + } + spec = { + # Reference the correct listener(s) for this route's hostnames + # If route has domain_prefix, reference the additional hostname listeners + # If route has no domain_prefix, reference the base domain listeners + parentRefs = lookup(v, "domain_prefix", null) == null || lookup(v, "domain_prefix", null) == "" ? [ + # No domain_prefix - use base domain listeners + for domain_key, domain in local.domains : { + name = local.name + namespace = var.environment.namespace + sectionName = "https-${domain_key}" + } + ] : [ + # Has domain_prefix - use additional hostname listeners + for domain_key, domain in local.domains : { + name = local.name + namespace = var.environment.namespace + sectionName = "https-${replace(replace("${lookup(v, "domain_prefix", null)}.${domain.domain}", ".", "-"), "*", "wildcard")}" + } + ] + + # Include all domains in hostnames - Gateway API supports multiple hostnames per route + hostnames = distinct([ + for domain_key, domain in local.domains : + lookup(v, "domain_prefix", null) == null || lookup(v, "domain_prefix", null) == "" ? + domain.domain : + "${lookup(v, "domain_prefix", null)}.${domain.domain}" + ]) + + rules = [{ + matches = concat( + # Path matching (with optional method and query params) + # Default: RegularExpression with .* suffix (e.g., /path becomes /path.*) + # This ensures proper regex ordering in NGINX (longer patterns match first) + [merge( + { + path = { + type = lookup(v, "path_type", "RegularExpression") + value = lookup(v, "path_type", "RegularExpression") == "RegularExpression" ? "${lookup(v, "path", "/")}.*" : lookup(v, "path", "/") + } + }, + # Method matching (ALL or null means match all methods) + lookup(v, "method", null) != null && lookup(v, "method", "ALL") != "ALL" ? { + method = v.method + } : {}, + # Query parameter matching + length(lookup(v, "query_param_matches", {})) > 0 ? { + queryParams = [ + for key, qp in v.query_param_matches : { + name = qp.name + value = qp.value + type = lookup(qp, "type", "Exact") + } + ] + } : {}, + # Header matching + length(lookup(v, "header_matches", {})) > 0 ? { + headers = [ + for key, header in v.header_matches : { + name = header.name + value = header.value + type = lookup(header, "type", "Exact") + } + ] + } : {} + )] + ) + + filters = concat( + # Static filters + [ + for filter in [ + # Request header modification + lookup(v, "request_header_modifier", null) != null ? { + type = "RequestHeaderModifier" + requestHeaderModifier = merge( + lookup(v.request_header_modifier, "add", null) != null ? { + add = [for key, header in v.request_header_modifier.add : { name = header.name, value = header.value }] + } : {}, + lookup(v.request_header_modifier, "set", null) != null ? { + set = [for key, header in v.request_header_modifier.set : { name = header.name, value = header.value }] + } : {}, + lookup(v.request_header_modifier, "remove", null) != null ? { + remove = [for key, header in v.request_header_modifier.remove : header.name] + } : {} + ) + } : null, + + # Response header modification (security headers + CORS + custom headers) + { + type = "ResponseHeaderModifier" + responseHeaderModifier = merge( + { + add = [for name, value in merge( + local.security_headers, + { for key, header in lookup(lookup(v, "response_header_modifier", {}), "add", {}) : header.name => header.value }, + local.cors_headers[k] + ) : { name = name, value = value }] + }, + lookup(lookup(v, "response_header_modifier", {}), "set", null) != null ? { + set = [for key, header in v.response_header_modifier.set : { name = header.name, value = header.value }] + } : {}, + lookup(lookup(v, "response_header_modifier", {}), "remove", null) != null ? { + remove = [for key, header in v.response_header_modifier.remove : header.name] + } : {} + ) + }, + + # Request mirroring + lookup(v, "request_mirror", null) != null ? { + type = "RequestMirror" + requestMirror = { + backendRef = { + name = v.request_mirror.service_name + port = tonumber(v.request_mirror.port) + namespace = lookup(v.request_mirror, "namespace", v.namespace) + } + } + } : null + # Note: SSL redirection is handled by separate http_redirect_resources HTTPRoutes + # RequestRedirect filter cannot be used together with backendRefs in the same rule + ] : filter if filter != null + ], + # URL rewriting (from patternProperties) + [ + for key, rewrite in lookup(v, "url_rewrite", {}) : { + type = "URLRewrite" + urlRewrite = merge( + lookup(rewrite, "hostname", null) != null ? { + hostname = rewrite.hostname + } : {}, + lookup(rewrite, "path_type", null) != null && lookup(rewrite, "replace_path", null) != null ? { + path = merge( + { type = rewrite.path_type }, + rewrite.path_type == "ReplaceFullPath" ? { + replaceFullPath = rewrite.replace_path + } : {}, + rewrite.path_type == "ReplacePrefixMatch" ? { + replacePrefixMatch = rewrite.replace_path + } : {} + ) + } : {} + ) + } + ] + ) + + # Request/backend timeouts - default 300s (equivalent to proxy-read-timeout/proxy-send-timeout) + timeouts = { + request = lookup(lookup(v, "timeouts", {}), "request", "300s") + backendRequest = lookup(lookup(v, "timeouts", {}), "backend_request", "300s") + } + + backendRefs = concat( + # Primary backend + [{ + name = v.service_name + port = tonumber(v.port) + weight = lookup(lookup(v, "canary_deployment", {}), "enabled", false) ? 100 - lookup(lookup(v, "canary_deployment", {}), "canary_weight", 10) : 100 + namespace = v.namespace + }], + # Canary backend (if enabled) + lookup(lookup(v, "canary_deployment", {}), "enabled", false) ? [{ + name = lookup(lookup(v, "canary_deployment", {}), "canary_service", "") + port = tonumber(v.port) + weight = lookup(lookup(v, "canary_deployment", {}), "canary_weight", 10) + namespace = v.namespace + }] : [] + ) + }] + } + } if !lookup(lookup(v, "grpc_config", {}), "enabled", false) + } + + # GRPCRoute Resources + grpcroute_resources = { + for k, v in local.rulesFiltered : "grpcroute-${lower(var.instance_name)}-${k}" => { + apiVersion = "gateway.networking.k8s.io/v1" + kind = "GRPCRoute" + metadata = { + name = "${lower(var.instance_name)}-${k}-grpc" + namespace = var.environment.namespace + } + spec = { + # Reference the correct listener(s) for this route's hostnames + # If route has domain_prefix, reference the additional hostname listeners + # If route has no domain_prefix, reference the base domain listeners + parentRefs = lookup(v, "domain_prefix", null) == null || lookup(v, "domain_prefix", null) == "" ? [ + # No domain_prefix - use base domain listeners + for domain_key, domain in local.domains : { + name = local.name + namespace = var.environment.namespace + sectionName = "https-${domain_key}" + } + ] : [ + # Has domain_prefix - use additional hostname listeners + for domain_key, domain in local.domains : { + name = local.name + namespace = var.environment.namespace + sectionName = "https-${replace(replace("${lookup(v, "domain_prefix", null)}.${domain.domain}", ".", "-"), "*", "wildcard")}" + } + ] + + # Include all domains in hostnames - Gateway API supports multiple hostnames per route + hostnames = distinct([ + for domain_key, domain in local.domains : + lookup(v, "domain_prefix", null) == null || lookup(v, "domain_prefix", null) == "" ? + domain.domain : + "${lookup(v, "domain_prefix", null)}.${domain.domain}" + ]) + + rules = [{ + # If match_all_methods is true (default) or method_match is empty, match all gRPC traffic + matches = !lookup(lookup(v, "grpc_config", {}), "match_all_methods", true) && lookup(lookup(v, "grpc_config", {}), "method_match", null) != null ? [ + for key, method in lookup(v.grpc_config, "method_match", {}) : { + method = { + type = lookup(method, "type", "Exact") + service = lookup(method, "service", "") + method = lookup(method, "method", "") + } + } + ] : [] + + backendRefs = [{ + name = v.service_name + port = tonumber(v.port) + namespace = v.namespace + }] + }] + } + } if lookup(lookup(v, "grpc_config", {}), "enabled", false) + } + + # ServiceMonitor (always enabled with defaults) + servicemonitor_resources = { + "servicemonitor-${local.name}" = { + apiVersion = "monitoring.coreos.com/v1" + kind = "ServiceMonitor" + metadata = { + name = "${local.name}-gateway-metrics" + namespace = var.environment.namespace + labels = { + prometheus = "kube-prometheus" + } + } + spec = { + selector = { + matchLabels = { + "app.kubernetes.io/name" = "nginx-gateway-fabric" + "app.kubernetes.io/instance" = "${local.name}-nginx-fabric" + } + } + endpoints = [{ + port = "metrics" + interval = "30s" + path = "/metrics" + }] + } + } + } + + # Collect unique namespaces that need ReferenceGrants (for cross-namespace backends) + cross_namespace_backends = { + for k, v in local.rulesFiltered : v.namespace => v.namespace + if v.namespace != var.environment.namespace + } + + # ReferenceGrant resources for cross-namespace backends + # Allows HTTPRoutes in Gateway namespace to reference Services in other namespaces + referencegrant_resources = { + for ns in local.cross_namespace_backends : "referencegrant-${ns}" => { + apiVersion = "gateway.networking.k8s.io/v1beta1" + kind = "ReferenceGrant" + metadata = { + name = "${local.name}-allow-routes" + namespace = ns + } + spec = { + from = [{ + group = "gateway.networking.k8s.io" + kind = "HTTPRoute" + namespace = var.environment.namespace + }] + to = [{ + group = "" + kind = "Service" + }] + } + } + } + + # ClientSettingsPolicy - applies body size limit to all traffic through the Gateway + # Equivalent to nginx.ingress.kubernetes.io/proxy-body-size + clientsettingspolicy_resources = { + "clientsettingspolicy-${local.name}" = { + apiVersion = "gateway.nginx.org/v1alpha1" + kind = "ClientSettingsPolicy" + metadata = { + name = "${local.name}-client-settings" + namespace = var.environment.namespace + } + spec = { + targetRef = { + group = "gateway.networking.k8s.io" + kind = "Gateway" + name = local.name + } + body = { + maxSize = lookup(var.instance.spec, "body_size", "150m") + } + } + } + } + + # Merge all Gateway API resources + gateway_api_resources = merge( + local.http_redirect_resources, + local.httproute_resources, + local.grpcroute_resources, + local.servicemonitor_resources, + local.referencegrant_resources, + local.clientsettingspolicy_resources + ) +} + +# Bootstrap TLS Private Key for HTTP-01 validation +# Creates a temporary self-signed cert so Gateway 443 listener can start +# cert-manager will overwrite this secret once HTTP-01 challenge succeeds +resource "tls_private_key" "bootstrap" { + for_each = local.bootstrap_tls_domains + algorithm = "RSA" + rsa_bits = 2048 +} + +resource "tls_self_signed_cert" "bootstrap" { + for_each = local.bootstrap_tls_domains + private_key_pem = tls_private_key.bootstrap[each.key].private_key_pem + + subject { + common_name = each.value.domain + } + + validity_period_hours = 8760 # 1 year + + dns_names = [ + each.value.domain, + "*.${each.value.domain}" + ] + + allowed_uses = [ + "key_encipherment", + "digital_signature", + "server_auth" + ] +} + +resource "kubernetes_secret" "bootstrap_tls" { + for_each = local.bootstrap_tls_domains + + metadata { + name = "${local.name}-${each.key}-tls-cert" + namespace = var.environment.namespace + } + + data = { + "tls.crt" = tls_self_signed_cert.bootstrap[each.key].cert_pem + "tls.key" = tls_private_key.bootstrap[each.key].private_key_pem + } + + type = "kubernetes.io/tls" + + lifecycle { + ignore_changes = [data, metadata[0].annotations, metadata[0].labels] + } +} + +# Bootstrap TLS for additional hostnames (from domain_prefix in rules) +# Only created for HTTP-01 validation (when disable_endpoint_validation is false) +resource "tls_private_key" "bootstrap_additional" { + for_each = local.disable_endpoint_validation ? {} : local.additional_hostname_configs + algorithm = "RSA" + rsa_bits = 2048 +} + +resource "tls_self_signed_cert" "bootstrap_additional" { + for_each = local.disable_endpoint_validation ? {} : local.additional_hostname_configs + private_key_pem = tls_private_key.bootstrap_additional[each.key].private_key_pem + + subject { + common_name = each.value.hostname + } + + validity_period_hours = 8760 # 1 year + + dns_names = [each.value.hostname] + + allowed_uses = [ + "key_encipherment", + "digital_signature", + "server_auth" + ] +} + +resource "kubernetes_secret" "bootstrap_tls_additional" { + for_each = local.disable_endpoint_validation ? {} : local.additional_hostname_configs + + metadata { + name = each.value.secret_name + namespace = var.environment.namespace + } + + data = { + "tls.crt" = tls_self_signed_cert.bootstrap_additional[each.key].cert_pem + "tls.key" = tls_private_key.bootstrap_additional[each.key].private_key_pem + } + + type = "kubernetes.io/tls" + + lifecycle { + ignore_changes = [data, metadata[0].annotations, metadata[0].labels] + } +} + +# Explicit Certificate resource for DNS-01 managed domains +# Created when NOT using gateway-shim (i.e., when some domains have certificate_reference) +# This ensures cert-manager only manages domains that need it, leaving custom certs alone +# For DNS-01, all managed domains share the same wildcard certificate (dns_validation_secret_name) +module "dns01_certificate" { + count = (!local.use_gateway_shim && local.disable_endpoint_validation && length(local.certmanager_managed_domains) > 0) ? 1 : 0 + + source = "github.com/Facets-cloud/facets-utility-modules//any-k8s-resource" + name = "${local.name}-dns01-certificate" + namespace = var.environment.namespace + advanced_config = {} + + data = { + apiVersion = "cert-manager.io/v1" + kind = "Certificate" + metadata = { + name = "${local.name}-dns01-cert" + namespace = var.environment.namespace + } + spec = { + secretName = local.dns_validation_secret_name + issuerRef = { + name = local.effective_cluster_issuer + kind = "ClusterIssuer" + } + # Include all managed domains (no wildcards - wildcards cause issues with cnameStrategy: Follow) + dnsNames = [ + for domain in values(local.certmanager_managed_domains) : domain.domain + ] + renewBefore = lookup(var.instance.spec, "renew_cert_before", "720h") + } + } + + depends_on = [helm_release.nginx_gateway_fabric] +} + +# Explicit Certificate resources for HTTP-01 managed domains +# Created when NOT using gateway-shim (i.e., when some domains have certificate_reference) +# For HTTP-01, each domain needs its own Certificate (unlike DNS-01 which can share) +module "http01_certificate" { + for_each = !local.use_gateway_shim && !local.disable_endpoint_validation ? local.certmanager_managed_domains : {} + + source = "github.com/Facets-cloud/facets-utility-modules//any-k8s-resource" + name = "${local.name}-http01-cert-${each.key}" + namespace = var.environment.namespace + advanced_config = {} + + data = { + apiVersion = "cert-manager.io/v1" + kind = "Certificate" + metadata = { + name = "${local.name}-http01-cert-${each.key}" + namespace = var.environment.namespace + } + spec = { + secretName = "${local.name}-${each.key}-tls-cert" + issuerRef = { + name = local.cluster_issuer_gateway_http + kind = "ClusterIssuer" + } + dnsNames = [ + each.value.domain + ] + renewBefore = lookup(var.instance.spec, "renew_cert_before", "720h") + } + } + + depends_on = [ + helm_release.nginx_gateway_fabric, + module.cluster-issuer-gateway-http01 + ] +} + +# Name module for additional hostname certificates (keeps helm release names under 53 chars) +# Only created when NOT using gateway-shim (same as http01_certificate for base domains) +module "http01_certificate_additional_name" { + for_each = !local.use_gateway_shim && !local.disable_endpoint_validation ? local.additional_hostname_configs : {} + + source = "github.com/Facets-cloud/facets-utility-modules//name" + environment = var.environment + limit = 53 + globally_unique = true + resource_name = "${local.name}-cert-${each.key}" + resource_type = "certificate" + is_k8s = true +} + +# Explicit Certificate resources for additional hostnames (domain_prefix + domain) +# Created when NOT using gateway-shim (gateway-shim handles certs automatically when enabled) +module "http01_certificate_additional" { + for_each = !local.use_gateway_shim && !local.disable_endpoint_validation ? local.additional_hostname_configs : {} + + source = "github.com/Facets-cloud/facets-utility-modules//any-k8s-resource" + name = module.http01_certificate_additional_name[each.key].name + namespace = var.environment.namespace + advanced_config = {} + + data = { + apiVersion = "cert-manager.io/v1" + kind = "Certificate" + metadata = { + name = module.http01_certificate_additional_name[each.key].name + namespace = var.environment.namespace + } + spec = { + secretName = each.value.secret_name + issuerRef = { + name = local.cluster_issuer_gateway_http + kind = "ClusterIssuer" + } + dnsNames = [ + each.value.hostname + ] + renewBefore = lookup(var.instance.spec, "renew_cert_before", "720h") + } + } + + depends_on = [ + helm_release.nginx_gateway_fabric, + module.cluster-issuer-gateway-http01 + ] +} + +# NGINX Gateway Fabric Helm Chart +# Note: Gateway API CRDs are installed by the gateway_api_crd module (dependency) +resource "helm_release" "nginx_gateway_fabric" { + name = "${local.name}-nginx-fabric" + wait = lookup(var.instance.spec, "helm_wait", true) + chart = "${path.module}/charts/nginx-gateway-fabric-2.3.0.tgz" + namespace = var.environment.namespace + max_history = 10 + skip_crds = false + create_namespace = false + timeout = 600 + + values = [ + yamlencode({ + # Use release-specific TLS secret names to support multiple instances in the same namespace + certGenerator = { + serverTLSSecretName = "${local.name}-server-tls" + agentTLSSecretName = "${local.name}-agent-tls" + overwrite = true + tolerations = local.ingress_tolerations + nodeSelector = local.nodepool_labels + } + + nginxGateway = { + # Configure the GatewayClass name + gatewayClassName = local.gateway_class_name + + # Labels for control plane deployment + labels = local.common_labels + + image = { + repository = "facetscloud/nginx-gateway-fabric" + tag = "2.3.0" + pullPolicy = "IfNotPresent" + } + imagePullSecrets = lookup(var.inputs, "artifactories", null) != null ? var.inputs.artifactories.attributes.registry_secrets_list : [] + + # Control plane resources + resources = { + requests = { + cpu = lookup(lookup(lookup(lookup(var.instance.spec, "control_plane", {}), "resources", {}), "requests", {}), "cpu", "200m") + memory = lookup(lookup(lookup(lookup(var.instance.spec, "control_plane", {}), "resources", {}), "requests", {}), "memory", "256Mi") + } + limits = { + cpu = lookup(lookup(lookup(lookup(var.instance.spec, "control_plane", {}), "resources", {}), "limits", {}), "cpu", "500m") + memory = lookup(lookup(lookup(lookup(var.instance.spec, "control_plane", {}), "resources", {}), "limits", {}), "memory", "512Mi") + } + } + + # Control plane autoscaling - always enabled + autoscaling = { + enable = true + minReplicas = lookup(lookup(lookup(var.instance.spec, "control_plane", {}), "scaling", {}), "min_replicas", 2) + maxReplicas = lookup(lookup(lookup(var.instance.spec, "control_plane", {}), "scaling", {}), "max_replicas", 3) + targetCPUUtilizationPercentage = lookup(lookup(lookup(var.instance.spec, "control_plane", {}), "scaling", {}), "target_cpu_utilization_percentage", 70) + targetMemoryUtilizationPercentage = lookup(lookup(lookup(var.instance.spec, "control_plane", {}), "scaling", {}), "target_memory_utilization_percentage", 80) + } + + tolerations = local.ingress_tolerations + nodeSelector = local.nodepool_labels + + # Labels for control plane service + service = { + labels = local.common_labels + } + } + + # NGINX data plane configuration (NginxProxy) + # Note: The following fields are NOT supported in NginxProxy CRD (NGF 2.3.0): + # - clientMaxBodySize (use ClientSettingsPolicy body.maxSize instead) + # - proxyConnectTimeout, proxySendTimeout, proxyReadTimeout (not exposed in any CRD) + nginx = { + config = { + # Enable Proxy Protocol to get real client IP with externalTrafficPolicy: Cluster + rewriteClientIP = local.cloud_provider == "AWS" ? { + mode = "ProxyProtocol" + trustedAddresses = [ + { + type = "CIDR" + value = "0.0.0.0/0" + } + ] + } : null + } + + # Data plane autoscaling - always enabled + autoscaling = { + enable = true + minReplicas = lookup(lookup(lookup(var.instance.spec, "data_plane", {}), "scaling", {}), "min_replicas", 2) + maxReplicas = lookup(lookup(lookup(var.instance.spec, "data_plane", {}), "scaling", {}), "max_replicas", 5) + targetCPUUtilizationPercentage = lookup(lookup(lookup(var.instance.spec, "data_plane", {}), "scaling", {}), "target_cpu_utilization_percentage", 70) + targetMemoryUtilizationPercentage = lookup(lookup(lookup(var.instance.spec, "data_plane", {}), "scaling", {}), "target_memory_utilization_percentage", 80) + } + + # Data plane container resources + container = { + resources = { + requests = { + cpu = lookup(lookup(lookup(lookup(var.instance.spec, "data_plane", {}), "resources", {}), "requests", {}), "cpu", "250m") + memory = lookup(lookup(lookup(lookup(var.instance.spec, "data_plane", {}), "resources", {}), "requests", {}), "memory", "256Mi") + } + limits = { + cpu = lookup(lookup(lookup(lookup(var.instance.spec, "data_plane", {}), "resources", {}), "limits", {}), "cpu", "1") + memory = lookup(lookup(lookup(lookup(var.instance.spec, "data_plane", {}), "resources", {}), "limits", {}), "memory", "512Mi") + } + } + } + + # Data plane pod configuration + pod = { + tolerations = local.ingress_tolerations + nodeSelector = local.nodepool_labels + } + + # Labels for data plane deployment via patches + patches = [ + { + type = "StrategicMerge" + value = { + metadata = { + labels = local.common_labels + } + } + } + ] + + service = { + type = "LoadBalancer" + externalTrafficPolicy = "Cluster" + # Service patches for annotations and labels + patches = [ + { + type = "StrategicMerge" + value = { + metadata = { + labels = local.common_labels + annotations = local.service_annotations + } + } + } + ] + } + } + + # Gateway configuration + gateways = [{ + name = local.name + namespace = var.environment.namespace + labels = merge(local.common_labels, { + "gateway.networking.k8s.io/gateway-name" = local.name + }) + # Only add cert-manager annotations when using gateway-shim + # When not using gateway-shim (custom certs present), we create explicit Certificate resources + annotations = local.use_gateway_shim ? { + "cert-manager.io/cluster-issuer" = local.effective_cluster_issuer + "cert-manager.io/renew-before" = lookup(var.instance.spec, "renew_cert_before", "720h") + } : {} + spec = { + gatewayClassName = local.gateway_class_name + listeners = concat( + # HTTP Listener + [{ + name = "http" + protocol = "HTTP" + port = 80 + allowedRoutes = { + namespaces = { + from = "All" + } + } + }], + # HTTPS Listeners per domain + [for domain_key, domain in local.domains : { + name = "https-${domain_key}" + protocol = "HTTPS" + port = 443 + hostname = domain.domain + tls = { + mode = "Terminate" + certificateRefs = [{ + kind = "Secret" + # If certificate_reference is provided, use it (custom cert) + # Otherwise: DNS-01 uses shared dns_validation_secret_name, HTTP-01 uses per-domain bootstrap cert + name = lookup(domain, "certificate_reference", "") != "" ? domain.certificate_reference : ( + local.disable_endpoint_validation ? local.dns_validation_secret_name : "${local.name}-${domain_key}-tls-cert" + ) + }] + } + allowedRoutes = { + namespaces = { + from = "All" + } + } + } if can(domain.domain)], + # HTTPS Listeners for additional hostnames from rules (domain_prefix + domain) + [for hostname_key, config in local.additional_hostname_configs : { + name = "https-${hostname_key}" + protocol = "HTTPS" + port = 443 + hostname = config.hostname + tls = { + mode = "Terminate" + certificateRefs = [{ + kind = "Secret" + name = local.disable_endpoint_validation ? local.dns_validation_secret_name : config.secret_name + }] + } + allowedRoutes = { + namespaces = { + from = "All" + } + } + }] + ) + } + }] + }), + yamlencode(local.base_helm_values) + ] + + depends_on = [ + kubernetes_secret.bootstrap_tls, + kubernetes_secret.bootstrap_tls_additional + ] +} + +# Gateway API HTTP-01 ClusterIssuer - bundled here as it requires parentRefs to the Gateway +# See: https://github.com/cert-manager/cert-manager/issues/7890 +module "cluster-issuer-gateway-http01" { + count = local.disable_endpoint_validation ? 0 : 1 + depends_on = [helm_release.nginx_gateway_fabric] + source = "github.com/Facets-cloud/facets-utility-modules//any-k8s-resource" + name = local.cluster_issuer_gateway_http + namespace = var.environment.namespace + advanced_config = {} + + data = { + apiVersion = "cert-manager.io/v1" + kind = "ClusterIssuer" + metadata = { + name = local.cluster_issuer_gateway_http + } + spec = { + acme = { + email = local.acme_email + server = "https://acme-v02.api.letsencrypt.org/directory" + privateKeySecretRef = { + name = "${local.cluster_issuer_gateway_http}-account-key" + } + solvers = [ + { + http01 = { + gatewayHTTPRoute = { + parentRefs = [ + { + name = local.name + namespace = var.environment.namespace + kind = "Gateway" + sectionName = "http" # Must target HTTP listener for HTTP-01 challenges + } + ] + } + } + }, + ] + } + } + } +} + +# Deploy all Gateway API resources using facets-utility-modules +module "gateway_api_resources" { + source = "github.com/Facets-cloud/facets-utility-modules//any-k8s-resources" + + name = "${local.name}-gateway-api" + release_name = "${local.name}-gateway-api" + namespace = var.environment.namespace + resources_data = local.gateway_api_resources + advanced_config = {} + + depends_on = [helm_release.nginx_gateway_fabric] +} + +# Basic Authentication +# NOTE: Basic auth is not natively supported in NGINX Gateway Fabric. +# Unlike ingress-nginx, NGF doesn't have auth annotations. +# Implementation would require SnippetsFilter + volume mounts which is complex and fragile. +# TODO: Implement when NGF adds native policy support or use app-level auth. +# +# resource "random_string" "basic_auth_password" { +# count = lookup(var.instance.spec, "basic_auth", false) ? 1 : 0 +# length = 16 +# special = true +# } +# +# resource "kubernetes_secret" "basic_auth" { +# count = lookup(var.instance.spec, "basic_auth", false) ? 1 : 0 +# +# metadata { +# name = "${local.name}-basic-auth" +# namespace = var.environment.namespace +# } +# +# data = { +# username = "${var.instance_name}-user" +# password = random_string.basic_auth_password[0].result +# } +# +# type = "Opaque" +# } + +# Load Balancer Service Discovery +# Note: The LoadBalancer service is created by NGINX Gateway Fabric controller +# when it processes the Gateway resource from the Helm chart +data "kubernetes_service" "gateway_lb" { + depends_on = [ + helm_release.nginx_gateway_fabric + ] + metadata { + # Service is created by controller with pattern: - + # Since both release name and gateway name are local.name, it becomes: - + name = "${local.name}-${local.name}" + namespace = var.environment.namespace + } +} + +# Route53 DNS Records (AWS) +resource "aws_route53_record" "cluster-base-domain" { + count = local.tenant_provider == "aws" && !lookup(var.instance.spec, "disable_base_domain", false) ? 1 : 0 + depends_on = [ + helm_release.nginx_gateway_fabric, + data.kubernetes_service.gateway_lb + ] + zone_id = var.cc_metadata.tenant_base_domain_id + name = local.base_domain + type = local.record_type + ttl = "300" + records = [local.lb_record_value] + provider = "aws3tooling" + lifecycle { + prevent_destroy = true + } +} + +resource "aws_route53_record" "cluster-base-domain-wildcard" { + count = local.tenant_provider == "aws" && !lookup(var.instance.spec, "disable_base_domain", false) ? 1 : 0 + depends_on = [ + helm_release.nginx_gateway_fabric, + data.kubernetes_service.gateway_lb + ] + zone_id = var.cc_metadata.tenant_base_domain_id + name = local.base_subdomain + type = local.record_type + ttl = "300" + records = [local.lb_record_value] + provider = "aws3tooling" + lifecycle { + prevent_destroy = true + } +} diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/outputs.tf b/modules/ingress/nginx_gateway_fabric_legacy/1.0/outputs.tf new file mode 100644 index 00000000..8f013ada --- /dev/null +++ b/modules/ingress/nginx_gateway_fabric_legacy/1.0/outputs.tf @@ -0,0 +1,102 @@ +locals { + # Basic auth is not supported in NGINX Gateway Fabric (see main.tf) + # username = lookup(var.instance.spec, "basic_auth", false) && length(random_string.basic_auth_password) > 0 ? "${var.instance_name}-user" : "" + # password = lookup(var.instance.spec, "basic_auth", false) && length(random_string.basic_auth_password) > 0 ? random_string.basic_auth_password[0].result : "" + # is_auth_enabled = length(local.username) > 0 && length(local.password) > 0 ? true : false + + output_attributes = merge( + { + # Always include base_domain for backward compatibility + base_domain = local.base_domain + gateway_class = local.gateway_class_name + gateway_name = local.name + # Load balancer DNS information + loadbalancer_dns = try(data.kubernetes_service.gateway_lb.status.0.load_balancer.0.ingress.0.hostname, + data.kubernetes_service.gateway_lb.status.0.load_balancer.0.ingress.0.ip, + null) + loadbalancer_hostname = try(data.kubernetes_service.gateway_lb.status.0.load_balancer.0.ingress.0.hostname, null) + loadbalancer_ip = try(data.kubernetes_service.gateway_lb.status.0.load_balancer.0.ingress.0.ip, null) + }, + # Only include base_domain_enabled if base domain is not disabled + !lookup(var.instance.spec, "disable_base_domain", false) ? { + base_domain_enabled = true + } : { + base_domain_enabled = false + } + ) + + output_interfaces = { + for route_key, route in local.rulesFiltered : route_key => { + connection_string = "https://${route.host}" + host = route.host + port = 443 + # Basic auth not supported - username/password removed + # username = local.username + # password = local.password + secrets = [] + } + } +} + +output "domains" { + value = concat( + # Only include base domain if not disabled + !lookup(var.instance.spec, "disable_base_domain", false) ? [local.base_domain] : [], + [for d in values(lookup(var.instance.spec, "domains", {})) : d.domain if can(d.domain)] + ) +} + +output "nginx_gateway_fabric" { + value = { + resource_type = "ingress" + resource_name = var.instance_name + } +} + +output "domain" { + value = !lookup(var.instance.spec, "disable_base_domain", false) ? local.base_domain : null +} + +output "secure_endpoint" { + value = !lookup(var.instance.spec, "disable_base_domain", false) ? "https://${local.base_domain}" : null +} + +output "gateway_class" { + value = local.gateway_class_name + description = "The GatewayClass name used by this gateway" +} + +output "gateway_name" { + value = local.name + description = "The Gateway resource name" +} + +output "subdomain" { + value = !lookup(var.instance.spec, "disable_base_domain", false) ? { + (var.instance_name) = merge( + { + for s in try(var.instance.spec.subdomains, []) : + "${s}.domain" => "${s}.${local.base_domain}" + }, + { + for s in try(var.instance.spec.subdomains, []) : + "${s}.secure_endpoint" => "https://${s}.${local.base_domain}" + } + ) + } : {} +} + +output "tls_secret" { + value = local.dns_validation_secret_name + description = "TLS certificate secret name" +} + +output "load_balancer_hostname" { + value = local.lb_hostname + description = "Load balancer hostname (for CNAME records)" +} + +output "load_balancer_ip" { + value = local.lb_ip + description = "Load balancer IP address (for A records)" +} diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/variables.tf b/modules/ingress/nginx_gateway_fabric_legacy/1.0/variables.tf new file mode 100644 index 00000000..0d889c8c --- /dev/null +++ b/modules/ingress/nginx_gateway_fabric_legacy/1.0/variables.tf @@ -0,0 +1,37 @@ +variable "cluster" { + type = any + default = {} +} + +variable "baseinfra" { + type = any + default = {} +} + +variable "cc_metadata" { + type = any + default = { + tenant_base_domain = "tenant.facets.cloud" + } +} + +variable "instance" { + type = any +} + +variable "instance_name" { + type = string + default = "test_instance" +} + +variable "environment" { + type = any + default = { + namespace = "default" + } +} + +variable "inputs" { + type = any + default = {} +} From e7ceb7abf1144c1cf804b2fc89bbaf9545f247bf Mon Sep 17 00:00:00 2001 From: Sanmesh Kakade Date: Tue, 3 Feb 2026 12:49:09 +0530 Subject: [PATCH 03/13] fix regular expression path type and change path type to path prefix as default --- .../nginx_gateway_fabric_legacy/1.0/README.md | 30 +++++++++---------- .../1.0/facets.yaml | 4 +-- .../nginx_gateway_fabric_legacy/1.0/main.tf | 6 ++-- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/README.md b/modules/ingress/nginx_gateway_fabric_legacy/1.0/README.md index 4eee9bbd..063a4454 100644 --- a/modules/ingress/nginx_gateway_fabric_legacy/1.0/README.md +++ b/modules/ingress/nginx_gateway_fabric_legacy/1.0/README.md @@ -39,7 +39,7 @@ This module deploys **NGINX Gateway Fabric**, NGINX's implementation of the Kube "namespace": "default", "port": "8080", "path": "/api", - "path_type": "RegularExpression" + "path_type": "PathPrefix" } } } @@ -59,12 +59,10 @@ This module deploys **NGINX Gateway Fabric**, NGINX's implementation of the Kube | Type | Default | Description | |------|---------|-------------| -| `RegularExpression` | Yes | Auto-appends `.*` to path (e.g., `/api` becomes `/api.*`). Ensures longer paths match before shorter ones in NGINX. | -| `PathPrefix` | No | Matches paths starting with the specified prefix | +| `PathPrefix` | Yes | Matches paths starting with the specified prefix | +| `RegularExpression` | No | Matches paths using regular expressions (e.g., `^/api/v[0-9]+/.*`) | | `Exact` | No | Matches the exact path only | -> **Note**: `RegularExpression` is the default because it ensures proper route ordering in NGINX. More specific paths (e.g., `/perform_login.*`) will match before catch-all patterns (e.g., `/.*`). - --- ## Routing Options @@ -81,7 +79,7 @@ Route traffic based on HTTP headers: "namespace": "default", "port": "8080", "path": "/", - "path_type": "RegularExpression", + "path_type": "PathPrefix", "header_matches": { "version_header": { "name": "X-API-Version", @@ -111,7 +109,7 @@ Route traffic based on query parameters: "namespace": "default", "port": "8080", "path": "/api", - "path_type": "RegularExpression", + "path_type": "PathPrefix", "query_param_matches": { "version_param": { "name": "version", @@ -136,7 +134,7 @@ Route traffic based on HTTP method: "namespace": "default", "port": "8080", "path": "/api", - "path_type": "RegularExpression", + "path_type": "PathPrefix", "method": "GET" } } @@ -159,7 +157,7 @@ Rewrite request URLs before forwarding to backend: "namespace": "default", "port": "8080", "path": "/old-api", - "path_type": "RegularExpression", + "path_type": "PathPrefix", "url_rewrite": { "rewrite_rule": { "hostname": "internal-api.svc.cluster.local", @@ -201,7 +199,7 @@ Modify headers sent to backend: "namespace": "default", "port": "8080", "path": "/", - "path_type": "RegularExpression", + "path_type": "PathPrefix", "request_header_modifier": { "add": { "custom_header": { @@ -270,7 +268,7 @@ Configure request and backend timeouts: "namespace": "default", "port": "8080", "path": "/api", - "path_type": "RegularExpression", + "path_type": "PathPrefix", "timeouts": { "request": "60s", "backend_request": "30s" @@ -294,7 +292,7 @@ Enable Cross-Origin Resource Sharing: "namespace": "default", "port": "8080", "path": "/", - "path_type": "RegularExpression", + "path_type": "PathPrefix", "cors": { "enabled": true, "allow_origins": { @@ -395,7 +393,7 @@ Split traffic between service versions: "namespace": "default", "port": "8080", "path": "/", - "path_type": "RegularExpression", + "path_type": "PathPrefix", "canary_deployment": { "enabled": true, "canary_service": "api-v2", @@ -422,7 +420,7 @@ Mirror traffic to a secondary service for testing: "namespace": "default", "port": "8080", "path": "/api", - "path_type": "RegularExpression", + "path_type": "PathPrefix", "request_mirror": { "service_name": "api-shadow", "port": "8080", @@ -467,7 +465,7 @@ Configure custom domains at the root level: "namespace": "default", "port": "8080", "path": "/", - "path_type": "RegularExpression" + "path_type": "PathPrefix" } } } @@ -540,7 +538,7 @@ Deploy with internal/private load balancer: "namespace": "default", "port": "8080", "path": "/", - "path_type": "RegularExpression" + "path_type": "PathPrefix" } } } diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml b/modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml index 1232a634..0074bd38 100644 --- a/modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml +++ b/modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml @@ -272,12 +272,12 @@ spec: path_type: type: string title: Path Type - description: Path matching type. RegularExpression (default) auto-appends .* to paths for proper NGINX regex ordering. Use PathPrefix or Exact for literal matching. + description: "Path matching type. PathPrefix (default) matches paths starting with the specified prefix. Use RegularExpression for regex matching (e.g., ^/api/v[0-9]+/.*). Use Exact for exact path matching." enum: - Exact - PathPrefix - RegularExpression - default: RegularExpression + default: PathPrefix x-ui-visible-if: field: spec.rules.{{this}}.grpc_config.enabled values: diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf b/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf index aec8b987..9d438daf 100644 --- a/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf +++ b/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf @@ -282,13 +282,11 @@ locals { rules = [{ matches = concat( # Path matching (with optional method and query params) - # Default: RegularExpression with .* suffix (e.g., /path becomes /path.*) - # This ensures proper regex ordering in NGINX (longer patterns match first) [merge( { path = { - type = lookup(v, "path_type", "RegularExpression") - value = lookup(v, "path_type", "RegularExpression") == "RegularExpression" ? "${lookup(v, "path", "/")}.*" : lookup(v, "path", "/") + type = lookup(v, "path_type", "PathPrefix") + value = lookup(v, "path", "/") } }, # Method matching (ALL or null means match all methods) From 88a73b8d757d0323e27a65341eb3a8210e637aed Mon Sep 17 00:00:00 2001 From: Sanmesh Kakade Date: Tue, 3 Feb 2026 18:58:51 +0530 Subject: [PATCH 04/13] remove regular expression from facets.yaml --- modules/ingress/nginx_gateway_fabric_legacy/1.0/README.md | 1 - modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml | 3 +-- modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf | 3 ++- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/README.md b/modules/ingress/nginx_gateway_fabric_legacy/1.0/README.md index 063a4454..d6093f76 100644 --- a/modules/ingress/nginx_gateway_fabric_legacy/1.0/README.md +++ b/modules/ingress/nginx_gateway_fabric_legacy/1.0/README.md @@ -60,7 +60,6 @@ This module deploys **NGINX Gateway Fabric**, NGINX's implementation of the Kube | Type | Default | Description | |------|---------|-------------| | `PathPrefix` | Yes | Matches paths starting with the specified prefix | -| `RegularExpression` | No | Matches paths using regular expressions (e.g., `^/api/v[0-9]+/.*`) | | `Exact` | No | Matches the exact path only | --- diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml b/modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml index 0074bd38..c29e3754 100644 --- a/modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml +++ b/modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml @@ -272,11 +272,10 @@ spec: path_type: type: string title: Path Type - description: "Path matching type. PathPrefix (default) matches paths starting with the specified prefix. Use RegularExpression for regex matching (e.g., ^/api/v[0-9]+/.*). Use Exact for exact path matching." + description: "Path matching type. PathPrefix (default) matches paths starting with the specified prefix. Use Exact for exact path matching." enum: - Exact - PathPrefix - - RegularExpression default: PathPrefix x-ui-visible-if: field: spec.rules.{{this}}.grpc_config.enabled diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf b/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf index 9d438daf..fc8ca329 100644 --- a/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf +++ b/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf @@ -48,7 +48,7 @@ locals { ( # gRPC routes don't need path/path_type - they use method matching lookup(lookup(v, "grpc_config", {}), "enabled", false) || - # HTTP routes require path (path_type defaults to RegularExpression, with .* suffix auto-added) + # HTTP routes require path (path_type defaults to PathPrefix) (lookup(v, "path", null) != null && lookup(v, "path", "") != "") ) && (lookup(v, "disable", false) == false) @@ -922,6 +922,7 @@ resource "helm_release" "nginx_gateway_fabric" { service = { type = "LoadBalancer" + # loadBalancerClass = local.cloud_provider == "AWS" ? "service.k8s.aws/nlb" : "" externalTrafficPolicy = "Cluster" # Service patches for annotations and labels patches = [ From dec6c5fd8077e7d8f2dc4de6d82b898d34d45260 Mon Sep 17 00:00:00 2001 From: Sanmesh Kakade Date: Wed, 4 Feb 2026 10:35:06 +0530 Subject: [PATCH 05/13] use legacy format for gateway api crd --- outputs/gateway_api_crd/output.facets.yaml | 23 ++++++++++++++++++++++ outputs/gateway_api_crd/outputs.yaml | 19 ------------------ 2 files changed, 23 insertions(+), 19 deletions(-) create mode 100644 outputs/gateway_api_crd/output.facets.yaml delete mode 100644 outputs/gateway_api_crd/outputs.yaml diff --git a/outputs/gateway_api_crd/output.facets.yaml b/outputs/gateway_api_crd/output.facets.yaml new file mode 100644 index 00000000..2e856b8f --- /dev/null +++ b/outputs/gateway_api_crd/output.facets.yaml @@ -0,0 +1,23 @@ +name: gateway_api_crd +out: + type: object + title: Gateway API CRD + description: Gateway API Custom Resource Definitions + properties: + attributes: + type: object + properties: + version: + type: string + channel: + type: string + install_url: + type: string + job_name: + type: string + namespace: + type: string + interfaces: + type: object + properties: {} +providers: [] diff --git a/outputs/gateway_api_crd/outputs.yaml b/outputs/gateway_api_crd/outputs.yaml deleted file mode 100644 index 43e39d23..00000000 --- a/outputs/gateway_api_crd/outputs.yaml +++ /dev/null @@ -1,19 +0,0 @@ -name: "@outputs/gateway_api_crd" -properties: - attributes: - type: object - properties: - version: - type: string - channel: - type: string - install_url: - type: string - job_name: - type: string - namespace: - type: string - interfaces: - type: object - properties: {} -providers: [] From 6ca6bb304ad98cddb6110c49c35362c0e488bcd7 Mon Sep 17 00:00:00 2001 From: Sanmesh Kakade Date: Wed, 4 Feb 2026 11:45:46 +0530 Subject: [PATCH 06/13] move domains inside spec --- .../1.0/facets.yaml | 32 +++++++++++++++++++ .../nginx_gateway_fabric_legacy/1.0/main.tf | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml b/modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml index c29e3754..b9c159d3 100644 --- a/modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml +++ b/modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml @@ -40,6 +40,37 @@ spec: title: Disable Base Domain description: Disable automatic creation of base domain for this gateway default: false + domains: + title: Domains + description: Map of domain key to rules + type: object + x-ui-overrides-only: true + patternProperties: + ^[a-zA-Z0-9_.-]*$: + title: Domain Object + description: Name of the domain object + type: object + properties: + domain: + type: string + title: Domain + description: Host name of the domain + pattern: ^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*\.[A-Za-z]{2,6}$ + x-ui-unique: true + x-ui-placeholder: 'Domain to map ingress. Eg: example.com, sub.example.com, my-domain.co.uk' + x-ui-error-message: 'Value doesn''t match the format. Eg: example.com, my-domain.co.uk' + alias: + type: string + title: Alias + description: Alias for the domain + certificate_reference: + type: string + title: Certificate Reference + description: Name of an existing TLS secret to use instead of cert-manager managed certificates + x-ui-toggle: true + required: + - domain + - alias data_plane: type: object title: Data Plane (NGINX) @@ -802,6 +833,7 @@ spec: - private - domain_prefix_override - disable_base_domain + - domains - force_ssl_redirection - body_size - disable_endpoint_validation diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf b/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf index fc8ca329..07a750e5 100644 --- a/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf +++ b/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf @@ -30,7 +30,7 @@ locals { } } - domains = merge(lookup(var.instance, "domains", {}), local.add_base_domain) + domains = merge(lookup(var.instance.spec, "domains", {}), local.add_base_domain) # List of all domain hostnames for HTTPRoutes all_domain_hostnames = [for domain_key, domain in local.domains : domain.domain] From ff8e84a1fe77e30030a50b621cef0e0fff924e11 Mon Sep 17 00:00:00 2001 From: Sanmesh Kakade Date: Fri, 6 Feb 2026 11:13:18 +0530 Subject: [PATCH 07/13] fix(nginx_gateway_fabric_legacy): remove hardcoded labels to avoid selector mismatch - Remove app.kubernetes.io/name and app.kubernetes.io/instance from common_labels - Add helm_release_name local for dynamic label matching - Update ServiceMonitor selector to use helm_release_name --- .../ingress/nginx_gateway_fabric_legacy/1.0/main.tf | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf b/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf index 07a750e5..8d7f59fc 100644 --- a/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf +++ b/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf @@ -93,10 +93,7 @@ locals { disable_endpoint_validation = lookup(var.instance.spec, "disable_endpoint_validation", false) || lookup(var.instance.spec, "private", false) # Common labels for all resources - # Note: app.kubernetes.io/instance must match the Helm release name for selector compatibility common_labels = { - "app.kubernetes.io/name" = "nginx-gateway-fabric" - "app.kubernetes.io/instance" = "${local.name}-nginx-fabric" "app.kubernetes.io/managed-by" = "facets" "facets.cloud/module" = "nginx_gateway_fabric" "facets.cloud/instance" = var.instance_name @@ -481,6 +478,9 @@ locals { } if lookup(lookup(v, "grpc_config", {}), "enabled", false) } + # Helm release name - keep under 63 chars for k8s label limit + helm_release_name = substr(local.name, 0, min(length(local.name), 63)) + # ServiceMonitor (always enabled with defaults) servicemonitor_resources = { "servicemonitor-${local.name}" = { @@ -496,8 +496,8 @@ locals { spec = { selector = { matchLabels = { - "app.kubernetes.io/name" = "nginx-gateway-fabric" - "app.kubernetes.io/instance" = "${local.name}-nginx-fabric" + "app.kubernetes.io/name" = local.helm_release_name + "app.kubernetes.io/instance" = local.helm_release_name } } endpoints = [{ @@ -921,7 +921,7 @@ resource "helm_release" "nginx_gateway_fabric" { ] service = { - type = "LoadBalancer" + type = "LoadBalancer" # loadBalancerClass = local.cloud_provider == "AWS" ? "service.k8s.aws/nlb" : "" externalTrafficPolicy = "Cluster" # Service patches for annotations and labels From 9014e7895430547f0fa1f538e53c5f024a6988f7 Mon Sep 17 00:00:00 2001 From: Sanmesh Kakade Date: Fri, 6 Feb 2026 16:02:09 +0530 Subject: [PATCH 08/13] enable access logs by default --- .../nginx_gateway_fabric_legacy/1.0/main.tf | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf b/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf index 8d7f59fc..048ac435 100644 --- a/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf +++ b/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf @@ -866,14 +866,27 @@ resource "helm_release" "nginx_gateway_fabric" { # - clientMaxBodySize (use ClientSettingsPolicy body.maxSize instead) # - proxyConnectTimeout, proxySendTimeout, proxyReadTimeout (not exposed in any CRD) nginx = { - config = { - # Enable Proxy Protocol to get real client IP with externalTrafficPolicy: Cluster - rewriteClientIP = local.cloud_provider == "AWS" ? { - mode = "ProxyProtocol" - trustedAddresses = [ - { - type = "CIDR" - value = "0.0.0.0/0" + # Enable Proxy Protocol to get real client IP with externalTrafficPolicy: Cluster (AWS only) + # Access logs are always enabled with upstream service name for debugging + config = merge( + local.cloud_provider == "AWS" ? { + rewriteClientIP = { + mode = "ProxyProtocol" + trustedAddresses = [ + { + type = "CIDR" + value = "0.0.0.0/0" + } + ] + } + } : {}, + { + logging = { + errorLevel = "info" + agentLevel = "info" + accessLog = { + disable = false + format = "$remote_addr - $remote_user [$time_local] \"$request\" $status $body_bytes_sent \"$http_referer\" \"$http_user_agent\" upstream=$upstream_addr upstream_name=$proxy_host" } ] } : null From f1d10ffdc3bcbf0e5ef5524285cd6d95aed8c5c9 Mon Sep 17 00:00:00 2001 From: Sanmesh Kakade Date: Fri, 6 Feb 2026 16:33:29 +0530 Subject: [PATCH 09/13] add missing fields --- intents/gateway_api_crd/facets.yaml | 2 +- modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/intents/gateway_api_crd/facets.yaml b/intents/gateway_api_crd/facets.yaml index d7e23c99..8f5c476b 100644 --- a/intents/gateway_api_crd/facets.yaml +++ b/intents/gateway_api_crd/facets.yaml @@ -5,4 +5,4 @@ description: Kubernetes Gateway API Custom Resource Definitions for advanced net iconUrl: https://uploads-ssl.webflow.com/6252ef50a9f5d4afb6983bc3/669fa0ccb5a2964fe7f4d754_k8s_resource.svg outputs: - name: default - type: "@outputs/gateway_api_crd" \ No newline at end of file + type: "@outputs/gateway_api_crd" diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf b/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf index 048ac435..9c0882fa 100644 --- a/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf +++ b/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf @@ -886,11 +886,11 @@ resource "helm_release" "nginx_gateway_fabric" { agentLevel = "info" accessLog = { disable = false - format = "$remote_addr - $remote_user [$time_local] \"$request\" $status $body_bytes_sent \"$http_referer\" \"$http_user_agent\" upstream=$upstream_addr upstream_name=$proxy_host" + format = "$remote_addr - $remote_user [$time_local] \"$request\" $status $body_bytes_sent \"$http_referer\" \"$http_user_agent\" $request_length $request_time [$proxy_host] $upstream_addr $upstream_response_length $upstream_response_time $upstream_status" } - ] - } : null - } + } + } + ) # Data plane autoscaling - always enabled autoscaling = { From 6abdc3014ce46dc5a9de6a9c217beddda3b9aa9b Mon Sep 17 00:00:00 2001 From: Sanmesh Kakade Date: Fri, 6 Feb 2026 17:04:11 +0530 Subject: [PATCH 10/13] docs(nginx_gateway_fabric_legacy): fix domains config examples and output type (#509) - Move domains configuration inside spec in documentation examples - Fix output description: domains returns a list, not a map Co-authored-by: Claude Opus 4.5 --- .../nginx_gateway_fabric_legacy/1.0/README.md | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/README.md b/modules/ingress/nginx_gateway_fabric_legacy/1.0/README.md index d6093f76..5523f5a4 100644 --- a/modules/ingress/nginx_gateway_fabric_legacy/1.0/README.md +++ b/modules/ingress/nginx_gateway_fabric_legacy/1.0/README.md @@ -436,28 +436,28 @@ Mirror traffic to a secondary service for testing: ### Custom Domains -Configure custom domains at the root level: +Configure custom domains inside spec: ```json { "kind": "ingress", "flavor": "nginx_gateway_fabric_legacy", "version": "1.0", - "domains": { - "production": { - "domain": "api.example.com", - "alias": "prod" - }, - "staging": { - "domain": "staging-api.example.com", - "alias": "staging", - "certificate_reference": "staging-tls" - } - }, "spec": { "private": false, "disable_base_domain": true, "force_ssl_redirection": true, + "domains": { + "production": { + "domain": "api.example.com", + "alias": "prod" + }, + "staging": { + "domain": "staging-api.example.com", + "alias": "staging", + "certificate_reference": "staging-tls" + } + }, "rules": { "api": { "service_name": "api-service", @@ -509,11 +509,13 @@ Use existing TLS certificates: ```json { - "domains": { - "custom": { - "domain": "api.example.com", - "alias": "api", - "certificate_reference": "my-existing-tls-secret" + "spec": { + "domains": { + "custom": { + "domain": "api.example.com", + "alias": "api", + "certificate_reference": "my-existing-tls-secret" + } } } } @@ -603,7 +605,7 @@ See available values: https://github.com/nginxinc/nginx-gateway-fabric/blob/main | Output | Description | |--------|-------------| -| `domains` | Map of all configured domains | +| `domains` | List of all configured domains | | `domain` | Base domain (if not disabled) | | `secure_endpoint` | HTTPS endpoint for base domain | | `gateway_class` | GatewayClass name | From ee71a78fcf7e88efa72fdd4cc50b968a6bf3107d Mon Sep 17 00:00:00 2001 From: Sanmesh Kakade Date: Fri, 6 Feb 2026 17:13:03 +0530 Subject: [PATCH 11/13] fix: address CodeRabbit review comments - Pin kubectl image to 1.31.4 instead of latest - Fix ServiceMonitor selector to use literal "nginx-gateway-fabric" for app.kubernetes.io/name - Add GRPCRoute to ReferenceGrant for cross-namespace support - Fix backend_request placeholder to match default (300s) - YAML formatting fixes in facets.yaml Co-Authored-By: Claude Opus 4.5 --- modules/gateway_api_crd/k8s/1.0/main.tf | 2 +- .../1.0/facets.yaml | 220 +++++++++--------- .../nginx_gateway_fabric_legacy/1.0/main.tf | 19 +- 3 files changed, 124 insertions(+), 117 deletions(-) diff --git a/modules/gateway_api_crd/k8s/1.0/main.tf b/modules/gateway_api_crd/k8s/1.0/main.tf index e4fbc8bf..43925aee 100644 --- a/modules/gateway_api_crd/k8s/1.0/main.tf +++ b/modules/gateway_api_crd/k8s/1.0/main.tf @@ -93,7 +93,7 @@ resource "kubernetes_job_v1" "gateway_api_crd_installer" { container { name = "kubectl" - image = "bitnami/kubectl:latest" + image = "bitnami/kubectl:1.31.4" command = ["/bin/sh", "-c"] args = [ # Using --server-side to avoid annotation size limit (262KB) diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml b/modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml index b9c159d3..b843dd42 100644 --- a/modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml +++ b/modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml @@ -1,12 +1,12 @@ intent: ingress flavor: nginx_gateway_fabric_legacy -version: '1.0' +version: "1.0" description: NGINX Gateway Fabric - Kubernetes Gateway API implementation (Legacy module format) clouds: -- aws -- azure -- gcp -- kubernetes + - aws + - azure + - gcp + - kubernetes inputs: kubernetes_details: type: "@outputs/kubernetes" @@ -57,8 +57,8 @@ spec: description: Host name of the domain pattern: ^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*\.[A-Za-z]{2,6}$ x-ui-unique: true - x-ui-placeholder: 'Domain to map ingress. Eg: example.com, sub.example.com, my-domain.co.uk' - x-ui-error-message: 'Value doesn''t match the format. Eg: example.com, my-domain.co.uk' + x-ui-placeholder: "Domain to map ingress. Eg: example.com, sub.example.com, my-domain.co.uk" + x-ui-error-message: "Value doesn't match the format. Eg: example.com, my-domain.co.uk" alias: type: string title: Alias @@ -69,8 +69,8 @@ spec: description: Name of an existing TLS secret to use instead of cert-manager managed certificates x-ui-toggle: true required: - - domain - - alias + - domain + - alias data_plane: type: object title: Data Plane (NGINX) @@ -253,8 +253,8 @@ spec: valueKey: resourceName valueTemplate: ${service.{{value}}.out.attributes.service_name} filterConditions: - - field: resourceType - value: service + - field: resourceType + value: service x-ui-typeable: true namespace: type: string @@ -269,8 +269,8 @@ spec: valueKey: resourceName valueTemplate: ${service.{{value}}.out.attributes.namespace} filterConditions: - - field: resourceType - value: service + - field: resourceType + value: service x-ui-typeable: true port: type: string @@ -294,25 +294,25 @@ spec: description: Path of the application (required for HTTP routes) pattern: ^(/[^/]+)*(/)?$ x-ui-placeholder: Enter path (e.g., / or /api) - x-ui-error-message: 'Value doesn''t match pattern, eg: / or /api' + x-ui-error-message: "Value doesn't match pattern, eg: / or /api" x-ui-visible-if: field: spec.rules.{{this}}.grpc_config.enabled values: - - false - - null + - false + - null path_type: type: string title: Path Type description: "Path matching type. PathPrefix (default) matches paths starting with the specified prefix. Use Exact for exact path matching." enum: - - Exact - - PathPrefix + - Exact + - PathPrefix default: PathPrefix x-ui-visible-if: field: spec.rules.{{this}}.grpc_config.enabled values: - - false - - null + - false + - null header_matches: type: object title: Header-Based Routing @@ -338,12 +338,12 @@ spec: title: Match Type description: How to match the header value (Exact or RegularExpression) enum: - - Exact - - RegularExpression + - Exact + - RegularExpression default: Exact required: - - name - - value + - name + - value query_param_matches: type: object title: Query Parameter Matching @@ -369,25 +369,25 @@ spec: title: Match Type description: How to match the parameter value (Exact or RegularExpression) enum: - - Exact - - RegularExpression + - Exact + - RegularExpression default: Exact required: - - name - - value + - name + - value method: type: string title: HTTP Method description: Match requests by HTTP method enum: - - ALL - - GET - - POST - - PUT - - DELETE - - PATCH - - HEAD - - OPTIONS + - ALL + - GET + - POST + - PUT + - DELETE + - PATCH + - HEAD + - OPTIONS default: ALL x-ui-toggle: true request_header_modifier: @@ -416,8 +416,8 @@ spec: description: Value for the header x-ui-placeholder: custom-value required: - - name - - value + - name + - value set: type: object title: Set Headers @@ -438,8 +438,8 @@ spec: description: Value for the header x-ui-placeholder: gateway required: - - name - - value + - name + - value remove: type: object title: Remove Headers @@ -455,7 +455,7 @@ spec: description: Name of the header to remove x-ui-placeholder: X-Sensitive-Header required: - - name + - name response_header_modifier: type: object title: Response Header Modification @@ -482,8 +482,8 @@ spec: description: Value for the header x-ui-placeholder: unique-id required: - - name - - value + - name + - value set: type: object title: Set Headers @@ -504,8 +504,8 @@ spec: description: Value for the header x-ui-placeholder: no-store required: - - name - - value + - name + - value remove: type: object title: Remove Headers @@ -521,7 +521,7 @@ spec: description: Name of the header to remove x-ui-placeholder: Server required: - - name + - name url_rewrite: type: object title: URL Rewriting @@ -542,8 +542,8 @@ spec: title: Path Rewrite Type description: How to rewrite the path (ReplaceFullPath or ReplacePrefixMatch) enum: - - ReplaceFullPath - - ReplacePrefixMatch + - ReplaceFullPath + - ReplacePrefixMatch replace_path: type: string title: Replace Path Value @@ -568,7 +568,7 @@ spec: x-ui-visible-if: field: spec.rules.{{this}}.canary_deployment.enabled values: - - true + - true canary_weight: type: integer title: Canary Traffic Percentage @@ -579,7 +579,7 @@ spec: x-ui-visible-if: field: spec.rules.{{this}}.canary_deployment.enabled values: - - true + - true request_mirror: type: object title: Request Mirroring @@ -611,14 +611,14 @@ spec: description: Total request timeout (e.g., 30s, 1m, 5m) pattern: ^\d+[smh]$ default: 300s - x-ui-placeholder: '300s' + x-ui-placeholder: "300s" backend_request: type: string title: Backend Request Timeout description: Backend response timeout (e.g., 30s, 1m, 5m) default: 300s pattern: ^\d+[smh]$ - x-ui-placeholder: '30s' + x-ui-placeholder: "300s" cors: type: object title: CORS Configuration @@ -637,7 +637,7 @@ spec: x-ui-visible-if: field: spec.rules.{{this}}.cors.enabled values: - - true + - true patternProperties: ^[a-zA-Z0-9_.-]*$: type: object @@ -649,7 +649,7 @@ spec: description: Allowed origin URL x-ui-placeholder: https://example.com required: - - origin + - origin allow_methods: type: object title: Allowed Methods @@ -657,7 +657,7 @@ spec: x-ui-visible-if: field: spec.rules.{{this}}.cors.enabled values: - - true + - true patternProperties: ^[a-zA-Z0-9_.-]*$: type: object @@ -668,15 +668,15 @@ spec: title: HTTP Method description: HTTP method to allow enum: - - GET - - POST - - PUT - - DELETE - - PATCH - - OPTIONS - - HEAD + - GET + - POST + - PUT + - DELETE + - PATCH + - OPTIONS + - HEAD required: - - method + - method allow_headers: type: object title: Allowed Headers @@ -684,7 +684,7 @@ spec: x-ui-visible-if: field: spec.rules.{{this}}.cors.enabled values: - - true + - true patternProperties: ^[a-zA-Z0-9_.-]*$: type: object @@ -696,7 +696,7 @@ spec: description: Header name to allow x-ui-placeholder: Content-Type required: - - header + - header allow_credentials: type: boolean title: Allow Credentials @@ -705,7 +705,7 @@ spec: x-ui-visible-if: field: spec.rules.{{this}}.cors.enabled values: - - true + - true max_age: type: integer title: Preflight Cache Max Age @@ -714,7 +714,7 @@ spec: x-ui-visible-if: field: spec.rules.{{this}}.cors.enabled values: - - true + - true grpc_config: type: object title: gRPC Configuration @@ -734,7 +734,7 @@ spec: x-ui-visible-if: field: spec.rules.{{this}}.grpc_config.enabled values: - - true + - true method_match: type: object title: gRPC Method Matching @@ -742,7 +742,7 @@ spec: x-ui-visible-if: field: spec.rules.{{this}}.grpc_config.match_all_methods values: - - false + - false patternProperties: ^[a-zA-Z0-9_.-]*$: type: object @@ -761,35 +761,35 @@ spec: title: Match Type description: NGINX Gateway Fabric only supports Exact matching enum: - - Exact + - Exact default: Exact required: - - service - - method + - service + - method required: - - service_name - - port - - namespace + - service_name + - port + - namespace x-ui-order: - - disable - - domain_prefix - - service_name - - namespace - - port - - path - - path_type - - method - - timeouts - - cors - - header_matches - - query_param_matches - - url_rewrite - - request_header_modifier - - response_header_modifier - - grpc_config -# Disabling below things as too advanced for initial release - # - canary_deployment - # - request_mirror + - disable + - domain_prefix + - service_name + - namespace + - port + - path + - path_type + - method + - timeouts + - cors + - header_matches + - query_param_matches + - url_rewrite + - request_header_modifier + - response_header_modifier + - grpc_config + # Disabling below things as too advanced for initial release + # - canary_deployment + # - request_mirror force_ssl_redirection: type: boolean title: Force SSL Redirection @@ -826,26 +826,26 @@ spec: default: true x-ui-toggle: true required: - - private - - rules - - force_ssl_redirection + - private + - rules + - force_ssl_redirection x-ui-order: - - private - - domain_prefix_override - - disable_base_domain - - domains - - force_ssl_redirection - - body_size - - disable_endpoint_validation - - data_plane - - control_plane - - helm_values - - helm_wait - - rules + - private + - domain_prefix_override + - disable_base_domain + - domains + - force_ssl_redirection + - body_size + - disable_endpoint_validation + - data_plane + - control_plane + - helm_values + - helm_wait + - rules sample: kind: ingress flavor: nginx_gateway_fabric_legacy - version: '1.0' + version: "1.0" disabled: true metadata: annotations: {} diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf b/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf index 9c0882fa..fe3741e3 100644 --- a/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf +++ b/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf @@ -496,7 +496,7 @@ locals { spec = { selector = { matchLabels = { - "app.kubernetes.io/name" = local.helm_release_name + "app.kubernetes.io/name" = "nginx-gateway-fabric" "app.kubernetes.io/instance" = local.helm_release_name } } @@ -526,11 +526,18 @@ locals { namespace = ns } spec = { - from = [{ - group = "gateway.networking.k8s.io" - kind = "HTTPRoute" - namespace = var.environment.namespace - }] + from = [ + { + group = "gateway.networking.k8s.io" + kind = "HTTPRoute" + namespace = var.environment.namespace + }, + { + group = "gateway.networking.k8s.io" + kind = "GRPCRoute" + namespace = var.environment.namespace + } + ] to = [{ group = "" kind = "Service" From 0dd4ac8f4f378633bfd2809f7c157aa13cc776eb Mon Sep 17 00:00:00 2001 From: Sanmesh Kakade Date: Fri, 6 Feb 2026 18:15:04 +0530 Subject: [PATCH 12/13] fix: replace ServiceMonitor with PodMonitor for metrics scraping (#511) - Add prometheus_details optional input - Use PodMonitor instead of ServiceMonitor (services don't expose metrics port) - PodMonitor scrapes both control plane and data plane pods - Use helm_release_id from prometheus for the release label Co-authored-by: Claude Opus 4.5 --- .../1.0/facets.yaml | 6 +++++ .../nginx_gateway_fabric_legacy/1.0/main.tf | 22 ++++++++++--------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml b/modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml index b843dd42..1642fdf1 100644 --- a/modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml +++ b/modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml @@ -24,6 +24,12 @@ inputs: default: resource_type: gateway_api_crd resource_name: default + prometheus_details: + type: "@outputs/prometheus" + optional: true + default: + resource_type: prometheus + resource_name: default outputs: default: type: "@outputs/ingress" diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf b/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf index fe3741e3..019b49d5 100644 --- a/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf +++ b/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf @@ -481,33 +481,35 @@ locals { # Helm release name - keep under 63 chars for k8s label limit helm_release_name = substr(local.name, 0, min(length(local.name), 63)) - # ServiceMonitor (always enabled with defaults) - servicemonitor_resources = { - "servicemonitor-${local.name}" = { + # PodMonitor (only created when prometheus_details input is provided) + # Scrapes both control plane and data plane pods using common instance label + podmonitor_resources = lookup(var.inputs, "prometheus_details", null) != null ? { + "podmonitor-${local.name}" = { apiVersion = "monitoring.coreos.com/v1" - kind = "ServiceMonitor" + kind = "PodMonitor" metadata = { - name = "${local.name}-gateway-metrics" + name = "${local.name}-metrics" namespace = var.environment.namespace labels = { - prometheus = "kube-prometheus" + # Label required by Prometheus Operator to discover this PodMonitor + release = try(var.inputs.prometheus_details.attributes.helm_release_id, "prometheus") } } spec = { selector = { matchLabels = { - "app.kubernetes.io/name" = "nginx-gateway-fabric" + # Common label shared by both control plane and data plane pods "app.kubernetes.io/instance" = local.helm_release_name } } - endpoints = [{ + podMetricsEndpoints = [{ port = "metrics" interval = "30s" path = "/metrics" }] } } - } + } : {} # Collect unique namespaces that need ReferenceGrants (for cross-namespace backends) cross_namespace_backends = { @@ -574,7 +576,7 @@ locals { local.http_redirect_resources, local.httproute_resources, local.grpcroute_resources, - local.servicemonitor_resources, + local.podmonitor_resources, local.referencegrant_resources, local.clientsettingspolicy_resources ) From f40331cacb8d1c86418e154ecd0e100d43aa7213 Mon Sep 17 00:00:00 2001 From: Sanmesh Kakade Date: Mon, 9 Feb 2026 11:33:22 +0530 Subject: [PATCH 13/13] change default prometheus input --- modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml b/modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml index 1642fdf1..4232af56 100644 --- a/modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml +++ b/modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml @@ -28,8 +28,8 @@ inputs: type: "@outputs/prometheus" optional: true default: - resource_type: prometheus - resource_name: default + resource_type: configuration + resource_name: prometheus outputs: default: type: "@outputs/ingress"