Difference between revisions of "Envoy from nginx for ingress"

From UVOO Tech Wiki
Jump to navigation Jump to search
(Created page with "go install github.com/kubernetes-sigs/ingress2gateway@latest # Translate an existing NGINX ingress into Gateway API manifests ingress2gateway print --providers ingress-nginx...")
 
 
Line 3: Line 3:
 
# Translate an existing NGINX ingress into Gateway API manifests
 
# Translate an existing NGINX ingress into Gateway API manifests
 
ingress2gateway print --providers ingress-nginx > new-envoy-routes.yaml
 
ingress2gateway print --providers ingress-nginx > new-envoy-routes.yaml
 +
 +
 +
ingress-to-envoy-gateway.sh
 +
```
 +
#!/usr/bin/env bash
 +
set -euo pipefail
 +
 +
usage() {
 +
  cat <<'USAGE'
 +
Usage:
 +
  scripts/ingress-to-envoy-gateway.sh -n NAMESPACE INGRESS [options]
 +
 +
Converts one Kubernetes Ingress into Gateway API resources for Envoy Gateway.
 +
By default the script prints YAML. Use --apply to apply it.
 +
 +
Options:
 +
  --gateway NAME                Gateway name. Default: INGRESS-gateway
 +
  --gateway-namespace NAMESPACE  Gateway namespace. Default: Ingress namespace
 +
  --use-existing-gateway        Do not emit a Gateway; attach routes to --gateway
 +
  --gateway-class NAME          GatewayClass name. Default: eg
 +
  --issuer NAME                  ClusterIssuer name for generated Certificates. Default: letsencrypt-prod
 +
  --emit-certificate            Emit cert-manager Certificate resources for Ingress TLS secrets.
 +
                                  With --use-existing-gateway, Certificates are emitted in
 +
                                  the Gateway namespace and --apply patches the Gateway
 +
                                  HTTPS listener certificateRefs.
 +
  --force-tls                    Route to the HTTPS listener even if the Ingress has no TLS block
 +
  --tls-secret NAME              TLS Secret/Certificate name to use with --force-tls
 +
  --backend-tls none|system|ca  Handle nginx backend-protocol=HTTPS. Default: none
 +
  --backend-tls-hostname NAME    SNI/validation hostname for BackendTLSPolicy. Default: first Ingress host
 +
  --backend-tls-ca-configmap NAME
 +
                                  ConfigMap with ca.crt when --backend-tls=ca
 +
  --apply                        Apply generated YAML instead of printing it
 +
  -h, --help                    Show this help
 +
 +
Examples:
 +
  # Preview conversion for uapp/ucontrol-ui.
 +
  scripts/ingress-to-envoy-gateway.sh -n uapp ucontrol-ui --backend-tls system
 +
 +
  # Apply conversion.
 +
  scripts/ingress-to-envoy-gateway.sh -n uapp ucontrol-ui --backend-tls system --apply
 +
 +
  # Attach uapp/ucontrol-ui to an existing central Gateway.
 +
  scripts/ingress-to-envoy-gateway.sh -n uapp ucontrol-ui \
 +
    --use-existing-gateway \
 +
    --gateway default-gateway \
 +
    --gateway-namespace example-io \
 +
    --backend-tls system
 +
 +
Notes:
 +
  - The original Ingress is not changed or deleted.
 +
  - DNS must be moved to the new Envoy Gateway IP after verification.
 +
  - nginx.ingress.kubernetes.io/backend-protocol=HTTPS requires BackendTLSPolicy.
 +
    Use --backend-tls system for publicly trusted backend certs, or --backend-tls ca
 +
    with --backend-tls-ca-configmap for private CAs.
 +
USAGE
 +
}
 +
 +
die() {
 +
  printf 'error: %s\n' "$*" >&2
 +
  exit 1
 +
}
 +
 +
need() {
 +
  command -v "$1" >/dev/null 2>&1 || die "missing required command: $1"
 +
}
 +
 +
default_kubectl() {
 +
  if [[ -x /snap/kubectl/current/kubectl ]]; then
 +
    printf '%s\n' /snap/kubectl/current/kubectl
 +
  else
 +
    printf '%s\n' kubectl
 +
  fi
 +
}
 +
 +
yaml_quote() {
 +
  printf '%s' "$1" | sed "s/'/''/g; s/^/'/; s/$/'/"
 +
}
 +
 +
resource_name() {
 +
  printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9.-]+/-/g; s/^-+//; s/-+$//' | cut -c1-63
 +
}
 +
 +
resolve_service_port() {
 +
  local svc="$1"
 +
  local port_name="$2"
 +
 +
  "$kubectl_cmd" get svc -n "$namespace" "$svc" -o json |
 +
    jq -r --arg name "$port_name" '
 +
      .spec.ports[] | select(.name == $name) | .port
 +
    ' | head -n 1
 +
}
 +
 +
gateway_has_cert_ref() {
 +
  local secret="$1"
 +
 +
  "$kubectl_cmd" get gateway -n "$gateway_namespace" "$gateway" -o json |
 +
    jq -e --arg secret "$secret" '
 +
      .spec.listeners[]
 +
      | select(.name == "https")
 +
      | (.tls.certificateRefs // [])
 +
      | any(.name == $secret)
 +
    ' >/dev/null
 +
}
 +
 +
patch_gateway_cert_ref() {
 +
  local secret="$1"
 +
 +
  if gateway_has_cert_ref "$secret"; then
 +
    printf 'Gateway %s/%s already references TLS secret %s\n' "$gateway_namespace" "$gateway" "$secret" >&2
 +
    return
 +
  fi
 +
 +
  "$kubectl_cmd" patch gateway "$gateway" -n "$gateway_namespace" --type json \
 +
    -p="[{\"op\":\"add\",\"path\":\"/spec/listeners/1/tls/certificateRefs/-\",\"value\":{\"name\":\"$secret\"}}]"
 +
}
 +
 +
namespace=""
 +
ingress=""
 +
gateway=""
 +
gateway_namespace=""
 +
create_gateway="true"
 +
gateway_class="eg"
 +
issuer="letsencrypt-prod"
 +
emit_certificate="false"
 +
backend_tls="none"
 +
backend_tls_hostname=""
 +
backend_tls_ca_configmap=""
 +
force_tls="false"
 +
tls_secret_override=""
 +
apply="false"
 +
kubectl_cmd="${KUBECTL:-$(default_kubectl)}"
 +
 +
while [[ $# -gt 0 ]]; do
 +
  case "$1" in
 +
    -n|--namespace)
 +
      namespace="${2:-}"
 +
      shift 2
 +
      ;;
 +
    --gateway)
 +
      gateway="${2:-}"
 +
      shift 2
 +
      ;;
 +
    --gateway-namespace)
 +
      gateway_namespace="${2:-}"
 +
      shift 2
 +
      ;;
 +
    --use-existing-gateway)
 +
      create_gateway="false"
 +
      shift
 +
      ;;
 +
    --gateway-class)
 +
      gateway_class="${2:-}"
 +
      shift 2
 +
      ;;
 +
    --issuer)
 +
      issuer="${2:-}"
 +
      shift 2
 +
      ;;
 +
    --emit-certificate)
 +
      emit_certificate="true"
 +
      shift
 +
      ;;
 +
    --force-tls)
 +
      force_tls="true"
 +
      shift
 +
      ;;
 +
    --tls-secret)
 +
      tls_secret_override="${2:-}"
 +
      shift 2
 +
      ;;
 +
    --backend-tls)
 +
      backend_tls="${2:-}"
 +
      shift 2
 +
      ;;
 +
    --backend-tls-hostname)
 +
      backend_tls_hostname="${2:-}"
 +
      shift 2
 +
      ;;
 +
    --backend-tls-ca-configmap)
 +
      backend_tls_ca_configmap="${2:-}"
 +
      shift 2
 +
      ;;
 +
    --apply)
 +
      apply="true"
 +
      shift
 +
      ;;
 +
    -h|--help)
 +
      usage
 +
      exit 0
 +
      ;;
 +
    -*)
 +
      die "unknown option: $1"
 +
      ;;
 +
    *)
 +
      if [[ -n "$ingress" ]]; then
 +
        die "unexpected extra argument: $1"
 +
      fi
 +
      ingress="$1"
 +
      shift
 +
      ;;
 +
  esac
 +
done
 +
 +
[[ -n "$namespace" ]] || die "namespace is required"
 +
[[ -n "$ingress" ]] || die "ingress name is required"
 +
case "$backend_tls" in
 +
  none|system|ca) ;;
 +
  *) die "--backend-tls must be one of: none, system, ca" ;;
 +
esac
 +
if [[ "$backend_tls" == "ca" && -z "$backend_tls_ca_configmap" ]]; then
 +
  die "--backend-tls-ca-configmap is required when --backend-tls=ca"
 +
fi
 +
if [[ "$force_tls" == "true" && -z "$tls_secret_override" ]]; then
 +
  die "--tls-secret is required when --force-tls is set"
 +
fi
 +
 +
need jq
 +
need sed
 +
need tr
 +
need cut
 +
need head
 +
if [[ "$kubectl_cmd" == */* ]]; then
 +
  [[ -x "$kubectl_cmd" ]] || die "kubectl is not executable: $kubectl_cmd"
 +
else
 +
  need "$kubectl_cmd"
 +
fi
 +
 +
gateway="${gateway:-$(resource_name "${ingress}-gateway")}"
 +
gateway_namespace="${gateway_namespace:-$namespace}"
 +
 +
ingress_json="$("$kubectl_cmd" get ingress -n "$namespace" "$ingress" -o json)"
 +
 +
mapfile -t hosts < <(jq -r '.spec.rules[]?.host // empty' <<<"$ingress_json" | awk 'NF' | sort -u)
 +
[[ "${#hosts[@]}" -gt 0 ]] || die "Ingress has no host rules"
 +
 +
if [[ -z "$backend_tls_hostname" ]]; then
 +
  backend_tls_hostname="${hosts[0]}"
 +
fi
 +
 +
mapfile -t tls_secrets < <(jq -r '.spec.tls[]?.secretName // empty' <<<"$ingress_json" | awk 'NF' | sort -u)
 +
if [[ -n "$tls_secret_override" ]]; then
 +
  tls_secrets=("$tls_secret_override")
 +
fi
 +
has_tls="false"
 +
if [[ "${#tls_secrets[@]}" -gt 0 || "$force_tls" == "true" ]]; then
 +
  has_tls="true"
 +
fi
 +
if [[ "$create_gateway" == "false" && "$has_tls" == "true" ]]; then
 +
  emit_certificate="true"
 +
fi
 +
 +
ssl_redirect="$(
 +
  jq -r '
 +
    .metadata.annotations["nginx.ingress.kubernetes.io/ssl-redirect"] //
 +
    .metadata.annotations["nginx.ingress.kubernetes.io/force-ssl-redirect"] //
 +
    "false"
 +
  ' <<<"$ingress_json"
 +
)"
 +
backend_protocol="$(jq -r '.metadata.annotations["nginx.ingress.kubernetes.io/backend-protocol"] // "HTTP"' <<<"$ingress_json" | tr '[:lower:]' '[:upper:]')"
 +
 +
if [[ "$backend_protocol" == "HTTPS" && "$backend_tls" == "none" ]]; then
 +
  printf 'warning: %s/%s uses nginx backend-protocol=HTTPS; generated route will need BackendTLSPolicy or backend HTTP support.\n' "$namespace" "$ingress" >&2
 +
fi
 +
 +
tmp="$(mktemp)"
 +
trap 'rm -f "$tmp"' EXIT
 +
 +
{
 +
  if [[ "$create_gateway" == "true" ]]; then
 +
    cat <<EOF
 +
apiVersion: gateway.networking.k8s.io/v1
 +
kind: Gateway
 +
metadata:
 +
  name: $gateway
 +
  namespace: $gateway_namespace
 +
  labels:
 +
    app.kubernetes.io/managed-by: ingress-to-envoy-gateway
 +
    app.kubernetes.io/source-ingress: $ingress
 +
spec:
 +
  gatewayClassName: $gateway_class
 +
  listeners:
 +
    - name: http
 +
      port: 80
 +
      protocol: HTTP
 +
      allowedRoutes:
 +
        namespaces:
 +
          from: Same
 +
EOF
 +
 +
    if [[ "$has_tls" == "true" ]]; then
 +
      cat <<'EOF'
 +
    - name: https
 +
      port: 443
 +
      protocol: HTTPS
 +
      tls:
 +
        mode: Terminate
 +
        certificateRefs:
 +
EOF
 +
      for secret in "${tls_secrets[@]}"; do
 +
        printf '          - name: %s\n' "$secret"
 +
      done
 +
      cat <<'EOF'
 +
      allowedRoutes:
 +
        namespaces:
 +
          from: Same
 +
EOF
 +
    fi
 +
    printf '%s\n' '---'
 +
  fi
 +
 +
  cat <<EOF
 +
apiVersion: gateway.networking.k8s.io/v1
 +
kind: HTTPRoute
 +
metadata:
 +
  name: $ingress
 +
  namespace: $namespace
 +
  labels:
 +
    app.kubernetes.io/managed-by: ingress-to-envoy-gateway
 +
    app.kubernetes.io/source-ingress: $ingress
 +
spec:
 +
  hostnames:
 +
EOF
 +
  for host in "${hosts[@]}"; do
 +
    printf '    - %s\n' "$(yaml_quote "$host")"
 +
  done
 +
  cat <<EOF
 +
  parentRefs:
 +
    - name: $gateway
 +
      namespace: $gateway_namespace
 +
      sectionName: $([[ "$has_tls" == "true" ]] && printf 'https' || printf 'http')
 +
  rules:
 +
EOF
 +
 +
  route_count="$(jq '[.spec.rules[]?.http.paths[]?] | length' <<<"$ingress_json")"
 +
  [[ "$route_count" != "0" ]] || die "Ingress has no HTTP paths"
 +
 +
  for i in $(seq 0 "$((route_count - 1))"); do
 +
    item="$(jq -c "[.spec.rules[]?.http.paths[]?][$i]" <<<"$ingress_json")"
 +
    path="$(jq -r '.path // "/"' <<<"$item")"
 +
    path_type="$(jq -r '.pathType // "Prefix"' <<<"$item")"
 +
    svc="$(jq -r '.backend.service.name // empty' <<<"$item")"
 +
    port_number="$(jq -r '.backend.service.port.number // empty' <<<"$item")"
 +
    port_name="$(jq -r '.backend.service.port.name // empty' <<<"$item")"
 +
 +
    [[ -n "$svc" ]] || die "path $i has no Service backend"
 +
    if [[ -z "$port_number" && -n "$port_name" ]]; then
 +
      port_number="$(resolve_service_port "$svc" "$port_name")"
 +
      [[ -n "$port_number" ]] || die "could not resolve named port $svc/$port_name"
 +
    fi
 +
    [[ -n "$port_number" ]] || die "path $i has no Service port"
 +
 +
    case "$path_type" in
 +
      Prefix) gateway_path_type="PathPrefix" ;;
 +
      Exact) gateway_path_type="Exact" ;;
 +
      ImplementationSpecific) gateway_path_type="PathPrefix" ;;
 +
      *) gateway_path_type="PathPrefix" ;;
 +
    esac
 +
 +
    cat <<EOF
 +
    - matches:
 +
        - path:
 +
            type: $gateway_path_type
 +
            value: $(yaml_quote "$path")
 +
      backendRefs:
 +
        - name: $svc
 +
          port: $port_number
 +
EOF
 +
  done
 +
 +
  if [[ "$has_tls" == "true" ]] && { [[ "$ssl_redirect" == "true" ]] || [[ "$force_tls" == "true" ]]; }; then
 +
    cat <<EOF
 +
---
 +
apiVersion: gateway.networking.k8s.io/v1
 +
kind: HTTPRoute
 +
metadata:
 +
  name: $(resource_name "${ingress}-https-redirect")
 +
  namespace: $namespace
 +
  labels:
 +
    app.kubernetes.io/managed-by: ingress-to-envoy-gateway
 +
    app.kubernetes.io/source-ingress: $ingress
 +
spec:
 +
  hostnames:
 +
EOF
 +
    for host in "${hosts[@]}"; do
 +
      printf '    - %s\n' "$(yaml_quote "$host")"
 +
    done
 +
    cat <<EOF
 +
  parentRefs:
 +
    - name: $gateway
 +
      namespace: $gateway_namespace
 +
      sectionName: http
 +
  rules:
 +
    - filters:
 +
        - type: RequestRedirect
 +
          requestRedirect:
 +
            scheme: https
 +
            statusCode: 301
 +
      matches:
 +
        - path:
 +
            type: PathPrefix
 +
            value: /
 +
EOF
 +
  fi
 +
 +
  if [[ "$backend_protocol" == "HTTPS" && "$backend_tls" != "none" ]]; then
 +
    mapfile -t https_services < <(
 +
      jq -r '.spec.rules[]?.http.paths[]?.backend.service.name // empty' <<<"$ingress_json" |
 +
        awk 'NF' | sort -u
 +
    )
 +
    for svc in "${https_services[@]}"; do
 +
      cat <<EOF
 +
---
 +
apiVersion: gateway.networking.k8s.io/v1
 +
kind: BackendTLSPolicy
 +
metadata:
 +
  name: $(resource_name "${ingress}-${svc}-backend-tls")
 +
  namespace: $namespace
 +
  labels:
 +
    app.kubernetes.io/managed-by: ingress-to-envoy-gateway
 +
    app.kubernetes.io/source-ingress: $ingress
 +
spec:
 +
  targetRefs:
 +
    - group: ""
 +
      kind: Service
 +
      name: $svc
 +
  validation:
 +
    hostname: $(yaml_quote "$backend_tls_hostname")
 +
EOF
 +
      if [[ "$backend_tls" == "system" ]]; then
 +
        cat <<'EOF'
 +
    wellKnownCACertificates: System
 +
EOF
 +
      else
 +
        cat <<EOF
 +
    caCertificateRefs:
 +
      - group: ""
 +
        kind: ConfigMap
 +
        name: $backend_tls_ca_configmap
 +
EOF
 +
      fi
 +
    done
 +
  fi
 +
 +
  if [[ "$emit_certificate" == "true" && "$has_tls" == "true" ]]; then
 +
    for secret in "${tls_secrets[@]}"; do
 +
      mapfile -t cert_hosts < <(
 +
        jq -r --arg secret "$secret" '
 +
          .spec.tls[]? | select(.secretName == $secret) | .hosts[]?
 +
        ' <<<"$ingress_json" | awk 'NF' | sort -u
 +
      )
 +
      [[ "${#cert_hosts[@]}" -gt 0 ]] || cert_hosts=("${hosts[@]}")
 +
      cat <<EOF
 +
---
 +
apiVersion: cert-manager.io/v1
 +
kind: Certificate
 +
metadata:
 +
  name: $secret
 +
  namespace: $([[ "$create_gateway" == "false" ]] && printf '%s' "$gateway_namespace" || printf '%s' "$namespace")
 +
  labels:
 +
    app.kubernetes.io/managed-by: ingress-to-envoy-gateway
 +
    app.kubernetes.io/source-ingress: $ingress
 +
spec:
 +
  secretName: $secret
 +
  dnsNames:
 +
EOF
 +
      for host in "${cert_hosts[@]}"; do
 +
        printf '    - %s\n' "$(yaml_quote "$host")"
 +
      done
 +
      cat <<EOF
 +
  issuerRef:
 +
    group: cert-manager.io
 +
    kind: ClusterIssuer
 +
    name: $issuer
 +
EOF
 +
    done
 +
  fi
 +
} >"$tmp"
 +
 +
if [[ "$apply" == "true" ]]; then
 +
  "$kubectl_cmd" apply -f "$tmp"
 +
  if [[ "$create_gateway" == "false" && "$has_tls" == "true" ]]; then
 +
    for secret in "${tls_secrets[@]}"; do
 +
      patch_gateway_cert_ref "$secret"
 +
    done
 +
  fi
 +
else
 +
  cat "$tmp"
 +
fi
 +
```

