diff --git a/deploy/kubernetes/README.md b/deploy/kubernetes/README.md new file mode 100644 index 0000000..5030bba --- /dev/null +++ b/deploy/kubernetes/README.md @@ -0,0 +1,91 @@ +# Kubernetes Static Manifests + +⚠️ **IMPORTANT**: These files are **auto-generated** from Helm templates. Do not edit them directly! + +## About These Files + +This directory contains pre-rendered Kubernetes manifests for deploying EasyHAProxy without Helm. These are generated from the Helm chart at `../../helm/easyhaproxy/` and provide three deployment options: + +| File | Type | Use Case | +|-----------------------------|------------------------|--------------------------------------------------------------| +| `easyhaproxy-daemonset.yml` | DaemonSet + hostPort | Direct host networking, best for bare-metal or simple setups | +| `easyhaproxy-nodeport.yml` | Deployment + NodePort | Exposes via NodePort (31080/31443/31936) | +| `easyhaproxy-clusterip.yml` | Deployment + ClusterIP | Internal cluster access only, use with external LoadBalancer | + +## How to Use + +Choose the manifest that fits your deployment scenario: + +```bash +# Option 1: DaemonSet mode (hostPort) +kubectl apply -f easyhaproxy-daemonset.yml + +# Option 2: NodePort mode +kubectl apply -f easyhaproxy-nodeport.yml + +# Option 3: ClusterIP mode +kubectl apply -f easyhaproxy-clusterip.yml +``` + +For more details, see the [Kubernetes documentation](../../docs/kubernetes.md). + +## Regenerating These Files + +**When to regenerate:** +- After modifying Helm chart templates (`helm/easyhaproxy/templates/`) +- After updating default values (`helm/easyhaproxy/values.yaml`) +- After a new release to sync with latest Helm chart + +**How to regenerate:** + +```bash +# Navigate to helm directory +cd helm + +# Generate DaemonSet manifest (hostPort mode) +helm template ingress ./easyhaproxy --namespace easyhaproxy \ + --set service.create=false \ + > ../deploy/kubernetes/easyhaproxy-daemonset.yml + +# Generate NodePort manifest +helm template ingress ./easyhaproxy --namespace easyhaproxy \ + --set service.create=true \ + --set service.type=NodePort \ + > ../deploy/kubernetes/easyhaproxy-nodeport.yml + +# Generate ClusterIP manifest +helm template ingress ./easyhaproxy --namespace easyhaproxy \ + --set service.create=true \ + --set service.type=ClusterIP \ + > ../deploy/kubernetes/easyhaproxy-clusterip.yml +``` + +**Verify regeneration:** + +```bash +# Check IngressClass is present +grep "kind: IngressClass" ../deploy/kubernetes/easyhaproxy-*.yml + +# Validate manifest syntax +kubectl apply --dry-run=client -f ../deploy/kubernetes/easyhaproxy-daemonset.yml +``` + +## What's Included + +Each manifest contains: +- **ServiceAccount**: RBAC identity for EasyHAProxy +- **ClusterRole**: Permissions to read Ingress resources and Secrets +- **ClusterRoleBinding**: Binds the role to the service account +- **IngressClass**: Defines `easyhaproxy` as the ingress class +- **DaemonSet/Deployment**: The EasyHAProxy workload +- **Service** (NodePort/ClusterIP only): Network exposure + +## Source of Truth + +The Helm chart at `../../helm/easyhaproxy/` is the **source of truth**. All changes should be made there, then these static manifests regenerated. + +**To modify these deployments:** +1. Edit Helm templates in `helm/easyhaproxy/templates/` +2. Update default values in `helm/easyhaproxy/values.yaml` +3. Regenerate static manifests using commands above +4. Commit both Helm changes and regenerated manifests \ No newline at end of file diff --git a/deploy/kubernetes/easyhaproxy-clusterip.yml b/deploy/kubernetes/easyhaproxy-clusterip.yml index 18bafaf..df23850 100644 --- a/deploy/kubernetes/easyhaproxy-clusterip.yml +++ b/deploy/kubernetes/easyhaproxy-clusterip.yml @@ -6,7 +6,7 @@ metadata: name: ingress-easyhaproxy namespace: easyhaproxy labels: - helm.sh/chart: easyhaproxy-1.0.1 + helm.sh/chart: easyhaproxy-1.0.0 app.kubernetes.io/name: easyhaproxy app.kubernetes.io/instance: ingress app.kubernetes.io/version: "5.0.0" @@ -19,7 +19,7 @@ metadata: name: ingress-easyhaproxy namespace: easyhaproxy labels: - helm.sh/chart: easyhaproxy-1.0.1 + helm.sh/chart: easyhaproxy-1.0.0 app.kubernetes.io/name: easyhaproxy app.kubernetes.io/instance: ingress app.kubernetes.io/version: "5.0.0" @@ -30,7 +30,7 @@ rules: resources: # - configmaps # - endpoints - # - nodes + - nodes - pods - services - namespaces @@ -41,23 +41,21 @@ rules: - list - watch - apiGroups: - - "extensions" - "networking.k8s.io" resources: - ingresses - # - ingresses/status - # - ingressclasses + - ingresses/status + - ingressclasses verbs: - get - list - watch -# - apiGroups: -# - "extensions" -# - "networking.k8s.io" -# resources: -# - ingresses/status -# verbs: -# - update +- apiGroups: + - "networking.k8s.io" + resources: + - ingresses/status + verbs: + - patch - apiGroups: - "" resources: @@ -85,7 +83,7 @@ metadata: name: ingress-easyhaproxy namespace: easyhaproxy labels: - helm.sh/chart: easyhaproxy-1.0.1 + helm.sh/chart: easyhaproxy-1.0.0 app.kubernetes.io/name: easyhaproxy app.kubernetes.io/instance: ingress app.kubernetes.io/version: "5.0.0" @@ -107,7 +105,7 @@ metadata: name: ingress-easyhaproxy namespace: easyhaproxy labels: - helm.sh/chart: easyhaproxy-1.0.1 + helm.sh/chart: easyhaproxy-1.0.0 app.kubernetes.io/name: easyhaproxy app.kubernetes.io/instance: ingress app.kubernetes.io/version: "5.0.0" @@ -139,12 +137,13 @@ metadata: name: ingress-easyhaproxy namespace: easyhaproxy labels: - helm.sh/chart: easyhaproxy-1.0.1 + helm.sh/chart: easyhaproxy-1.0.0 app.kubernetes.io/name: easyhaproxy app.kubernetes.io/instance: ingress app.kubernetes.io/version: "5.0.0" app.kubernetes.io/managed-by: Helm spec: + replicas: 1 selector: matchLabels: app.kubernetes.io/name: easyhaproxy @@ -155,15 +154,6 @@ spec: app.kubernetes.io/name: easyhaproxy app.kubernetes.io/instance: ingress spec: - affinity: - nodeAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - nodeSelectorTerms: - - matchExpressions: - - key: easyhaproxy/node - operator: In - values: - - master serviceAccountName: ingress-easyhaproxy securityContext: {} @@ -184,9 +174,7 @@ spec: containerPort: 1936 resources: - requests: - cpu: 100m - memory: 128Mi + {} env: - name: EASYHAPROXY_DISCOVER value: kubernetes @@ -206,3 +194,27 @@ spec: value: DEBUG - name: CERTBOT_LOG_LEVEL value: DEBUG + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: EASYHAPROXY_UPDATE_INGRESS_STATUS + value: "true" + - name: EASYHAPROXY_DEPLOYMENT_MODE + value: "auto" + - name: EASYHAPROXY_STATUS_UPDATE_INTERVAL + value: "30" +--- +# Source: easyhaproxy/templates/ingressclass.yaml +apiVersion: networking.k8s.io/v1 +kind: IngressClass +metadata: + name: easyhaproxy + labels: + helm.sh/chart: easyhaproxy-1.0.0 + app.kubernetes.io/name: easyhaproxy + app.kubernetes.io/instance: ingress + app.kubernetes.io/version: "5.0.0" + app.kubernetes.io/managed-by: Helm +spec: + controller: byjg.com/easyhaproxy diff --git a/deploy/kubernetes/easyhaproxy-daemonset.yml b/deploy/kubernetes/easyhaproxy-daemonset.yml index 441aaac..f1616e4 100644 --- a/deploy/kubernetes/easyhaproxy-daemonset.yml +++ b/deploy/kubernetes/easyhaproxy-daemonset.yml @@ -6,7 +6,7 @@ metadata: name: ingress-easyhaproxy namespace: easyhaproxy labels: - helm.sh/chart: easyhaproxy-1.0.1 + helm.sh/chart: easyhaproxy-1.0.0 app.kubernetes.io/name: easyhaproxy app.kubernetes.io/instance: ingress app.kubernetes.io/version: "5.0.0" @@ -19,7 +19,7 @@ metadata: name: ingress-easyhaproxy namespace: easyhaproxy labels: - helm.sh/chart: easyhaproxy-1.0.1 + helm.sh/chart: easyhaproxy-1.0.0 app.kubernetes.io/name: easyhaproxy app.kubernetes.io/instance: ingress app.kubernetes.io/version: "5.0.0" @@ -30,7 +30,7 @@ rules: resources: # - configmaps # - endpoints - # - nodes + - nodes - pods - services - namespaces @@ -41,23 +41,21 @@ rules: - list - watch - apiGroups: - - "extensions" - "networking.k8s.io" resources: - ingresses - # - ingresses/status - # - ingressclasses + - ingresses/status + - ingressclasses verbs: - get - list - watch -# - apiGroups: -# - "extensions" -# - "networking.k8s.io" -# resources: -# - ingresses/status -# verbs: -# - update +- apiGroups: + - "networking.k8s.io" + resources: + - ingresses/status + verbs: + - patch - apiGroups: - "" resources: @@ -85,7 +83,7 @@ metadata: name: ingress-easyhaproxy namespace: easyhaproxy labels: - helm.sh/chart: easyhaproxy-1.0.1 + helm.sh/chart: easyhaproxy-1.0.0 app.kubernetes.io/name: easyhaproxy app.kubernetes.io/instance: ingress app.kubernetes.io/version: "5.0.0" @@ -106,7 +104,7 @@ metadata: name: ingress-easyhaproxy namespace: easyhaproxy labels: - helm.sh/chart: easyhaproxy-1.0.1 + helm.sh/chart: easyhaproxy-1.0.0 app.kubernetes.io/name: easyhaproxy app.kubernetes.io/instance: ingress app.kubernetes.io/version: "5.0.0" @@ -151,9 +149,7 @@ spec: containerPort: 1936 hostPort: 1936 resources: - requests: - cpu: 100m - memory: 128Mi + {} env: - name: EASYHAPROXY_DISCOVER value: kubernetes @@ -173,3 +169,27 @@ spec: value: DEBUG - name: CERTBOT_LOG_LEVEL value: DEBUG + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: EASYHAPROXY_UPDATE_INGRESS_STATUS + value: "true" + - name: EASYHAPROXY_DEPLOYMENT_MODE + value: "auto" + - name: EASYHAPROXY_STATUS_UPDATE_INTERVAL + value: "30" +--- +# Source: easyhaproxy/templates/ingressclass.yaml +apiVersion: networking.k8s.io/v1 +kind: IngressClass +metadata: + name: easyhaproxy + labels: + helm.sh/chart: easyhaproxy-1.0.0 + app.kubernetes.io/name: easyhaproxy + app.kubernetes.io/instance: ingress + app.kubernetes.io/version: "5.0.0" + app.kubernetes.io/managed-by: Helm +spec: + controller: byjg.com/easyhaproxy diff --git a/deploy/kubernetes/easyhaproxy-nodeport.yml b/deploy/kubernetes/easyhaproxy-nodeport.yml index 50fc4c3..06c3430 100644 --- a/deploy/kubernetes/easyhaproxy-nodeport.yml +++ b/deploy/kubernetes/easyhaproxy-nodeport.yml @@ -6,7 +6,7 @@ metadata: name: ingress-easyhaproxy namespace: easyhaproxy labels: - helm.sh/chart: easyhaproxy-1.0.1 + helm.sh/chart: easyhaproxy-1.0.0 app.kubernetes.io/name: easyhaproxy app.kubernetes.io/instance: ingress app.kubernetes.io/version: "5.0.0" @@ -19,7 +19,7 @@ metadata: name: ingress-easyhaproxy namespace: easyhaproxy labels: - helm.sh/chart: easyhaproxy-1.0.1 + helm.sh/chart: easyhaproxy-1.0.0 app.kubernetes.io/name: easyhaproxy app.kubernetes.io/instance: ingress app.kubernetes.io/version: "5.0.0" @@ -30,7 +30,7 @@ rules: resources: # - configmaps # - endpoints - # - nodes + - nodes - pods - services - namespaces @@ -41,23 +41,21 @@ rules: - list - watch - apiGroups: - - "extensions" - "networking.k8s.io" resources: - ingresses - # - ingresses/status - # - ingressclasses + - ingresses/status + - ingressclasses verbs: - get - list - watch -# - apiGroups: -# - "extensions" -# - "networking.k8s.io" -# resources: -# - ingresses/status -# verbs: -# - update +- apiGroups: + - "networking.k8s.io" + resources: + - ingresses/status + verbs: + - patch - apiGroups: - "" resources: @@ -85,7 +83,7 @@ metadata: name: ingress-easyhaproxy namespace: easyhaproxy labels: - helm.sh/chart: easyhaproxy-1.0.1 + helm.sh/chart: easyhaproxy-1.0.0 app.kubernetes.io/name: easyhaproxy app.kubernetes.io/instance: ingress app.kubernetes.io/version: "5.0.0" @@ -107,7 +105,7 @@ metadata: name: ingress-easyhaproxy namespace: easyhaproxy labels: - helm.sh/chart: easyhaproxy-1.0.1 + helm.sh/chart: easyhaproxy-1.0.0 app.kubernetes.io/name: easyhaproxy app.kubernetes.io/instance: ingress app.kubernetes.io/version: "5.0.0" @@ -119,13 +117,13 @@ spec: ports: - name: http port: 80 - nodePort: 31080 + nodePort: 80 - name: https port: 443 - nodePort: 31443 + nodePort: 443 - name: stats port: 1936 - nodePort: 31936 + nodePort: 1936 selector: @@ -139,12 +137,13 @@ metadata: name: ingress-easyhaproxy namespace: easyhaproxy labels: - helm.sh/chart: easyhaproxy-1.0.1 + helm.sh/chart: easyhaproxy-1.0.0 app.kubernetes.io/name: easyhaproxy app.kubernetes.io/instance: ingress app.kubernetes.io/version: "5.0.0" app.kubernetes.io/managed-by: Helm spec: + replicas: 1 selector: matchLabels: app.kubernetes.io/name: easyhaproxy @@ -155,15 +154,6 @@ spec: app.kubernetes.io/name: easyhaproxy app.kubernetes.io/instance: ingress spec: - affinity: - nodeAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - nodeSelectorTerms: - - matchExpressions: - - key: easyhaproxy/node - operator: In - values: - - master serviceAccountName: ingress-easyhaproxy securityContext: {} @@ -184,9 +174,7 @@ spec: containerPort: 1936 resources: - requests: - cpu: 100m - memory: 128Mi + {} env: - name: EASYHAPROXY_DISCOVER value: kubernetes @@ -206,3 +194,27 @@ spec: value: DEBUG - name: CERTBOT_LOG_LEVEL value: DEBUG + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: EASYHAPROXY_UPDATE_INGRESS_STATUS + value: "true" + - name: EASYHAPROXY_DEPLOYMENT_MODE + value: "auto" + - name: EASYHAPROXY_STATUS_UPDATE_INTERVAL + value: "30" +--- +# Source: easyhaproxy/templates/ingressclass.yaml +apiVersion: networking.k8s.io/v1 +kind: IngressClass +metadata: + name: easyhaproxy + labels: + helm.sh/chart: easyhaproxy-1.0.0 + app.kubernetes.io/name: easyhaproxy + app.kubernetes.io/instance: ingress + app.kubernetes.io/version: "5.0.0" + app.kubernetes.io/managed-by: Helm +spec: + controller: byjg.com/easyhaproxy diff --git a/docs/kubernetes.md b/docs/kubernetes.md index cd54dff..fe8afc1 100644 --- a/docs/kubernetes.md +++ b/docs/kubernetes.md @@ -7,9 +7,10 @@ sidebar_position: 1 ## Setup Kubernetes EasyHAProxy :::info How it works -EasyHAProxy for Kubernetes operates by querying all ingress definitions with the annotation -`kubernetes.io/ingress.class: easyhaproxy-ingress`. Upon finding this annotation, -EasyHAProxy immediately sets up HAProxy and begins serving traffic. +EasyHAProxy for Kubernetes operates by querying all ingress definitions with either the +`spec.ingressClassName: easyhaproxy` field (recommended) or the deprecated annotation +`kubernetes.io/ingress.class: easyhaproxy-ingress` (for backward compatibility). Upon finding +a matching ingress class, EasyHAProxy immediately sets up HAProxy and begins serving traffic. ::: For Kubernetes installations, there are three available installation modes: @@ -54,18 +55,18 @@ If necessary, you can configure environment variables. To get a list of the vari ## Running containers -Your container only requires creating an ingress with the annotation `kubernetes.io/ingress.class: easyhaproxy-ingress` pointing to your service. +Your container only requires creating an ingress with the `spec.ingressClassName: easyhaproxy` field pointing to your service. e.g. ```yaml kind: Ingress metadata: - annotations: - kubernetes.io/ingress.class: easyhaproxy-ingress name: example-ingress namespace: example spec: + # Use ingressClassName (recommended) + ingressClassName: easyhaproxy rules: - host: example.org http: @@ -78,6 +79,10 @@ spec: pathType: ImplementationSpecific ``` +:::note Backward Compatibility +The deprecated annotation `kubernetes.io/ingress.class: easyhaproxy-ingress` is still supported for backward compatibility, but `spec.ingressClassName` is the recommended approach for new deployments. +::: + Once the container is running, EasyHAProxy will detect automatically and start to redirect all traffic from `example.org:80` to your container at port 8080. You don't need to expose any port in your container. @@ -92,7 +97,7 @@ You don't need to expose any port in your container. | annotation | Description | Default | Example | |-------------------------------------|-------------------------------------------------------------------------------------|--------------|----------------------------| -| kubernetes.io/ingress.class | (required) Activate EasyHAProxy. | **required** | easyhaproxy-ingress | +| kubernetes.io/ingress.class | (deprecated) Activate EasyHAProxy. Use `spec.ingressClassName` instead. | *optional* | easyhaproxy-ingress | | easyhaproxy.redirect_ssl | (optional) Boolean. Force redirect all endpoints to HTTPS. | false | true or false | | easyhaproxy.certbot | (optional) Boolean. It will request certbot certificates for the ingresses domains. | false | true or false | | easyhaproxy.redirect | (optional) JSON. Key pair with a domain and its destination. | *empty* | \{"domain":"redirect_url"} | @@ -116,11 +121,11 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: - kubernetes.io/ingress.class: easyhaproxy-ingress easyhaproxy.plugins: "cloudflare,deny_pages" name: example-ingress namespace: example spec: + ingressClassName: easyhaproxy rules: - host: example.org http: @@ -142,13 +147,13 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: - kubernetes.io/ingress.class: easyhaproxy-ingress easyhaproxy.plugins: "deny_pages" easyhaproxy.plugin.deny_pages.paths: "/admin,/private,/config" easyhaproxy.plugin.deny_pages.status_code: "403" name: secure-app-ingress namespace: production spec: + ingressClassName: easyhaproxy rules: - host: myapp.example.com http: @@ -168,12 +173,13 @@ spec: ```yaml metadata: annotations: - kubernetes.io/ingress.class: easyhaproxy-ingress easyhaproxy.plugins: "jwt_validator" easyhaproxy.plugin.jwt_validator.algorithm: "RS256" easyhaproxy.plugin.jwt_validator.issuer: "https://auth.example.com/" easyhaproxy.plugin.jwt_validator.audience: "https://api.example.com" easyhaproxy.plugin.jwt_validator.pubkey_path: "/etc/haproxy/jwt_keys/api_pubkey.pem" +spec: + ingressClassName: easyhaproxy ``` **Note:** For JWT validation, you'll need to mount the public key file into the EasyHAProxy pod. See [Using Plugins](plugins.md#protect-api-with-jwt-authentication) for details. @@ -183,10 +189,11 @@ metadata: ```yaml metadata: annotations: - kubernetes.io/ingress.class: easyhaproxy-ingress easyhaproxy.plugins: "ip_whitelist" easyhaproxy.plugin.ip_whitelist.allowed_ips: "192.168.1.0/24,10.0.0.5" easyhaproxy.plugin.ip_whitelist.status_code: "403" +spec: + ingressClassName: easyhaproxy ``` **Restore Cloudflare visitor IPs:** @@ -194,8 +201,9 @@ metadata: ```yaml metadata: annotations: - kubernetes.io/ingress.class: easyhaproxy-ingress easyhaproxy.plugins: "cloudflare" +spec: + ingressClassName: easyhaproxy ``` **Multiple plugins together:** @@ -203,10 +211,11 @@ metadata: ```yaml metadata: annotations: - kubernetes.io/ingress.class: easyhaproxy-ingress easyhaproxy.plugins: "cloudflare,deny_pages" easyhaproxy.plugin.deny_pages.paths: "/wp-admin,/wp-login.php" easyhaproxy.plugin.deny_pages.status_code: "404" +spec: + ingressClassName: easyhaproxy ``` ### Global Plugin Configuration @@ -238,17 +247,17 @@ For more information on plugin types and available plugins, see the [Using Plugi ## Certbot / ACME / Letsencrypt -It is necessary add the annotation `easyhaproxy.certbot` to the ingress configuration: +It is necessary to add the annotation `easyhaproxy.certbot` to the ingress configuration: ```yaml kind: Ingress metadata: annotations: - kubernetes.io/ingress.class: easyhaproxy-ingress easyhaproxy.certbot: 'true' name: example-ingress namespace: example spec: + ingressClassName: easyhaproxy .... ``` @@ -276,11 +285,10 @@ type: kubernetes.io/tls apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - annotations: - kubernetes.io/ingress.class: easyhaproxy-ingress name: tls-example namespace: default spec: + ingressClassName: easyhaproxy tls: - hosts: - host2.local diff --git a/examples/kubernetes/cloudflare.yml b/examples/kubernetes/cloudflare.yml index 43e5ac7..106a8ad 100644 --- a/examples/kubernetes/cloudflare.yml +++ b/examples/kubernetes/cloudflare.yml @@ -112,8 +112,6 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: - kubernetes.io/ingress.class: easyhaproxy-ingress - # Enable Cloudflare plugin easyhaproxy.plugins: "cloudflare" @@ -122,6 +120,9 @@ metadata: name: webapp-ingress-cloudflare namespace: default spec: + # Use ingressClassName instead of the deprecated annotation + # For backward compatibility, annotation kubernetes.io/ingress.class is still supported + ingressClassName: easyhaproxy rules: - host: myapp.example.local http: diff --git a/examples/kubernetes/ip-whitelist.yml b/examples/kubernetes/ip-whitelist.yml index 8f6e1f3..a92334b 100644 --- a/examples/kubernetes/ip-whitelist.yml +++ b/examples/kubernetes/ip-whitelist.yml @@ -95,8 +95,6 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: - kubernetes.io/ingress.class: easyhaproxy-ingress - # Enable IP whitelist plugin easyhaproxy.plugins: "ip_whitelist" @@ -109,6 +107,9 @@ metadata: name: admin-ingress-whitelist namespace: default spec: + # Use ingressClassName instead of the deprecated annotation + # For backward compatibility, annotation kubernetes.io/ingress.class is still supported + ingressClassName: easyhaproxy rules: - host: admin.example.local http: diff --git a/examples/kubernetes/jwt-validator.yml b/examples/kubernetes/jwt-validator.yml index 2d5ee0e..f49f0a7 100644 --- a/examples/kubernetes/jwt-validator.yml +++ b/examples/kubernetes/jwt-validator.yml @@ -116,8 +116,6 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: - kubernetes.io/ingress.class: easyhaproxy-ingress - # Enable JWT validator plugin easyhaproxy.plugins: "jwt_validator" @@ -129,6 +127,9 @@ metadata: name: api-ingress-jwt namespace: default spec: + # Use ingressClassName instead of the deprecated annotation + # For backward compatibility, annotation kubernetes.io/ingress.class is still supported + ingressClassName: easyhaproxy rules: - host: api.example.local http: diff --git a/examples/kubernetes/plugins-combined.yml b/examples/kubernetes/plugins-combined.yml index 274922f..fc9ea74 100644 --- a/examples/kubernetes/plugins-combined.yml +++ b/examples/kubernetes/plugins-combined.yml @@ -113,7 +113,6 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: - kubernetes.io/ingress.class: easyhaproxy-ingress # Cloudflare IP restoration + deny pages easyhaproxy.plugins: "cloudflare,deny_pages" easyhaproxy.plugin.deny_pages.paths: "/admin,/wp-admin,/wp-login.php,/.env,/config" @@ -121,6 +120,9 @@ metadata: name: website-ingress namespace: default spec: + # Use ingressClassName instead of the deprecated annotation + # For backward compatibility, annotation kubernetes.io/ingress.class is still supported + ingressClassName: easyhaproxy rules: - host: website.example.local http: @@ -179,7 +181,6 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: - kubernetes.io/ingress.class: easyhaproxy-ingress # JWT validation + block internal endpoints easyhaproxy.plugins: "jwt_validator,deny_pages" # JWT config @@ -193,6 +194,9 @@ metadata: name: api-ingress namespace: default spec: + # Use ingressClassName instead of the deprecated annotation + # For backward compatibility, annotation kubernetes.io/ingress.class is still supported + ingressClassName: easyhaproxy rules: - host: api.example.local http: @@ -251,7 +255,6 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: - kubernetes.io/ingress.class: easyhaproxy-ingress # IP whitelist only (strictest security) easyhaproxy.plugins: "ip_whitelist" # UPDATE with your office/VPN IPs! @@ -260,6 +263,9 @@ metadata: name: admin-ingress namespace: default spec: + # Use ingressClassName instead of the deprecated annotation + # For backward compatibility, annotation kubernetes.io/ingress.class is still supported + ingressClassName: easyhaproxy rules: - host: admin.example.local http: diff --git a/examples/kubernetes/service.yml b/examples/kubernetes/service.yml index b328846..9770541 100644 --- a/examples/kubernetes/service.yml +++ b/examples/kubernetes/service.yml @@ -56,11 +56,12 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - annotations: - kubernetes.io/ingress.class: easyhaproxy-ingress name: container-example namespace: default spec: + # Use ingressClassName instead of the deprecated annotation + # For backward compatibility, annotation kubernetes.io/ingress.class is still supported + ingressClassName: easyhaproxy rules: - host: example.org http: diff --git a/examples/kubernetes/service_tls.yml b/examples/kubernetes/service_tls.yml index 076f9a9..11398cd 100644 --- a/examples/kubernetes/service_tls.yml +++ b/examples/kubernetes/service_tls.yml @@ -57,11 +57,12 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - annotations: - kubernetes.io/ingress.class: easyhaproxy-ingress name: tls-example namespace: default spec: + # Use ingressClassName instead of the deprecated annotation + # For backward compatibility, annotation kubernetes.io/ingress.class is still supported + ingressClassName: easyhaproxy tls: - hosts: - host2.local diff --git a/helm/easyhaproxy/templates/clusterrole.yaml b/helm/easyhaproxy/templates/clusterrole.yaml index 04ac23e..6968b91 100644 --- a/helm/easyhaproxy/templates/clusterrole.yaml +++ b/helm/easyhaproxy/templates/clusterrole.yaml @@ -17,7 +17,7 @@ rules: resources: # - configmaps # - endpoints - # - nodes + - nodes - pods - services - namespaces @@ -28,23 +28,21 @@ rules: - list - watch - apiGroups: - - "extensions" - "networking.k8s.io" resources: - ingresses - # - ingresses/status - # - ingressclasses + - ingresses/status + - ingressclasses verbs: - get - list - watch -# - apiGroups: -# - "extensions" -# - "networking.k8s.io" -# resources: -# - ingresses/status -# verbs: -# - update +- apiGroups: + - "networking.k8s.io" + resources: + - ingresses/status + verbs: + - patch - apiGroups: - "" resources: diff --git a/helm/easyhaproxy/templates/deployment.yaml b/helm/easyhaproxy/templates/deployment.yaml index 75e14e5..bdbfadf 100644 --- a/helm/easyhaproxy/templates/deployment.yaml +++ b/helm/easyhaproxy/templates/deployment.yaml @@ -77,4 +77,18 @@ spec: {{- if .Values.easyhaproxy.certbot.email }} - name: EASYHAPROXY_CERTBOT_EMAIL value: {{ .Values.easyhaproxy.certbot.email }} - {{ end }} + {{- end }} + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: EASYHAPROXY_UPDATE_INGRESS_STATUS + value: {{ .Values.ingressStatus.enabled | quote }} + - name: EASYHAPROXY_DEPLOYMENT_MODE + value: {{ .Values.ingressStatus.deploymentMode | quote }} + {{- if .Values.ingressStatus.externalHostname }} + - name: EASYHAPROXY_EXTERNAL_HOSTNAME + value: {{ .Values.ingressStatus.externalHostname | quote }} + {{- end }} + - name: EASYHAPROXY_STATUS_UPDATE_INTERVAL + value: {{ .Values.ingressStatus.updateInterval | quote }} diff --git a/helm/easyhaproxy/templates/ingressclass.yaml b/helm/easyhaproxy/templates/ingressclass.yaml new file mode 100644 index 0000000..af9fa11 --- /dev/null +++ b/helm/easyhaproxy/templates/ingressclass.yaml @@ -0,0 +1,14 @@ +{{- if .Values.ingressClass.create -}} +apiVersion: networking.k8s.io/v1 +kind: IngressClass +metadata: + name: easyhaproxy + labels: + {{- include "easyhaproxy.labels" . | nindent 4 }} + {{- with .Values.ingressClass.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + controller: byjg.com/easyhaproxy +{{- end }} diff --git a/helm/easyhaproxy/values.yaml b/helm/easyhaproxy/values.yaml index 835151d..bf4906b 100644 --- a/helm/easyhaproxy/values.yaml +++ b/helm/easyhaproxy/values.yaml @@ -31,6 +31,24 @@ serviceAccount: annotations: {} name: "" +# IngressClass configuration +ingressClass: + # Create IngressClass resource + create: true + # Additional annotations for the IngressClass + annotations: {} + +# Ingress status update configuration +ingressStatus: + # Enable updating ingress status with load balancer IPs + enabled: true + # Deployment mode: auto (detect), daemonset, nodeport, or clusterip + deploymentMode: auto + # External hostname override (for ClusterIP mode without LoadBalancer) + externalHostname: "" + # How often to update status (seconds) + updateInterval: 30 + podAnnotations: {} podSecurityContext: {} diff --git a/src/functions/__init__.py b/src/functions/__init__.py index 979eb42..51f22d1 100644 --- a/src/functions/__init__.py +++ b/src/functions/__init__.py @@ -105,6 +105,12 @@ def read(): env_vars["plugins"]["config"].setdefault(plugin_name, {}) env_vars["plugins"]["config"][plugin_name][config_key] = value + # Ingress status update configuration + env_vars["update_ingress_status"] = os.getenv("EASYHAPROXY_UPDATE_INGRESS_STATUS", "true").lower() == "true" + env_vars["deployment_mode"] = os.getenv("EASYHAPROXY_DEPLOYMENT_MODE", "auto") + env_vars["external_hostname"] = os.getenv("EASYHAPROXY_EXTERNAL_HOSTNAME", "") + env_vars["ingress_status_update_interval"] = int(os.getenv("EASYHAPROXY_STATUS_UPDATE_INTERVAL", "30")) + return env_vars diff --git a/src/processor/__init__.py b/src/processor/__init__.py index 0695a46..3e363a3 100644 --- a/src/processor/__init__.py +++ b/src/processor/__init__.py @@ -226,8 +226,214 @@ def __init__(self, filename=None): self.api_instance = client.CoreV1Api() self.v1 = client.NetworkingV1Api() self.cert_cache = {} + self.deployment_mode_cache = None + self.ingress_addresses_cache = None + self.addresses_cache_time = 0 super().__init__() + def _detect_deployment_mode(self): + """ + Detect the deployment mode (daemonset, nodeport, clusterip). + Returns: tuple (mode: str, service: V1Service or None) + """ + import os + import time + + # Return cached if available + if self.deployment_mode_cache: + return self.deployment_mode_cache + + env_config = ContainerEnv.read() + + # Check for manual override + if env_config['deployment_mode'] != 'auto': + loggerEasyHaproxy.info(f"Using manual deployment mode: {env_config['deployment_mode']}") + service = self._get_easyhaproxy_service() if env_config['deployment_mode'] in ['nodeport', 'clusterip'] else None + self.deployment_mode_cache = (env_config['deployment_mode'], service) + return self.deployment_mode_cache + + try: + # Get current pod name from hostname + pod_name = socket.gethostname() + namespace = os.getenv('POD_NAMESPACE', 'easyhaproxy') + + # Read current pod + pod = self.api_instance.read_namespaced_pod(pod_name, namespace) + + # Check owner references to determine if DaemonSet or Deployment + if pod.metadata.owner_references: + owner_kind = pod.metadata.owner_references[0].kind + + if owner_kind == 'DaemonSet': + loggerEasyHaproxy.info("Detected deployment mode: daemonset") + self.deployment_mode_cache = ('daemonset', None) + return self.deployment_mode_cache + elif owner_kind in ['ReplicaSet', 'Deployment']: + # Check if Service exists + service = self._get_easyhaproxy_service() + if service: + if service.spec.type == 'NodePort': + loggerEasyHaproxy.info("Detected deployment mode: nodeport") + self.deployment_mode_cache = ('nodeport', service) + return self.deployment_mode_cache + else: + loggerEasyHaproxy.info("Detected deployment mode: clusterip") + self.deployment_mode_cache = ('clusterip', service) + return self.deployment_mode_cache + except Exception as e: + loggerEasyHaproxy.warn(f"Failed to detect deployment mode: {e}, defaulting to daemonset") + + self.deployment_mode_cache = ('daemonset', None) + return self.deployment_mode_cache + + def _get_easyhaproxy_service(self): + """Get the EasyHAProxy service if it exists.""" + import os + + try: + namespace = os.getenv('POD_NAMESPACE', 'easyhaproxy') + # Try common service names + service_names = ['easyhaproxy', 'ingress-easyhaproxy'] + + for service_name in service_names: + try: + service = self.api_instance.read_namespaced_service(service_name, namespace) + return service + except: + continue + return None + except Exception as e: + loggerEasyHaproxy.warn(f"Failed to get EasyHAProxy service: {e}") + return None + + def _get_ingress_addresses(self, mode, service): + """ + Get IP addresses or hostnames to report in ingress status. + + Args: + mode: Deployment mode (daemonset, nodeport, clusterip) + service: V1Service object (for nodeport/clusterip modes) + + Returns: + List of dicts: [{"ip": "..."}, {"hostname": "..."}] + """ + import os + import time + + env_config = ContainerEnv.read() + cache_ttl = env_config.get('ingress_status_update_interval', 30) + + # Return cached if still valid + if self.ingress_addresses_cache and (time.time() - self.addresses_cache_time) < cache_ttl: + return self.ingress_addresses_cache + + addresses = [] + + try: + if mode == 'daemonset': + # Get nodes where DaemonSet pods are running + namespace = os.getenv('POD_NAMESPACE', 'easyhaproxy') + label_selector = "app.kubernetes.io/name=easyhaproxy" + + pods = self.api_instance.list_namespaced_pod(namespace, label_selector=label_selector) + node_names = set(pod.spec.node_name for pod in pods.items if pod.spec.node_name) + + # Get external IPs from these nodes + for node_name in node_names: + node = self.api_instance.read_node(node_name) + for addr in node.status.addresses: + if addr.type == 'ExternalIP': + addresses.append({"ip": addr.address}) + break + else: + # Fallback to InternalIP if no ExternalIP + for addr in node.status.addresses: + if addr.type == 'InternalIP': + addresses.append({"ip": addr.address}) + break + + elif mode == 'nodeport': + # Get all node IPs (traffic can reach any node via NodePort) + nodes = self.api_instance.list_node() + for node in nodes.items: + for addr in node.status.addresses: + if addr.type == 'ExternalIP': + addresses.append({"ip": addr.address}) + break + else: + # Fallback to InternalIP + for addr in node.status.addresses: + if addr.type == 'InternalIP': + addresses.append({"ip": addr.address}) + break + + elif mode == 'clusterip': + # Check if LoadBalancer status is available + if service and service.status and service.status.load_balancer: + lb_ingress = service.status.load_balancer.ingress or [] + for ing in lb_ingress: + if ing.ip: + addresses.append({"ip": ing.ip}) + if ing.hostname: + addresses.append({"hostname": ing.hostname}) + + # If no LoadBalancer, check for external hostname override + if not addresses and env_config['external_hostname']: + addresses.append({"hostname": env_config['external_hostname']}) + + # Fallback to ClusterIP + if not addresses and service: + addresses.append({"ip": service.spec.cluster_ip}) + + except Exception as e: + loggerEasyHaproxy.warn(f"Failed to get ingress addresses: {e}") + + # Cache the result + self.ingress_addresses_cache = addresses + self.addresses_cache_time = time.time() + + return addresses + + def _update_ingress_status(self, ingress, addresses): + """ + Update the status of an ingress resource. + + Args: + ingress: V1Ingress object + addresses: List of address dicts [{"ip": "..."}, {"hostname": "..."}] + """ + if not addresses: + return + + try: + # Create status patch + status_body = { + "status": { + "loadBalancer": { + "ingress": addresses + } + } + } + + # Update status using patch (not replace) + self.v1.patch_namespaced_ingress_status( + name=ingress.metadata.name, + namespace=ingress.metadata.namespace, + body=status_body, + field_manager="easyhaproxy" + ) + + loggerEasyHaproxy.debug( + f"Updated ingress {ingress.metadata.namespace}/{ingress.metadata.name} " + f"status with {len(addresses)} address(es)" + ) + + except Exception as e: + loggerEasyHaproxy.warn( + f"Failed to update status for ingress " + f"{ingress.metadata.namespace}/{ingress.metadata.name}: {e}" + ) + def _check_annotation(self, annotations, key, default=None): if key not in annotations: return default @@ -237,11 +443,33 @@ def inspect_network(self): ret = self.v1.list_ingress_for_all_namespaces(watch=False) + # Detect deployment mode once per cycle for ingress status updates + env_config = ContainerEnv.read() + if env_config['update_ingress_status']: + deployment_mode, service = self._detect_deployment_mode() + ingress_addresses = self._get_ingress_addresses(deployment_mode, service) + else: + ingress_addresses = [] + self.parsed_object = {} for ingress in ret.items: - if 'kubernetes.io/ingress.class' not in ingress.metadata.annotations: - continue - if ingress.metadata.annotations['kubernetes.io/ingress.class'] != "easyhaproxy-ingress": + # Support both new spec.ingressClassName and deprecated annotation for backward compatibility + ingress_class = None + is_match = False + + # Check new spec.ingressClassName first (preferred) + if hasattr(ingress.spec, 'ingress_class_name') and ingress.spec.ingress_class_name is not None: + ingress_class = ingress.spec.ingress_class_name + # Modern spec uses 'easyhaproxy' + is_match = (ingress_class == "easyhaproxy") + # Fall back to deprecated annotation + elif ingress.metadata.annotations and 'kubernetes.io/ingress.class' in ingress.metadata.annotations: + ingress_class = ingress.metadata.annotations['kubernetes.io/ingress.class'] + # Deprecated annotation uses 'easyhaproxy-ingress' for backward compatibility + is_match = (ingress_class == "easyhaproxy-ingress") + + # Skip if no ingress class is defined or it doesn't match + if not is_match: continue ssl_hosts = [] @@ -326,3 +554,7 @@ def inspect_network(self): if cluster_ip not in self.parsed_object.keys(): self.parsed_object[cluster_ip] = data self.parsed_object[cluster_ip].update(rule_data) + + # Update ingress status if enabled + if env_config['update_ingress_status'] and ingress_addresses: + self._update_ingress_status(ingress, ingress_addresses) diff --git a/src/tests/test_containerenv.py b/src/tests/test_containerenv.py index f5837d7..8fecdd3 100644 --- a/src/tests/test_containerenv.py +++ b/src/tests/test_containerenv.py @@ -25,7 +25,11 @@ def test_container_env_empty(): "abort_on_error": False, "config": {}, "enabled": [] - } + }, + "update_ingress_status": True, + "deployment_mode": "auto", + "external_hostname": "", + "ingress_status_update_interval": 30 } == ContainerEnv.read() # os.environ['CERTBOT_LOG_LEVEL'] = 'warn' @@ -55,7 +59,11 @@ def test_container_env_customerrors(): "abort_on_error": False, "config": {}, "enabled": [] - } + }, + "update_ingress_status": True, + "deployment_mode": "auto", + "external_hostname": "", + "ingress_status_update_interval": 30 } == ContainerEnv.read() finally: del os.environ['HAPROXY_CUSTOMERRORS'] @@ -85,7 +93,11 @@ def test_container_env_sslmode(): "abort_on_error": False, "config": {}, "enabled": [] - } + }, + "update_ingress_status": True, + "deployment_mode": "auto", + "external_hostname": "", + "ingress_status_update_interval": 30 } == ContainerEnv.read() finally: del os.environ['EASYHAPROXY_SSL_MODE'] @@ -116,7 +128,11 @@ def test_container_env_stats(): "abort_on_error": False, "config": {}, "enabled": [] - } + }, + "update_ingress_status": True, + "deployment_mode": "auto", + "external_hostname": "", + "ingress_status_update_interval": 30 } == ContainerEnv.read() finally: del os.environ['HAPROXY_USERNAME'] @@ -153,7 +169,11 @@ def test_container_env_stats_password(): "abort_on_error": False, "config": {}, "enabled": [] - } + }, + "update_ingress_status": True, + "deployment_mode": "auto", + "external_hostname": "", + "ingress_status_update_interval": 30 } == ContainerEnv.read() finally: del os.environ['HAPROXY_PASSWORD'] @@ -190,7 +210,11 @@ def test_container_env_stats_password_2(): "abort_on_error": False, "config": {}, "enabled": [] - } + }, + "update_ingress_status": True, + "deployment_mode": "auto", + "external_hostname": "", + "ingress_status_update_interval": 30 } == ContainerEnv.read() finally: del os.environ['HAPROXY_USERNAME'] @@ -224,7 +248,11 @@ def test_container_env_certbot_email(): "abort_on_error": False, "config": {}, "enabled": [] - } + }, + "update_ingress_status": True, + "deployment_mode": "auto", + "external_hostname": "", + "ingress_status_update_interval": 30 } == ContainerEnv.read() finally: del os.environ['EASYHAPROXY_CERTBOT_EMAIL'] @@ -262,7 +290,11 @@ def test_container_env_certbot_full(): "abort_on_error": False, "config": {}, "enabled": [] - } + }, + "update_ingress_status": True, + "deployment_mode": "auto", + "external_hostname": "", + "ingress_status_update_interval": 30 } == ContainerEnv.read() finally: del os.environ['EASYHAPROXY_CERTBOT_EMAIL'] @@ -302,7 +334,11 @@ def test_container_log_level(): "abort_on_error": False, "config": {}, "enabled": [] - } + }, + "update_ingress_status": True, + "deployment_mode": "auto", + "external_hostname": "", + "ingress_status_update_interval": 30 } == ContainerEnv.read() finally: del os.environ['CERTBOT_LOG_LEVEL']