diff --git a/intents/gateway_api_crd/facets.yaml b/intents/gateway_api_crd/facets.yaml new file mode 100644 index 00000000..8f5c476b --- /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" 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..43925aee --- /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:1.31.4" + 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/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..5523f5a4 --- /dev/null +++ b/modules/ingress/nginx_gateway_fabric_legacy/1.0/README.md @@ -0,0 +1,663 @@ +# 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": "PathPrefix" + } + } + } +} +``` + +### 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 | +|------|---------|-------------| +| `PathPrefix` | Yes | Matches paths starting with the specified prefix | +| `Exact` | No | Matches the exact path only | + +--- + +## 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": "PathPrefix", + "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": "PathPrefix", + "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": "PathPrefix", + "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": "PathPrefix", + "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": "PathPrefix", + "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": "PathPrefix", + "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": "PathPrefix", + "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": "PathPrefix", + "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": "PathPrefix", + "request_mirror": { + "service_name": "api-shadow", + "port": "8080", + "namespace": "testing" + } + } + } +} +``` + +--- + +## Multi-Domain Configuration + +### Custom Domains + +Configure custom domains inside spec: + +```json +{ + "kind": "ingress", + "flavor": "nginx_gateway_fabric_legacy", + "version": "1.0", + "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", + "namespace": "default", + "port": "8080", + "path": "/", + "path_type": "PathPrefix" + } + } + } +} +``` + +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 +{ + "spec": { + "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": "PathPrefix" + } + } + } +} +``` + +--- + +## 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` | List 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 00000000..dbdf7d2c Binary files /dev/null and b/modules/ingress/nginx_gateway_fabric_legacy/1.0/charts/nginx-gateway-fabric-2.3.0.tgz differ diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml b/modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml new file mode 100644 index 00000000..4232af56 --- /dev/null +++ b/modules/ingress/nginx_gateway_fabric_legacy/1.0/facets.yaml @@ -0,0 +1,889 @@ +intent: ingress +flavor: nginx_gateway_fabric_legacy +version: "1.0" +description: NGINX Gateway Fabric - Kubernetes Gateway API implementation (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 + - helm + gateway_api_crd_details: + type: "@outputs/gateway_api_crd" + optional: false + default: + resource_type: gateway_api_crd + resource_name: default + prometheus_details: + type: "@outputs/prometheus" + optional: true + default: + resource_type: configuration + resource_name: prometheus +outputs: + default: + type: "@outputs/ingress" +spec: + title: NGINX Gateway Fabric + type: object + properties: + private: + type: boolean + title: Private Load Balancer + description: Set load balancer as private/internal + disable_base_domain: + type: boolean + 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) + description: Configuration for NGINX pods that handle actual traffic + properties: + scaling: + type: object + title: Autoscaling + description: Horizontal Pod Autoscaler configuration + properties: + min_replicas: + type: integer + title: Minimum Replicas + description: Minimum number of NGINX pods (HPA lower bound) + default: 2 + max_replicas: + type: integer + title: Maximum Replicas + description: Maximum number of NGINX pods (HPA upper bound) + default: 10 + target_cpu_utilization_percentage: + type: integer + title: Target CPU Utilization (%) + description: Target CPU utilization percentage for HPA scaling + default: 70 + target_memory_utilization_percentage: + type: integer + title: Target Memory Utilization (%) + description: Target memory utilization percentage for HPA scaling + default: 80 + resources: + type: object + title: Resources + description: CPU and memory resources per NGINX pod + properties: + requests: + type: object + title: Resource Requests + description: Minimum resource requirements + properties: + cpu: + type: string + title: CPU Request + description: Minimum CPU requirement (e.g., 250m, 500m, 1) + pattern: ^([0-9]*\.?[0-9]+m?|[0-9]+)$ + default: 250m + x-ui-placeholder: 250m + memory: + type: string + title: Memory Request + description: Minimum memory requirement (e.g., 256Mi, 512Mi, 1Gi) + pattern: ^[0-9]+(\.[0-9]+)?(Ki|Mi|Gi|Ti|Pi|Ei|k|M|G|T|P|E)?$ + default: 256Mi + x-ui-placeholder: 256Mi + limits: + type: object + title: Resource Limits + description: Maximum resource limits + properties: + cpu: + type: string + title: CPU Limit + description: Maximum CPU allowed (e.g., 1, 2, 500m) + pattern: ^([0-9]*\.?[0-9]+m?|[0-9]+)$ + default: "1" + x-ui-placeholder: "1" + memory: + type: string + title: Memory Limit + description: Maximum memory allowed (e.g., 512Mi, 1Gi, 2Gi) + pattern: ^[0-9]+(\.[0-9]+)?(Ki|Mi|Gi|Ti|Pi|Ei|k|M|G|T|P|E)?$ + default: 512Mi + x-ui-placeholder: 512Mi + control_plane: + type: object + title: Control Plane (Gateway Controller) + description: Configuration for the NGINX Gateway Fabric controller that manages configuration + x-ui-toggle: true + properties: + scaling: + type: object + title: Autoscaling + description: Horizontal Pod Autoscaler configuration + properties: + min_replicas: + type: integer + title: Minimum Replicas + description: Minimum number of controller pods (HPA lower bound) + default: 2 + max_replicas: + type: integer + title: Maximum Replicas + description: Maximum number of controller pods (HPA upper bound) + default: 3 + target_cpu_utilization_percentage: + type: integer + title: Target CPU Utilization (%) + description: Target CPU utilization percentage for HPA scaling + default: 70 + target_memory_utilization_percentage: + type: integer + title: Target Memory Utilization (%) + description: Target memory utilization percentage for HPA scaling + default: 80 + resources: + type: object + title: Resources + description: CPU and memory resources per controller pod + properties: + requests: + type: object + title: Resource Requests + description: Minimum resource requirements + properties: + cpu: + type: string + title: CPU Request + description: Minimum CPU requirement (e.g., 100m, 250m, 500m) + pattern: ^([0-9]*\.?[0-9]+m?|[0-9]+)$ + default: 200m + x-ui-placeholder: 200m + memory: + type: string + title: Memory Request + description: Minimum memory requirement (e.g., 256Mi, 512Mi) + pattern: ^[0-9]+(\.[0-9]+)?(Ki|Mi|Gi|Ti|Pi|Ei|k|M|G|T|P|E)?$ + default: 256Mi + x-ui-placeholder: 256Mi + limits: + type: object + title: Resource Limits + description: Maximum resource limits + properties: + cpu: + type: string + title: CPU Limit + description: Maximum CPU allowed (e.g., 500m, 1, 2) + pattern: ^([0-9]*\.?[0-9]+m?|[0-9]+)$ + default: 500m + x-ui-placeholder: 500m + memory: + type: string + title: Memory Limit + description: Maximum memory allowed (e.g., 512Mi, 1Gi) + pattern: ^[0-9]+(\.[0-9]+)?(Ki|Mi|Gi|Ti|Pi|Ei|k|M|G|T|P|E)?$ + default: 512Mi + x-ui-placeholder: 512Mi + rules: + title: Routing Rules + description: Gateway API routing configurations (HTTP and gRPC) + + type: object + patternProperties: + ^[a-zA-Z0-9_.-]*$: + title: Route Object + description: HTTP route configuration + type: object + properties: + disable: + type: boolean + title: Disable Route + description: Enable/Disable this route + domain_prefix: + type: string + title: Domain Prefix + description: Subdomain prefix + pattern: ^[a-z0-9]([a-z0-9-]{0,34}[a-z0-9])?$|^[a-z0-9]$ + x-ui-placeholder: Enter the subdomain prefix + x-ui-error-message: Max 36 characters, only hyphens allowed, cannot start/end with hyphen + service_name: + type: string + title: Service Name + description: Kubernetes service name + x-ui-api-source: + endpoint: /cc-ui/v1/dropdown/stack/{{stackName}}/resources-info + method: GET + params: + includeContent: false + labelKey: resourceName + valueKey: resourceName + valueTemplate: ${service.{{value}}.out.attributes.service_name} + filterConditions: + - field: resourceType + value: service + x-ui-typeable: true + namespace: + type: string + title: Service Namespace + description: Kubernetes service namespace + x-ui-api-source: + endpoint: /cc-ui/v1/dropdown/stack/{{stackName}}/resources-info + method: GET + params: + includeContent: false + labelKey: resourceName + valueKey: resourceName + valueTemplate: ${service.{{value}}.out.attributes.namespace} + filterConditions: + - field: resourceType + value: service + x-ui-typeable: true + port: + type: string + title: Port + description: Service port number + x-ui-api-source: + endpoint: /cc-ui/v1/dropdown/stack/{{stackName}}/service/{{serviceName}}/overview + dynamicProperties: + serviceName: + key: service_name + lookup: regex + x-ui-lookup-regex: \${[^.]+\.([^.]+).* + method: GET + labelKey: port + valueKey: port + path: ports + x-ui-typeable: true + path: + type: string + title: Path + 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-visible-if: + field: spec.rules.{{this}}.grpc_config.enabled + values: + - 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 + default: PathPrefix + x-ui-visible-if: + field: spec.rules.{{this}}.grpc_config.enabled + values: + - false + - null + header_matches: + type: object + title: Header-Based Routing + description: Route traffic based on HTTP headers + x-ui-toggle: true + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Header Match + properties: + name: + type: string + title: Header Name + description: HTTP header name to match + x-ui-placeholder: X-API-Version + value: + type: string + title: Header Value + description: Header value to match + x-ui-placeholder: v2 + type: + type: string + title: Match Type + description: How to match the header value (Exact or RegularExpression) + enum: + - Exact + - RegularExpression + default: Exact + required: + - name + - value + query_param_matches: + type: object + title: Query Parameter Matching + description: Route traffic based on query parameters + x-ui-toggle: true + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Query Param Match + properties: + name: + type: string + title: Parameter Name + description: Query parameter name to match + x-ui-placeholder: version + value: + type: string + title: Parameter Value + description: Query parameter value to match + x-ui-placeholder: beta + type: + type: string + title: Match Type + description: How to match the parameter value (Exact or RegularExpression) + enum: + - Exact + - RegularExpression + default: Exact + required: + - name + - value + method: + type: string + title: HTTP Method + description: Match requests by HTTP method + enum: + - ALL + - GET + - POST + - PUT + - DELETE + - PATCH + - HEAD + - OPTIONS + default: ALL + x-ui-toggle: true + request_header_modifier: + type: object + title: Request Header Modification + description: Modify request headers + x-ui-toggle: true + properties: + add: + type: object + title: Add Headers + description: Headers to add to requests + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Header + properties: + name: + type: string + title: Header Name + description: Name of the header to add + x-ui-placeholder: X-Custom-Header + value: + type: string + title: Header Value + description: Value for the header + x-ui-placeholder: custom-value + required: + - name + - value + set: + type: object + title: Set Headers + description: Headers to set (override) in requests + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Header + properties: + name: + type: string + title: Header Name + description: Name of the header to set + x-ui-placeholder: X-Request-Source + value: + type: string + title: Header Value + description: Value for the header + x-ui-placeholder: gateway + required: + - name + - value + remove: + type: object + title: Remove Headers + description: Header names to remove from requests + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Header + properties: + name: + type: string + title: Header Name + description: Name of the header to remove + x-ui-placeholder: X-Sensitive-Header + required: + - name + response_header_modifier: + type: object + title: Response Header Modification + description: Modify response headers + x-ui-toggle: true + properties: + add: + type: object + title: Add Headers + description: Headers to add to responses + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Header + properties: + name: + type: string + title: Header Name + description: Name of the header to add + x-ui-placeholder: X-Response-ID + value: + type: string + title: Header Value + description: Value for the header + x-ui-placeholder: unique-id + required: + - name + - value + set: + type: object + title: Set Headers + description: Headers to set (override) in responses + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Header + properties: + name: + type: string + title: Header Name + description: Name of the header to set + x-ui-placeholder: Cache-Control + value: + type: string + title: Header Value + description: Value for the header + x-ui-placeholder: no-store + required: + - name + - value + remove: + type: object + title: Remove Headers + description: Header names to remove from responses + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Header + properties: + name: + type: string + title: Header Name + description: Name of the header to remove + x-ui-placeholder: Server + required: + - name + url_rewrite: + type: object + title: URL Rewriting + description: Rewrite request URLs + x-ui-toggle: true + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: URL Rewrite Rule + properties: + hostname: + type: string + title: Hostname + description: New hostname for the request + x-ui-placeholder: internal-api.svc.cluster.local + path_type: + type: string + title: Path Rewrite Type + description: How to rewrite the path (ReplaceFullPath or ReplacePrefixMatch) + enum: + - ReplaceFullPath + - ReplacePrefixMatch + replace_path: + type: string + title: Replace Path Value + description: New path value (full path or prefix depending on type) + x-ui-placeholder: /v2/api + canary_deployment: + type: object + title: Canary Deployment + description: Configure traffic splitting for canary deployments + x-ui-toggle: true + x-ui-skip: true + properties: + enabled: + type: boolean + title: Enable Canary + description: Enable canary deployment with traffic splitting + default: false + canary_service: + type: string + title: Canary Service Name + description: Name of the canary version service + x-ui-visible-if: + field: spec.rules.{{this}}.canary_deployment.enabled + values: + - true + canary_weight: + type: integer + title: Canary Traffic Percentage + description: Percentage of traffic to send to canary (0-100) + minimum: 0 + maximum: 100 + default: 10 + x-ui-visible-if: + field: spec.rules.{{this}}.canary_deployment.enabled + values: + - true + request_mirror: + type: object + title: Request Mirroring + description: Mirror traffic to a secondary backend for testing + x-ui-toggle: true + x-ui-skip: true + properties: + service_name: + type: string + title: Mirror Service Name + description: Name of the service to mirror traffic to + port: + type: string + title: Mirror Service Port + description: Port of the mirror service + namespace: + type: string + title: Mirror Service Namespace + description: Namespace of the mirror service + timeouts: + type: object + title: Request Timeouts + description: Configure request and backend timeouts + x-ui-toggle: true + properties: + request: + type: string + title: Request Timeout + description: Total request timeout (e.g., 30s, 1m, 5m) + pattern: ^\d+[smh]$ + default: 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: "300s" + cors: + type: object + title: CORS Configuration + description: Configure Cross-Origin Resource Sharing + x-ui-toggle: true + properties: + enabled: + type: boolean + title: Enable CORS + description: Enable Cross-Origin Resource Sharing headers + default: false + allow_origins: + type: object + title: Allowed Origins + description: List of allowed origins (* for all) + x-ui-visible-if: + field: spec.rules.{{this}}.cors.enabled + values: + - true + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Origin + properties: + origin: + type: string + title: Origin URL + description: Allowed origin URL + x-ui-placeholder: https://example.com + required: + - origin + allow_methods: + type: object + title: Allowed Methods + description: HTTP methods allowed for CORS requests + x-ui-visible-if: + field: spec.rules.{{this}}.cors.enabled + values: + - true + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Method + properties: + method: + type: string + title: HTTP Method + description: HTTP method to allow + enum: + - GET + - POST + - PUT + - DELETE + - PATCH + - OPTIONS + - HEAD + required: + - method + allow_headers: + type: object + title: Allowed Headers + description: HTTP headers allowed in CORS requests + x-ui-visible-if: + field: spec.rules.{{this}}.cors.enabled + values: + - true + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Header + properties: + header: + type: string + title: Header Name + description: Header name to allow + x-ui-placeholder: Content-Type + required: + - header + allow_credentials: + type: boolean + title: Allow Credentials + description: Allow cookies and authentication headers in CORS requests + default: false + x-ui-visible-if: + field: spec.rules.{{this}}.cors.enabled + values: + - true + max_age: + type: integer + title: Preflight Cache Max Age + description: Seconds to cache preflight response + default: 86400 + x-ui-visible-if: + field: spec.rules.{{this}}.cors.enabled + values: + - true + grpc_config: + type: object + title: gRPC Configuration + description: Configure gRPC routing + x-ui-toggle: true + properties: + enabled: + type: boolean + title: Enable gRPC + description: Enable gRPC routing for this service + default: false + match_all_methods: + type: boolean + title: Match All Methods + description: Route all gRPC traffic (no method filtering) + default: true + x-ui-visible-if: + field: spec.rules.{{this}}.grpc_config.enabled + values: + - true + method_match: + type: object + title: gRPC Method Matching + description: Whitelist specific gRPC services/methods + x-ui-visible-if: + field: spec.rules.{{this}}.grpc_config.match_all_methods + values: + - false + patternProperties: + ^[a-zA-Z0-9_.-]*$: + type: object + title: Method Match + properties: + service: + type: string + title: Service Name + description: Protobuf service name (e.g., helloworld.Greeter) + method: + type: string + title: Method Name + description: gRPC method name (e.g., SayHello) + type: + type: string + title: Match Type + description: NGINX Gateway Fabric only supports Exact matching + enum: + - Exact + default: Exact + required: + - service + - method + required: + - 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 + force_ssl_redirection: + type: boolean + title: Force SSL Redirection + description: Force HTTP to HTTPS redirection + body_size: + type: string + title: Max Body Size + description: Maximum allowed size of the client request body (e.g., 150m, 1g) + pattern: ^\d{1,4}(k|m|g)?$ + default: 150m + x-ui-placeholder: 150m + x-ui-toggle: true + helm_values: + type: object + title: Additional Helm Values + description: "Additional Helm values to merge with the default configuration. See available values at: https://github.com/nginxinc/nginx-gateway-fabric/blob/main/charts/nginx-gateway-fabric/values.yaml" + x-ui-toggle: true + x-ui-yaml-editor: true + domain_prefix_override: + type: string + title: Domain Prefix Override + description: Override the automatically generated domain prefix + x-ui-toggle: true + disable_endpoint_validation: + type: boolean + title: Disable Endpoint Validation + description: Disable endpoint validation for cert-manager (uses DNS validation instead of HTTP) + default: false + x-ui-toggle: true + helm_wait: + type: boolean + title: Helm Wait + description: Wait for all resources to be ready before marking the release as successful + default: true + x-ui-toggle: true + required: + - 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 +sample: + kind: ingress + flavor: nginx_gateway_fabric_legacy + version: "1.0" + disabled: true + metadata: + annotations: {} + spec: + private: false + disable_base_domain: false + force_ssl_redirection: true + body_size: 150m + data_plane: + scaling: + min_replicas: 2 + max_replicas: 5 + target_cpu_utilization_percentage: 70 + target_memory_utilization_percentage: 80 + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: "1" + memory: 512Mi + control_plane: + scaling: + min_replicas: 2 + max_replicas: 3 + target_cpu_utilization_percentage: 70 + target_memory_utilization_percentage: 80 + resources: + requests: + cpu: 200m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + rules: {} diff --git a/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf b/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf new file mode 100644 index 00000000..019b49d5 --- /dev/null +++ b/modules/ingress/nginx_gateway_fabric_legacy/1.0/main.tf @@ -0,0 +1,1179 @@ +locals { + tenant_provider = lower(lookup(var.cc_metadata, "cc_tenant_provider", "aws")) + base_helm_values = lookup(var.instance.spec, "helm_values", {}) + + # Load balancer configuration - determine record type based on what's actually available + lb_hostname = try(data.kubernetes_service.gateway_lb.status[0].load_balancer[0].ingress[0].hostname, "") + lb_ip = try(data.kubernetes_service.gateway_lb.status[0].load_balancer[0].ingress[0].ip, "") + record_type = local.lb_hostname != "" ? "CNAME" : "A" + lb_record_value = local.lb_hostname != "" ? local.lb_hostname : local.lb_ip + + # Rules configuration + rulesRaw = lookup(var.instance.spec, "rules", {}) + + # Domain configuration (same as nginx_k8s) + instance_env_name = length(var.environment.unique_name) + length(var.instance_name) + length(var.cc_metadata.tenant_base_domain) >= 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.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] + + # 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 PathPrefix) + (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 + common_labels = { + "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) + [merge( + { + path = { + type = lookup(v, "path_type", "PathPrefix") + value = 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) + } + + # Helm release name - keep under 63 chars for k8s label limit + helm_release_name = substr(local.name, 0, min(length(local.name), 63)) + + # 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 = "PodMonitor" + metadata = { + name = "${local.name}-metrics" + namespace = var.environment.namespace + labels = { + # Label required by Prometheus Operator to discover this PodMonitor + release = try(var.inputs.prometheus_details.attributes.helm_release_id, "prometheus") + } + } + spec = { + selector = { + matchLabels = { + # Common label shared by both control plane and data plane pods + "app.kubernetes.io/instance" = local.helm_release_name + } + } + podMetricsEndpoints = [{ + 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 + }, + { + group = "gateway.networking.k8s.io" + kind = "GRPCRoute" + 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.podmonitor_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 = { + # 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\" $request_length $request_time [$proxy_host] $upstream_addr $upstream_response_length $upstream_response_time $upstream_status" + } + } + } + ) + + # 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" + # loadBalancerClass = local.cloud_provider == "AWS" ? "service.k8s.aws/nlb" : "" + 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 = {} +} 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: []