Latest revision as of 05:29, 22 June 2026

go install github.com/kubernetes-sigs/ingress2gateway@latest

Translate an existing NGINX ingress into Gateway API manifests

ingress2gateway print --providers ingress-nginx > new-envoy-routes.yaml

ingress-to-envoy-gateway.sh

#!/usr/bin/env bash
set -euo pipefail

usage() {
  cat <<'USAGE'
Usage:
  scripts/ingress-to-envoy-gateway.sh -n NAMESPACE INGRESS [options]

Converts one Kubernetes Ingress into Gateway API resources for Envoy Gateway.
By default the script prints YAML. Use --apply to apply it.

Options:
  --gateway NAME                 Gateway name. Default: INGRESS-gateway
  --gateway-namespace NAMESPACE  Gateway namespace. Default: Ingress namespace
  --use-existing-gateway         Do not emit a Gateway; attach routes to --gateway
  --gateway-class NAME           GatewayClass name. Default: eg
  --issuer NAME                  ClusterIssuer name for generated Certificates. Default: letsencrypt-prod
  --emit-certificate             Emit cert-manager Certificate resources for Ingress TLS secrets.
                                  With --use-existing-gateway, Certificates are emitted in
                                  the Gateway namespace and --apply patches the Gateway
                                  HTTPS listener certificateRefs.
  --force-tls                    Route to the HTTPS listener even if the Ingress has no TLS block
  --tls-secret NAME              TLS Secret/Certificate name to use with --force-tls
  --backend-tls none|system|ca   Handle nginx backend-protocol=HTTPS. Default: none
  --backend-tls-hostname NAME    SNI/validation hostname for BackendTLSPolicy. Default: first Ingress host
  --backend-tls-ca-configmap NAME
                                  ConfigMap with ca.crt when --backend-tls=ca
  --apply                        Apply generated YAML instead of printing it
  -h, --help                     Show this help

Examples:
  # Preview conversion for uapp/ucontrol-ui.
  scripts/ingress-to-envoy-gateway.sh -n uapp ucontrol-ui --backend-tls system

  # Apply conversion.
  scripts/ingress-to-envoy-gateway.sh -n uapp ucontrol-ui --backend-tls system --apply

  # Attach uapp/ucontrol-ui to an existing central Gateway.
  scripts/ingress-to-envoy-gateway.sh -n uapp ucontrol-ui \
    --use-existing-gateway \
    --gateway default-gateway \
    --gateway-namespace example-io \
    --backend-tls system

Notes:
  - The original Ingress is not changed or deleted.
  - DNS must be moved to the new Envoy Gateway IP after verification.
  - nginx.ingress.kubernetes.io/backend-protocol=HTTPS requires BackendTLSPolicy.
    Use --backend-tls system for publicly trusted backend certs, or --backend-tls ca
    with --backend-tls-ca-configmap for private CAs.
USAGE
}

