Difference between revisions of "Envoy from nginx for ingress"
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