die() {
  printf 'error: %s\n' "$*" >&2
  exit 1
}

need() {
  command -v "$1" >/dev/null 2>&1 || die "missing required command: $1"
}

default_kubectl() {
  if [[ -x /snap/kubectl/current/kubectl ]]; then
    printf '%s\n' /snap/kubectl/current/kubectl
  else
    printf '%s\n' kubectl
  fi
}

yaml_quote() {
  printf '%s' "$1" | sed "s/'/''/g; s/^/'/; s/$/'/"
}

resource_name() {
  printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9.-]+/-/g; s/^-+//; s/-+$//' | cut -c1-63
}

resolve_service_port() {
  local svc="$1"
  local port_name="$2"

  "$kubectl_cmd" get svc -n "$namespace" "$svc" -o json |
    jq -r --arg name "$port_name" '
      .spec.ports[] | select(.name == $name) | .port
    ' | head -n 1
}

gateway_has_cert_ref() {
  local secret="$1"

  "$kubectl_cmd" get gateway -n "$gateway_namespace" "$gateway" -o json |
    jq -e --arg secret "$secret" '
      .spec.listeners[]
      | select(.name == "https")
      | (.tls.certificateRefs // [])
      | any(.name == $secret)
    ' >/dev/null
}

patch_gateway_cert_ref() {
  local secret="$1"

  if gateway_has_cert_ref "$secret"; then
    printf 'Gateway %s/%s already references TLS secret %s\n' "$gateway_namespace" "$gateway" "$secret" >&2
    return
  fi

  "$kubectl_cmd" patch gateway "$gateway" -n "$gateway_namespace" --type json \
    -p="[{\"op\":\"add\",\"path\":\"/spec/listeners/1/tls/certificateRefs/-\",\"value\":{\"name\":\"$secret\"}}]"
}

namespace=""
ingress=""
gateway=""
gateway_namespace=""
create_gateway="true"
gateway_class="eg"
issuer="letsencrypt-prod"
emit_certificate="false"
backend_tls="none"
backend_tls_hostname=""
backend_tls_ca_configmap=""
force_tls="false"
tls_secret_override=""
apply="false"
kubectl_cmd="${KUBECTL:-$(default_kubectl)}"

while [[ $# -gt 0 ]]; do
  case "$1" in
    -n|--namespace)
      namespace="${2:-}"
      shift 2
      ;;
    --gateway)
      gateway="${2:-}"
      shift 2
      ;;
    --gateway-namespace)
      gateway_namespace="${2:-}"
      shift 2
      ;;
    --use-existing-gateway)
      create_gateway="false"
      shift
      ;;
    --gateway-class)
      gateway_class="${2:-}"
      shift 2
      ;;
    --issuer)
      issuer="${2:-}"
      shift 2
      ;;
    --emit-certificate)
      emit_certificate="true"
      shift
      ;;
    --force-tls)
      force_tls="true"
      shift
      ;;
    --tls-secret)
      tls_secret_override="${2:-}"
      shift 2
      ;;
    --backend-tls)
      backend_tls="${2:-}"
      shift 2
      ;;
    --backend-tls-hostname)
      backend_tls_hostname="${2:-}"
      shift 2
      ;;
    --backend-tls-ca-configmap)
      backend_tls_ca_configmap="${2:-}"
      shift 2
      ;;
    --apply)
      apply="true"
      shift
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    -*)
      die "unknown option: $1"
      ;;
    *)
      if [[ -n "$ingress" ]]; then
        die "unexpected extra argument: $1"
      fi
      ingress="$1"
      shift
      ;;
  esac
done

[[ -n "$namespace" ]] || die "namespace is required"
[[ -n "$ingress" ]] || die "ingress name is required"
case "$backend_tls" in
  none|system|ca) ;;
  *) die "--backend-tls must be one of: none, system, ca" ;;
esac
if [[ "$backend_tls" == "ca" && -z "$backend_tls_ca_configmap" ]]; then
  die "--backend-tls-ca-configmap is required when --backend-tls=ca"
fi
if [[ "$force_tls" == "true" && -z "$tls_secret_override" ]]; then
  die "--tls-secret is required when --force-tls is set"
fi

need jq
need sed
need tr
need cut
need head
if [[ "$kubectl_cmd" == */* ]]; then
  [[ -x "$kubectl_cmd" ]] || die "kubectl is not executable: $kubectl_cmd"
else
  need "$kubectl_cmd"
fi

gateway="${gateway:-$(resource_name "${ingress}-gateway")}"
gateway_namespace="${gateway_namespace:-$namespace}"

ingress_json="$("$kubectl_cmd" get ingress -n "$namespace" "$ingress" -o json)"

mapfile -t hosts < <(jq -r '.spec.rules[]?.host // empty' <<<"$ingress_json" | awk 'NF' | sort -u)
[[ "${#hosts[@]}" -gt 0 ]] || die "Ingress has no host rules"

if [[ -z "$backend_tls_hostname" ]]; then
  backend_tls_hostname="${hosts[0]}"
fi

mapfile -t tls_secrets < <(jq -r '.spec.tls[]?.secretName // empty' <<<"$ingress_json" | awk 'NF' | sort -u)
if [[ -n "$tls_secret_override" ]]; then
  tls_secrets=("$tls_secret_override")
fi
has_tls="false"
if [[ "${#tls_secrets[@]}" -gt 0 || "$force_tls" == "true" ]]; then
  has_tls="true"
fi
if [[ "$create_gateway" == "false" && "$has_tls" == "true" ]]; then
  emit_certificate="true"
fi

ssl_redirect="$(
  jq -r '
    .metadata.annotations["nginx.ingress.kubernetes.io/ssl-redirect"] //
    .metadata.annotations["nginx.ingress.kubernetes.io/force-ssl-redirect"] //
    "false"
  ' <<<"$ingress_json"
)"
backend_protocol="$(jq -r '.metadata.annotations["nginx.ingress.kubernetes.io/backend-protocol"] // "HTTP"' <<<"$ingress_json" | tr '[:lower:]' '[:upper:]')"

if [[ "$backend_protocol" == "HTTPS" && "$backend_tls" == "none" ]]; then
  printf 'warning: %s/%s uses nginx backend-protocol=HTTPS; generated route will need BackendTLSPolicy or backend HTTP support.\n' "$namespace" "$ingress" >&2
fi

tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT

{
  if [[ "$create_gateway" == "true" ]]; then
    cat <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: $gateway
  namespace: $gateway_namespace
  labels:
    app.kubernetes.io/managed-by: ingress-to-envoy-gateway
    app.kubernetes.io/source-ingress: $ingress
spec:
  gatewayClassName: $gateway_class
  listeners:
    - name: http
      port: 80
      protocol: HTTP
      allowedRoutes:
        namespaces:
          from: Same
EOF

    if [[ "$has_tls" == "true" ]]; then
      cat <<'EOF'
    - name: https
      port: 443
      protocol: HTTPS
      tls:
        mode: Terminate
        certificateRefs:
EOF
      for secret in "${tls_secrets[@]}"; do
        printf '          - name: %s\n' "$secret"
      done
      cat <<'EOF'
      allowedRoutes:
        namespaces:
          from: Same
EOF
    fi
    printf '%s\n' '---'
  fi

  cat <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: $ingress
  namespace: $namespace
  labels:
    app.kubernetes.io/managed-by: ingress-to-envoy-gateway
    app.kubernetes.io/source-ingress: $ingress
spec:
  hostnames:
EOF
  for host in "${hosts[@]}"; do
    printf '    - %s\n' "$(yaml_quote "$host")"
  done
  cat <<EOF
  parentRefs:
    - name: $gateway
      namespace: $gateway_namespace
      sectionName: $([[ "$has_tls" == "true" ]] && printf 'https' || printf 'http')
  rules:
EOF

  route_count="$(jq '[.spec.rules[]?.http.paths[]?] | length' <<<"$ingress_json")"
  [[ "$route_count" != "0" ]] || die "Ingress has no HTTP paths"

  for i in $(seq 0 "$((route_count - 1))"); do
    item="$(jq -c "[.spec.rules[]?.http.paths[]?][$i]" <<<"$ingress_json")"
    path="$(jq -r '.path // "/"' <<<"$item")"
    path_type="$(jq -r '.pathType // "Prefix"' <<<"$item")"
    svc="$(jq -r '.backend.service.name // empty' <<<"$item")"
    port_number="$(jq -r '.backend.service.port.number // empty' <<<"$item")"
    port_name="$(jq -r '.backend.service.port.name // empty' <<<"$item")"

    [[ -n "$svc" ]] || die "path $i has no Service backend"
    if [[ -z "$port_number" && -n "$port_name" ]]; then
      port_number="$(resolve_service_port "$svc" "$port_name")"
      [[ -n "$port_number" ]] || die "could not resolve named port $svc/$port_name"
    fi
    [[ -n "$port_number" ]] || die "path $i has no Service port"

    case "$path_type" in
      Prefix) gateway_path_type="PathPrefix" ;;
      Exact) gateway_path_type="Exact" ;;
      ImplementationSpecific) gateway_path_type="PathPrefix" ;;
      *) gateway_path_type="PathPrefix" ;;
    esac

    cat <<EOF
    - matches:
        - path:
            type: $gateway_path_type
            value: $(yaml_quote "$path")
      backendRefs:
        - name: $svc
          port: $port_number
EOF
  done

  if [[ "$has_tls" == "true" ]] && { [[ "$ssl_redirect" == "true" ]] || [[ "$force_tls" == "true" ]]; }; then
    cat <<EOF
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: $(resource_name "${ingress}-https-redirect")
  namespace: $namespace
  labels:
    app.kubernetes.io/managed-by: ingress-to-envoy-gateway
    app.kubernetes.io/source-ingress: $ingress
spec:
  hostnames:
EOF
    for host in "${hosts[@]}"; do
      printf '    - %s\n' "$(yaml_quote "$host")"
    done
    cat <<EOF
  parentRefs:
    - name: $gateway
      namespace: $gateway_namespace
      sectionName: http
  rules:
    - filters:
        - type: RequestRedirect
          requestRedirect:
            scheme: https
            statusCode: 301
      matches:
        - path:
            type: PathPrefix
            value: /
EOF
  fi

  if [[ "$backend_protocol" == "HTTPS" && "$backend_tls" != "none" ]]; then
    mapfile -t https_services < <(
      jq -r '.spec.rules[]?.http.paths[]?.backend.service.name // empty' <<<"$ingress_json" |
        awk 'NF' | sort -u
    )
    for svc in "${https_services[@]}"; do
      cat <<EOF
---
apiVersion: gateway.networking.k8s.io/v1
kind: BackendTLSPolicy
metadata:
  name: $(resource_name "${ingress}-${svc}-backend-tls")
  namespace: $namespace
  labels:
    app.kubernetes.io/managed-by: ingress-to-envoy-gateway
    app.kubernetes.io/source-ingress: $ingress
spec:
  targetRefs:
    - group: ""
      kind: Service
      name: $svc
  validation:
    hostname: $(yaml_quote "$backend_tls_hostname")
EOF
      if [[ "$backend_tls" == "system" ]]; then
        cat <<'EOF'
    wellKnownCACertificates: System
EOF
      else
        cat <<EOF
    caCertificateRefs:
      - group: ""
        kind: ConfigMap
        name: $backend_tls_ca_configmap
EOF
      fi
    done
  fi

  if [[ "$emit_certificate" == "true" && "$has_tls" == "true" ]]; then
    for secret in "${tls_secrets[@]}"; do
      mapfile -t cert_hosts < <(
        jq -r --arg secret "$secret" '
          .spec.tls[]? | select(.secretName == $secret) | .hosts[]?
        ' <<<"$ingress_json" | awk 'NF' | sort -u
      )
      [[ "${#cert_hosts[@]}" -gt 0 ]] || cert_hosts=("${hosts[@]}")
      cat <<EOF
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: $secret
  namespace: $([[ "$create_gateway" == "false" ]] && printf '%s' "$gateway_namespace" || printf '%s' "$namespace")
  labels:
    app.kubernetes.io/managed-by: ingress-to-envoy-gateway
    app.kubernetes.io/source-ingress: $ingress
spec:
  secretName: $secret
  dnsNames:
EOF
      for host in "${cert_hosts[@]}"; do
        printf '    - %s\n' "$(yaml_quote "$host")"
      done
      cat <<EOF
  issuerRef:
    group: cert-manager.io
    kind: ClusterIssuer
    name: $issuer
EOF
    done
  fi
} >"$tmp"

if [[ "$apply" == "true" ]]; then
  "$kubectl_cmd" apply -f "$tmp"
  if [[ "$create_gateway" == "false" && "$has_tls" == "true" ]]; then
    for secret in "${tls_secrets[@]}"; do
      patch_gateway_cert_ref "$secret"
    done
  fi
else
  cat "$tmp"
fi