You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by ki...@apache.org on 2025/12/24 07:07:43 UTC
(trafficserver-ingress-controller) branch master updated: Sni CRD (#320)
This is an automated email from the ASF dual-hosted git repository.
kichan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver-ingress-controller.git
The following commit(s) were added to refs/heads/master by this push:
new 630332c Sni CRD (#320)
630332c is described below
commit 630332c644e499c7e4dfbe814251117caf91c732
Author: saurabh-saraswat <85...@users.noreply.github.com>
AuthorDate: Wed Dec 24 12:37:37 2025 +0530
Sni CRD (#320)
* Added support for SNI CRD
* Corrected docker image name. Fixed marshalling in handlerSni. Removed not needed certificates from test file
* Updated ats.go
* updated versions
* Added documentation for how to use SNI
---
.github/workflows/build.yml | 10 +
ats_caching/atscachingpolicy.yaml | 6 +
ats_sni/ats-snipolicy-binding.yaml | 12 +
ats_sni/ats-snipolicy-role.yaml | 8 +
ats_sni/atssnipolicy.yaml | 11 +
ats_sni/crd-atssnipolicy.yaml | 90 ++++
ats_sni/host-sni-policy/disabled.yaml | 8 +
ats_sni/host-sni-policy/enforced.yaml | 9 +
ats_sni/host-sni-policy/permissive.yaml | 9 +
ats_sni/http2/off.yaml | 8 +
ats_sni/http2/on.yaml | 8 +
ats_sni/verify-client/moderate.yaml | 8 +
ats_sni/verify-client/none.yaml | 8 +
ats_sni/verify-client/strict.yaml | 8 +
ats_sni/verify-server-policy/disabled.yaml | 8 +
ats_sni/verify-server-policy/enforced.yaml | 8 +
ats_sni/verify-server-policy/permissive.yaml | 10 +
docs/CRD_SNI.md | 442 +++++++++++++++++
go.mod | 2 +-
k8s/images/node-app-3/.dockerignore | 3 +
k8s/images/node-app-3/Dockerfile | 31 ++
k8s/images/node-app-3/hello.html | 15 +
k8s/images/node-app-3/package-lock.json | 715 +++++++++++++++++++++++++++
k8s/images/node-app-3/package.json | 14 +
k8s/images/node-app-3/server.js | 51 ++
k8s/images/node-app-3/yaml/app3-deploy.yaml | 66 +++
k8s/images/node-app-4/.dockerignore | 3 +
k8s/images/node-app-4/Dockerfile | 31 ++
k8s/images/node-app-4/hello.html | 15 +
k8s/images/node-app-4/package-lock.json | 715 +++++++++++++++++++++++++++
k8s/images/node-app-4/package.json | 14 +
k8s/images/node-app-4/server.js | 51 ++
k8s/images/node-app-4/yaml/app4-deploy.yaml | 66 +++
k8s/images/node-app-4/yaml/app4-ingress.yaml | 31 ++
proxy/ats.go | 11 +
proxy/fakeATS.go | 4 +-
tests/suite/test_ingress.py | 516 ++++++++++++++++++-
watcher/handlerSni.go | 282 +++++++++++
watcher/handlerSni_test.go | 247 +++++++++
watcher/watcher.go | 31 +-
watcher/watcher_test.go | 187 +++++++
41 files changed, 3757 insertions(+), 15 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index bb09f5b..7352eab 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -64,6 +64,16 @@ jobs:
eval $(minikube -p minikube docker-env)
docker build -t caching-app k8s/images/caching-app/
+ - name: Build App 3
+ run: |
+ eval $(minikube -p minikube docker-env)
+ docker build -t node-app-3 k8s/images/node-app-3/
+
+ - name: Build App 4
+ run: |
+ eval $(minikube -p minikube docker-env)
+ docker build -t node-app-4 k8s/images/node-app-4/
+
- name: Install dependencies
run: |
cd tests
diff --git a/ats_caching/atscachingpolicy.yaml b/ats_caching/atscachingpolicy.yaml
index 29e2db4..df97a00 100644
--- a/ats_caching/atscachingpolicy.yaml
+++ b/ats_caching/atscachingpolicy.yaml
@@ -17,3 +17,9 @@ spec:
pattern: ".*/cache-test"
action: cache
ttl: "12s"
+ - name: app3-endpoint
+ primarySpecifier:
+ type: url_regex
+ pattern: ".*/node-app3"
+ action: cache
+ ttl: "12s"
diff --git a/ats_sni/ats-snipolicy-binding.yaml b/ats_sni/ats-snipolicy-binding.yaml
new file mode 100644
index 0000000..eb1f74c
--- /dev/null
+++ b/ats_sni/ats-snipolicy-binding.yaml
@@ -0,0 +1,12 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: ats-snipolicy-binding
+subjects:
+ - kind: ServiceAccount
+ name: default
+ namespace: trafficserver-test
+roleRef:
+ kind: ClusterRole
+ name: ats-snipolicy-role
+ apiGroup: rbac.authorization.k8s.io
diff --git a/ats_sni/ats-snipolicy-role.yaml b/ats_sni/ats-snipolicy-role.yaml
new file mode 100644
index 0000000..3ac34b0
--- /dev/null
+++ b/ats_sni/ats-snipolicy-role.yaml
@@ -0,0 +1,8 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: ats-snipolicy-role
+rules:
+ - apiGroups: ["trafficserver.apache.org"]
+ resources: ["atssnipolicies"]
+ verbs: ["get", "list", "watch"]
diff --git a/ats_sni/atssnipolicy.yaml b/ats_sni/atssnipolicy.yaml
new file mode 100644
index 0000000..7c33e98
--- /dev/null
+++ b/ats_sni/atssnipolicy.yaml
@@ -0,0 +1,11 @@
+apiVersion: trafficserver.apache.org/v1alpha1
+kind: ATSSniPolicy
+metadata:
+ name: my-sni-config
+spec:
+ sni:
+ - fqdn: "test.edge.com"
+ host_sni_policy: "PERMISSIVE"
+ verify_server_policy: "PERMISSIVE"
+ http2: "on"
+ verify_client: "MODERATE"
diff --git a/ats_sni/crd-atssnipolicy.yaml b/ats_sni/crd-atssnipolicy.yaml
new file mode 100644
index 0000000..ec58dd0
--- /dev/null
+++ b/ats_sni/crd-atssnipolicy.yaml
@@ -0,0 +1,90 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ name: atssnipolicies.trafficserver.apache.org
+spec:
+ group: trafficserver.apache.org
+ names:
+ kind: ATSSniPolicy
+ plural: atssnipolicies
+ singular: atssnipolicy
+ scope: Cluster
+ versions:
+ - name: v1alpha1
+ served: true
+ storage: true
+ schema:
+ openAPIV3Schema:
+ type: object
+ required: ["spec"]
+ properties:
+ spec:
+ type: object
+ properties:
+ sni:
+ type: array
+ items:
+ type: object
+ properties:
+ fqdn:
+ type: string
+ verify_client:
+ type: string
+ enum: ["NONE", "MODERATE", "STRICT"]
+ verify_client_ca_certs:
+ type: object
+ properties:
+ file:
+ type: string
+ dir:
+ type: string
+ verify_server_policy:
+ type: string
+ enum: ["DISABLED", "PERMISSIVE", "ENFORCED"]
+ verify_server_properties:
+ type: string
+ enum: ["NONE", "SIGNATURE", "NAME", "ALL"]
+ client_cert:
+ type: string
+ client_key:
+ type: string
+ client_sni_policy:
+ type: string
+ ip_allow:
+ type: array
+ items:
+ type: string
+ host_sni_policy:
+ type: string
+ enum: ["DISABLED", "PERMISSIVE", "ENFORCED"]
+ valid_tls_versions_in:
+ type: array
+ items:
+ type: string
+ enum: ["TLSv1", "TLSv1_1", "TLSv1_2", "TLSv1_3"]
+ http2:
+ type: string
+ enum: ["on", "off"]
+ http2_buffer_water_mark:
+ type: integer
+ http2_max_settings_frames_per_minute:
+ type: integer
+ http2_max_ping_frames_per_minute:
+ type: integer
+ http2_max_priority_frames_per_minute:
+ type: integer
+ http2_max_rst_stream_frames_per_minute:
+ type: integer
+ disable_h2:
+ type: boolean
+ tunnel_route:
+ type: string
+ forward_route:
+ type: string
+ partial_blind_route:
+ type: string
+ tunnel_alpn:
+ type: array
+ items:
+ type: string
+
diff --git a/ats_sni/host-sni-policy/disabled.yaml b/ats_sni/host-sni-policy/disabled.yaml
new file mode 100644
index 0000000..0c75d06
--- /dev/null
+++ b/ats_sni/host-sni-policy/disabled.yaml
@@ -0,0 +1,8 @@
+apiVersion: trafficserver.apache.org/v1alpha1
+kind: ATSSniPolicy
+metadata:
+ name: my-sni-config
+spec:
+ sni:
+ - fqdn: "test.edge.com"
+ host_sni_policy: "DISABLED"
diff --git a/ats_sni/host-sni-policy/enforced.yaml b/ats_sni/host-sni-policy/enforced.yaml
new file mode 100644
index 0000000..c357c3c
--- /dev/null
+++ b/ats_sni/host-sni-policy/enforced.yaml
@@ -0,0 +1,9 @@
+apiVersion: trafficserver.apache.org/v1alpha1
+kind: ATSSniPolicy
+metadata:
+ name: my-sni-config
+spec:
+ sni:
+ - fqdn: "test.edge.com"
+ host_sni_policy: "ENFORCED"
+ verify_client: "MODERATE"
diff --git a/ats_sni/host-sni-policy/permissive.yaml b/ats_sni/host-sni-policy/permissive.yaml
new file mode 100644
index 0000000..0d33622
--- /dev/null
+++ b/ats_sni/host-sni-policy/permissive.yaml
@@ -0,0 +1,9 @@
+apiVersion: trafficserver.apache.org/v1alpha1
+kind: ATSSniPolicy
+metadata:
+ name: my-sni-config
+spec:
+ sni:
+ - fqdn: "test.edge.com"
+ host_sni_policy: "PERMISSIVE"
+ verify_client: "MODERATE"
diff --git a/ats_sni/http2/off.yaml b/ats_sni/http2/off.yaml
new file mode 100644
index 0000000..f5f7d46
--- /dev/null
+++ b/ats_sni/http2/off.yaml
@@ -0,0 +1,8 @@
+apiVersion: trafficserver.apache.org/v1alpha1
+kind: ATSSniPolicy
+metadata:
+ name: my-sni-config
+spec:
+ sni:
+ - fqdn: "test.edge.com"
+ http2: "off"
diff --git a/ats_sni/http2/on.yaml b/ats_sni/http2/on.yaml
new file mode 100644
index 0000000..b050f68
--- /dev/null
+++ b/ats_sni/http2/on.yaml
@@ -0,0 +1,8 @@
+apiVersion: trafficserver.apache.org/v1alpha1
+kind: ATSSniPolicy
+metadata:
+ name: my-sni-config
+spec:
+ sni:
+ - fqdn: "test.edge.com"
+ http2: "on"
diff --git a/ats_sni/verify-client/moderate.yaml b/ats_sni/verify-client/moderate.yaml
new file mode 100644
index 0000000..5aa9048
--- /dev/null
+++ b/ats_sni/verify-client/moderate.yaml
@@ -0,0 +1,8 @@
+apiVersion: trafficserver.apache.org/v1alpha1
+kind: ATSSniPolicy
+metadata:
+ name: my-sni-config
+spec:
+ sni:
+ - fqdn: "test.edge.com"
+ verify_client: "MODERATE"
diff --git a/ats_sni/verify-client/none.yaml b/ats_sni/verify-client/none.yaml
new file mode 100644
index 0000000..6ddb7de
--- /dev/null
+++ b/ats_sni/verify-client/none.yaml
@@ -0,0 +1,8 @@
+apiVersion: trafficserver.apache.org/v1alpha1
+kind: ATSSniPolicy
+metadata:
+ name: my-sni-config
+spec:
+ sni:
+ - fqdn: "test.edge.com"
+ verify_client: "NONE"
diff --git a/ats_sni/verify-client/strict.yaml b/ats_sni/verify-client/strict.yaml
new file mode 100644
index 0000000..b6f9c7b
--- /dev/null
+++ b/ats_sni/verify-client/strict.yaml
@@ -0,0 +1,8 @@
+apiVersion: trafficserver.apache.org/v1alpha1
+kind: ATSSniPolicy
+metadata:
+ name: my-sni-config
+spec:
+ sni:
+ - fqdn: "test.edge.com"
+ verify_client: "STRICT"
diff --git a/ats_sni/verify-server-policy/disabled.yaml b/ats_sni/verify-server-policy/disabled.yaml
new file mode 100644
index 0000000..2ffe582
--- /dev/null
+++ b/ats_sni/verify-server-policy/disabled.yaml
@@ -0,0 +1,8 @@
+apiVersion: trafficserver.apache.org/v1alpha1
+kind: ATSSniPolicy
+metadata:
+ name: my-sni-config
+spec:
+ sni:
+ - fqdn: test.example.com
+ verify_server_policy: "DISABLED"
diff --git a/ats_sni/verify-server-policy/enforced.yaml b/ats_sni/verify-server-policy/enforced.yaml
new file mode 100644
index 0000000..4152573
--- /dev/null
+++ b/ats_sni/verify-server-policy/enforced.yaml
@@ -0,0 +1,8 @@
+apiVersion: trafficserver.apache.org/v1alpha1
+kind: ATSSniPolicy
+metadata:
+ name: my-sni-config
+spec:
+ sni:
+ - fqdn: test.example.com
+ verify_server_policy: "ENFORCED"
diff --git a/ats_sni/verify-server-policy/permissive.yaml b/ats_sni/verify-server-policy/permissive.yaml
new file mode 100644
index 0000000..fd5ea91
--- /dev/null
+++ b/ats_sni/verify-server-policy/permissive.yaml
@@ -0,0 +1,10 @@
+apiVersion: trafficserver.apache.org/v1alpha1
+kind: ATSSniPolicy
+metadata:
+ name: my-sni-config
+spec:
+ sni:
+ - fqdn: test.example.com
+ verify_server_policy: "PERMISSIVE"
+
+
diff --git a/docs/CRD_SNI.md b/docs/CRD_SNI.md
new file mode 100644
index 0000000..5e0723f
--- /dev/null
+++ b/docs/CRD_SNI.md
@@ -0,0 +1,442 @@
+# Basic HTTPS Request Setup
+Before testing SNI policies, a secure TLS setup is required. This involves:
+
+## 1. Certificates Required
+
+- Root CA Certificate: The Root Certificate Authority is the trust anchor. It signs all other certificates (host, client, backend) so they can be verified. Clients like curl use the Root CA to confirm the server certificate is trusted.
+- Host certificate and key: Installed inside the ATS pod to terminate TLS.
+- Client certificate and key: Required for client verification modes like moderate or strict.
+- Backend Certificate: Used by ATS when connecting to backend services over TLS, ensuring secure communication and backend identity verification.
+
+
+
+## 2. Why These Are Needed
+
+- Root CA: Ensures the client trusts the server certificate by acting as the trust anchor.
+- Host Certificate: Allows ATS to present a valid identity during the TLS handshake.
+- Client Certificate: If client verification is enabled, ATS validates the client certificate against the CA to ensure mutual TLS.
+- Backend Certificate: Ensures ATS can securely connect to backend services and verify their identity.
+
+## 3. TLS Handshake Flow
+
+- Client sends SNI (hostname) during TLS handshake.
+- ATS responds with the correct certificate for that hostname.
+- If verification policies are enabled, ATS checks client/server certificates accordingly.
+
+## Different Backend Apps used.
+
+
+
+| Application | Host | Description |
+|---|---|---|
+| app2 | test.edge.com | This is for HTTP, so no backend certificates required |
+| node-app4 | test.example.com | Contains self signed backend certificates |
+| node-app3 | test.example.com | Contains backend certificates signed by rootCA.|
+
+#### The host certificates for node-app3 & node-app4 are mounted inside their pods.
+
+### Basic Curl Request
+```
+curl --cacert certs/rootCA.crt -v --resolve test.edge.com:30443:{minikubeip} https://test.edge.com:30443/app2
+```
+- --cacert certs/rootCA.crt: Trust the CA that signed the server certificate.
+
+- --resolve: Map hostname to Minikube IP and port.
+https://test.edge.com:30443/app2: HTTPS endpoint exposed by ATS ingress
+- app2 is the backend application endpoint that ATS is routing traffic to.
+ Components Explained
+
+- --cert certs/client1.crt: Provides the client certificate for mutual TLS (mTLS).
+This proves the client’s identity to the server.
+
+- --key certs/client1.key: Private key corresponding to client1.crt.
+Required to complete the client authentication process.
+
+
+### This is our expected response
+```
+* Added test.edge.com:30443:10.63.20.30 to DNS cache
+* Hostname test.edge.com was found in DNS cache
+* Trying 10.63.20.30:30443...
+* Connected to test.edge.com (10.63.20.30) port 30443
+* ALPN: curl offers h2,http/1.1
+* TLSv1.3 (OUT), TLS handshake, Client hello (1):
+* CAfile: certs/rootCA.crt
+* CApath: /etc/ssl/certs
+* TLSv1.3 (IN), TLS handshake, Server hello (2):
+* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
+* TLSv1.3 (IN), TLS handshake, Request CERT (13):
+* TLSv1.3 (IN), TLS handshake, Certificate (11):
+* TLSv1.3 (IN), TLS handshake, CERT verify (15):
+* TLSv1.3 (IN), TLS handshake, Finished (20):
+* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
+* TLSv1.3 (OUT), TLS handshake, Certificate (11):
+* TLSv1.3 (OUT), TLS handshake, Finished (20):
+* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519 / RSASSA-PSS
+* ALPN: server accepted h2
+* Server certificate:
+* subject: C=US; ST=State; L=City; O=MyOrg; OU=MyUnit; CN=test.edge.com
+* start date: Nov 27 05:42:34 2025 GMT
+* expire date: Nov 27 05:42:34 2026 GMT
+* subjectAltName: host "test.edge.com" matched cert's "test.edge.com"
+* issuer: C=US; ST=State; L=City; O=MyOrg; OU=MyUnit; CN=TestRootCA
+* SSL certificate verify ok.
+* Certificate level 0: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
+* Certificate level 1: Public key type RSA (4096/152 Bits/secBits), signed using sha256WithRSAEncryption
+* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
+* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
+* old SSL session ID is stale, removing
+* using HTTP/2
+* [HTTP/2] [1] OPENED stream for https://test.edge.com:30443/app2
+* [HTTP/2] [1] [:method: GET]
+* [HTTP/2] [1] [:scheme: https]
+* [HTTP/2] [1] [:authority: test.edge.com:30443]
+* [HTTP/2] [1] [:path: /app2]
+* [HTTP/2] [1] [user-agent: curl/8.5.0]
+* [HTTP/2] [1] [accept: */*]
+> GET /app2 HTTP/2
+> Host: test.edge.com:30443
+> User-Agent: curl/8.5.0
+> Accept: */*
+>
+< HTTP/2 200
+< x-powered-by: Express
+< accept-ranges: bytes
+< cache-control: public, max-age=0
+< last-modified: Tue, 23 Sep 2025 06:38:46 GMT
+< content-type: text/html; charset=UTF-8
+< content-length: 188
+< date: Thu, 27 Nov 2025 05:49:50 GMT
+< etag: W/"bc-199754bccf0"
+< age: 0
+< server: ATS/9.2.11
+<
+<!DOCTYPE html>
+<HTML>
+
+<HEAD>
+ <TITLE>
+ A Small Hello
+ </TITLE>
+</HEAD>
+
+<BODY>
+ <H1>Hi</H1>
+ <P>This is very minimal "hello world" HTML document.</P>
+</BODY>
+
+</HTML>
+* Connection #0 to host test.edge.com left intact
+```
+# The policies which have been tested are listed below
+
+
+
+# 1. HTTP/2 Enabled & Disabled Test
+## I. Enabled
+- When HTTP/2 is enabled in the SNI Policy, ATS should negotiate HTTP/2 during the TLS handshake.
+
+- Apply HTTP/2 Enabled Policy
+```
+kubectl apply -f ats_sni/http2/on.yaml
+```
+After applying the policy, the next step is to make the request to obtain the response.
+
+```
+curl --cacert certs/rootCA.crt -v --resolve test.edge.com:30443:{minikubeip} https://test.edge.com:30443/app2
+```
+Response:
+```
+* SSL connection using TLSv1.3
+* ALPN, server accepted to use h2
+> GET /app2 HTTP/2
+< HTTP/2 200
+```
+This Shows:
+- ALPN, server accepted to use h2 → HTTP/2 negotiated.
+- Response status line: HTTP/2 200.
+
+## II. Disabled
+
+When HTTP/2 is disabled in the SNI Policy, ATS should NOT negotiate HTTP/2 during the TLS handshake. It should fall back to HTTP/1.1.
+
+
+Apply HTTP/2 Disabled Policy
+```
+kubectl apply -f ats_sni/http2/off.yaml
+```
+Make the Request
+```
+curl --cacert certs/rootCA.crt -v --resolve test.edge.com:30443:{minikubeip} https://test.edge.com:30443/app2
+```
+Expected Response
+```
+* SSL connection using TLSv1.3
+* ALPN, server accepted to use http/1.1
+> GET /app2 HTTP/1.1
+< HTTP/1.1 200 OK
+```
+
+This Shows:
+
+- ALPN, server accepted to use http/1.1 → HTTP/2 was NOT negotiated.
+- Response status line: HTTP/1.1 200 OK → Confirms fallback to HTTP/1.1.
+
+
+# 2. Verify Client Policy
+## I. Strict Mode
+When verifyClient is set to strict, ATS requires the client to present a valid certificate during the TLS handshake. If the client does not provide a certificate, the connection fails.
+
+Apply Strict Policy
+```
+kubectl apply -f ats_sni/verify-client/strict.yaml
+```
+
+Curl Without Client Certificate
+```
+curl --cacert certs/rootCA.crt -v --resolve test.edge.com:30443:{minikubeip} https://test.edge.com:30443/app2
+```
+
+Expected Response:
+```
+* SSL connection using TLSv1.3
+* error: tlsv13 alert certificate required
+curl: (35) error: tlsv13 alert certificate required
+```
+This indicates:
+
+- TLS handshake failed because ATS enforced client certificate verification.
+- No HTTP response is returned.
+
+
+Curl With Client Certificate
+```
+curl --cacert certs/rootCA.crt --cert certs/client1.crt --key certs/client1.key -v --resolve test.edge.com:30443:{minikubeip} https://test.edge.com:30443/app2
+```
+
+
+
+Expected Response:
+```
+* SSL connection using TLSv1.3
+> GET /app2 HTTP/2
+< HTTP/2 200
+```
+This confirms:
+
+- TLS handshake succeeded with client certificate.
+- HTTP/2 or HTTP/1.1 response based on policy.
+
+Other Modes:
+- none → No client certificate required; all requests succeed.
+- moderate → Client certificate requested but optional; connection succeeds even without cert.
+
+# 3. Host SNI Policy
+Host SNI Policy determines how ATS handles mismatches between the SNI hostname (sent during TLS handshake) and the HTTP Host header.
+Modes in Brief:
+
+- disabled → ATS ignores mismatches; all requests succeed.
+- enforced → ATS blocks mismatches with 403 Forbidden.
+- permissive → ATS allows mismatches but logs them; request may return 404.
+
+
+For verifying the Host SNI policy, the node-app3 application is used as the backend
+
+## I. Disabled Mode
+
+Apply Policy
+```
+kubectl apply -f ats_sni/host-sni-policy/disabled.yaml
+```
+
+Curl with mismatched SNI and Host:
+```
+curl --cacert certs/rootCA.crt -v \
+ --resolve test.example.com:30443:{minikubeip} \
+ https://test.example.com:30443/node-app3 \
+ -H "Host: test.edge.com"
+```
+
+Expected Response:
+```
+* SSL connection using TLS
+> GET /node-app3 HTTP/1.1
+< HTTP/1.1 200 OK
+```
+Mismatch is ignored.
+
+## II. Enforced Mode
+
+Apply policy:
+```
+kubectl apply -f ats_sni/host-sni-policy/enforced.yaml
+```
+Curl with mismatch:
+```
+curl --cacert certs/rootCA.crt -v \
+--resolve test.example.com:30443:{minikubeip} \
+https://test.example.com:30443/node-app3 \
+-H "Host: test.edge.com"
+```
+
+Expected Response:
+```
+* SSL connection using TLS
+> GET /node-app3 HTTP/2
+< HTTP/2 403
+```
+Check ATS logs:
+```
+kubectl exec <trafficserver-pod> -- \grep 'SNI/hostname mismatch' /opt/ats/var/log/trafficserver/diags.log
+```
+Log entry:
+```
+SNI/hostname mismatch sni=test.example.com host=test.edge.com action=terminate
+```
+## III. Permissive Mode
+Apply policy:
+```
+kubectl apply -f ats_sni/host-sni-policy/permissive.yaml
+```
+Curl with mismatch:
+```
+curl --cacert certs/rootCA.crt -v \
+--resolve test.example.com:30443:{minikubeip} \
+https://test.example.com:30443/node-app3 \
+-H "Host: test.edge.com"
+```
+
+Expected Response:
+```
+* SSL connection using TLS
+> GET /node-app3 HTTP/2
+< HTTP/2 404
+```
+**Reason for 404:**
+The Host header (test.edge.com) does not match the SNI policy or routing configuration for test.example.com, so ATS cannot find a matching route and returns 404 Not Found.
+
+Check ATS logs:
+```
+kubectl exec <trafficserver-pod> -- \grep 'SNI/hostname mismatch' /opt/ats/var/log/trafficserver/diags.log
+```
+Log entry:
+```
+SNI/hostname mismatch sni=test.example.com host=test.edge.com action=continue
+```
+
+# 4. Verify Server Policy
+
+This policy controls how ATS validates the backend server’s certificate when making TLS connections to origin servers.
+
+Modes in Brief:
+- disabled → ATS does not verify backend certificates; all connections succeed.
+- enforced → ATS strictly verifies backend certificates; invalid certs cause 502 Bad Gateway.
+- permissive → ATS allows invalid certs but logs warnings; request still succeeds.
+
+For verifying the server policy, the node-app3 application is used as the backend, and it has valid backend certificates to ensure secure TLS communication.
+
+## I. Enforced Mode
+Apply policy:
+```
+kubectl apply -f ats_sni/verify-server-policy/enforced.yaml
+```
+
+Valid Backend Certificate
+```
+curl --cacert certs/rootCA.crt -v \
+--resolve test.example.com:30443:{minikubeip} \
+https://test.example.com:30443/node-app3
+```
+
+Expected Response:
+```
+* SSL connection using TLS
+< HTTP/2 200
+```
+
+## Invalid Backend Certificate
+
+In this the backend certificates provided in node-app4 are self signed hence the verification will fail and response will show error 502 Bad Gateway.
+
+```
+curl --cacert certs/rootCA.crt -v \
+--resolve test.example.com:30443:{minikubeip} \
+https://test.example.com:30443/node-app4
+```
+
+Expected Response:
+```
+* SSL connection using TLS
+< HTTP/2 502
+```
+Check logs:
+```
+kubectl exec <trafficserver-pod> -- \grep 'Action=Terminate' /opt/ats/var/log/trafficserver/diags.log
+```
+
+
+Log entry:
+```
+Core server certificate verification failed Action=Terminate
+```
+
+## II. Disabled Mode
+Apply policy:
+```
+kubectl apply -f ats_sni/verify-server-policy/disabled.yaml
+```
+
+Curl with invalid cert:
+```
+curl --cacert certs/rootCA.crt -v \
+--resolve test.example.com:30443:{minikubeip} \
+https://test.example.com:30443/node-app4
+```
+
+Expected Response:
+```
+* SSL connection using TLS
+< HTTP/2 200
+```
+
+ATS ignores certificate validation.
+
+## III. Permissive Mode
+Apply policy:
+```
+kubectl apply -f ats_sni/verify-server-policy/permissive.yaml
+```
+
+Curl with invalid cert:
+```
+curl --cacert certs/rootCA.crt -v \
+--resolve test.example.com:30443:{minikubeip} \
+https://test.example.com:30443/node-app4
+```
+
+Expected Response:
+```
+* SSL connection using TLS
+< HTTP/2 200
+```
+
+Check logs for warnings:
+```
+kubectl exec <trafficserver-pod> -- \grep 'Core server certificate verification failed' /opt/ats/var/log/trafficserver/diags.log
+```
+
+Log entry:
+```
+Core server certificate verification failed Action=Continue
+```
+
+### All configured tests have been executed successfully. Each scenario behaved as expected according to the applied policies, validating correct functionality across all test cases.
+
+
+
+
+
+
+
+
+
diff --git a/go.mod b/go.mod
index 2dffb77..3b72a93 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@ go 1.24.10
require (
github.com/alicebob/miniredis/v2 v2.31.1
github.com/go-redis/redis v6.15.9+incompatible
+ gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.27.9
k8s.io/apimachinery v0.27.9
k8s.io/client-go v0.27.9
@@ -45,7 +46,6 @@ require (
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
- gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.90.1 // indirect
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect
k8s.io/utils v0.0.0-20230313181309-38a27ef9d749 // indirect
diff --git a/k8s/images/node-app-3/.dockerignore b/k8s/images/node-app-3/.dockerignore
new file mode 100644
index 0000000..29d6828
--- /dev/null
+++ b/k8s/images/node-app-3/.dockerignore
@@ -0,0 +1,3 @@
+node_modules
+npm-debug.log
+
diff --git a/k8s/images/node-app-3/Dockerfile b/k8s/images/node-app-3/Dockerfile
new file mode 100644
index 0000000..68964b7
--- /dev/null
+++ b/k8s/images/node-app-3/Dockerfile
@@ -0,0 +1,31 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Dockerfile
+FROM node:20-alpine
+
+WORKDIR /usr/src/app
+
+COPY package*.json ./
+RUN npm install --production
+
+COPY . .
+
+# expose the HTTPS port that the node app listens on
+EXPOSE 8443
+
+CMD ["node", "server.js"]
+
diff --git a/k8s/images/node-app-3/hello.html b/k8s/images/node-app-3/hello.html
new file mode 100644
index 0000000..710fe77
--- /dev/null
+++ b/k8s/images/node-app-3/hello.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<HTML>
+
+<HEAD>
+ <TITLE>
+ A Small Hello from App3
+ </TITLE>
+</HEAD>
+
+<BODY>
+ <H1>Hi</H1>
+ <P>This is very minimal "hello world" HTML document.</P>
+</BODY>
+
+</HTML>
diff --git a/k8s/images/node-app-3/package-lock.json b/k8s/images/node-app-3/package-lock.json
new file mode 100644
index 0000000..948d9c2
--- /dev/null
+++ b/k8s/images/node-app-3/package-lock.json
@@ -0,0 +1,715 @@
+{
+ "name": "dockernode",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "dockernode",
+ "version": "1.0.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "express": "^4.21.2"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.3",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
+ "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.13.0",
+ "raw-body": "2.5.2",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
+ "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
+ "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
+ "dependencies": {
+ "get-intrinsic": "^1.2.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.21.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.3",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.7.1",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.3.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.13.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.19.0",
+ "serve-static": "1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
+ "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
+ "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "hasown": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.1.0.tgz",
+ "integrity": "sha512-QLdzI9IIO1Jg7f9GT1gXpPpXArAn6cS31R1eEZqz08Gc+uQ8/XiqHWt17Fiw+2p6oTTIq5GXEpQkAlA88YRl/Q==",
+ "dependencies": {
+ "call-bind": "^1.0.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
+ "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
+ "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
+ "dependencies": {
+ "side-channel": "^1.0.6"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "node_modules/send": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.19.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
+ },
+ "node_modules/side-channel": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
+ "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.4",
+ "object-inspect": "^1.13.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ }
+ }
+}
diff --git a/k8s/images/node-app-3/package.json b/k8s/images/node-app-3/package.json
new file mode 100644
index 0000000..844a7d7
--- /dev/null
+++ b/k8s/images/node-app-3/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "dockernode",
+ "version": "1.0.0",
+ "description": "docker-ize a very simple express.js app",
+ "main": "server.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "Apache Software Foundation",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "express": "^4.21.2"
+ }
+}
diff --git a/k8s/images/node-app-3/server.js b/k8s/images/node-app-3/server.js
new file mode 100644
index 0000000..2619eb1
--- /dev/null
+++ b/k8s/images/node-app-3/server.js
@@ -0,0 +1,51 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+'use strict';
+
+const express = require('express');
+const https = require('https');
+const fs = require('fs');
+const path = require('path');
+
+// Constants
+const PORT = 8443;
+const HOST = '0.0.0.0';
+
+// Read TLS cert/key mounted from k8s secret at /etc/ats/ssl/server2
+const options = {
+ cert: fs.readFileSync('backend.crt'),
+ key: fs.readFileSync('backend.key'),
+};
+
+const app = express();
+
+// serve static HTML file(s) from project dir (assumes hello.html exists)
+app.get('/', (req, res) => {
+ res.sendFile(path.join(__dirname, 'hello.html'));
+});
+
+app.get('/test', (req, res) => {
+ res.sendFile(path.join(__dirname, 'hello.html'));
+});
+
+app.get('/node-app3', (req, res) => {
+ res.sendFile(path.join(__dirname, 'hello.html'));
+});
+
+// simple healthcheck used by k8s probes
+app.get('/health', (req, res) => res.status(200).send('OK'));
+
+https.createServer(options, app).listen(PORT, HOST, () => {
+ console.log(`HTTPS server running on https://${HOST}:${PORT}`);
+});
+
diff --git a/k8s/images/node-app-3/yaml/app3-deploy.yaml b/k8s/images/node-app-3/yaml/app3-deploy.yaml
new file mode 100644
index 0000000..1c5d2bc
--- /dev/null
+++ b/k8s/images/node-app-3/yaml/app3-deploy.yaml
@@ -0,0 +1,66 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: backend
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: node-app
+ namespace: backend
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: node-app
+ template:
+ metadata:
+ labels:
+ app: node-app
+ spec:
+ containers:
+ - name: node-https
+ image: node-https-app:latest
+ ports:
+ - containerPort: 8443
+ name: https
+ protocol: TCP
+ volumeMounts:
+ - name: https-tls
+ mountPath: /etc/ats/ssl/server2
+ readOnly: true
+ readinessProbe:
+ httpGet:
+ path: /health
+ port: 8443
+ scheme: HTTPS
+ initialDelaySeconds: 5
+ periodSeconds: 10
+ failureThreshold: 3
+ livenessProbe:
+ httpGet:
+ path: /health
+ port: 8443
+ scheme: HTTPS
+ initialDelaySeconds: 15
+ periodSeconds: 20
+ volumes:
+ - name: https-tls
+ secret:
+ secretName: app3-secret
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: appsvc3
+ namespace: backend
+spec:
+ selector:
+ app: node-app
+ ports:
+ - name: https
+ port: 8443
+ targetPort: 8443
+ protocol: TCP
+ type: ClusterIP
+
diff --git a/k8s/images/node-app-4/.dockerignore b/k8s/images/node-app-4/.dockerignore
new file mode 100644
index 0000000..29d6828
--- /dev/null
+++ b/k8s/images/node-app-4/.dockerignore
@@ -0,0 +1,3 @@
+node_modules
+npm-debug.log
+
diff --git a/k8s/images/node-app-4/Dockerfile b/k8s/images/node-app-4/Dockerfile
new file mode 100644
index 0000000..68964b7
--- /dev/null
+++ b/k8s/images/node-app-4/Dockerfile
@@ -0,0 +1,31 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Dockerfile
+FROM node:20-alpine
+
+WORKDIR /usr/src/app
+
+COPY package*.json ./
+RUN npm install --production
+
+COPY . .
+
+# expose the HTTPS port that the node app listens on
+EXPOSE 8443
+
+CMD ["node", "server.js"]
+
diff --git a/k8s/images/node-app-4/hello.html b/k8s/images/node-app-4/hello.html
new file mode 100644
index 0000000..fea9e0e
--- /dev/null
+++ b/k8s/images/node-app-4/hello.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<HTML>
+
+<HEAD>
+ <TITLE>
+ A Small Hello from App4
+ </TITLE>
+</HEAD>
+
+<BODY>
+ <H1>Hi</H1>
+ <P>This is very minimal "hello world" HTML document.</P>
+</BODY>
+
+</HTML>
diff --git a/k8s/images/node-app-4/package-lock.json b/k8s/images/node-app-4/package-lock.json
new file mode 100644
index 0000000..948d9c2
--- /dev/null
+++ b/k8s/images/node-app-4/package-lock.json
@@ -0,0 +1,715 @@
+{
+ "name": "dockernode",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "dockernode",
+ "version": "1.0.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "express": "^4.21.2"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.3",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
+ "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.13.0",
+ "raw-body": "2.5.2",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
+ "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
+ "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
+ "dependencies": {
+ "get-intrinsic": "^1.2.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.21.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.3",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.7.1",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.3.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.13.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.19.0",
+ "serve-static": "1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
+ "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
+ "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "hasown": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.1.0.tgz",
+ "integrity": "sha512-QLdzI9IIO1Jg7f9GT1gXpPpXArAn6cS31R1eEZqz08Gc+uQ8/XiqHWt17Fiw+2p6oTTIq5GXEpQkAlA88YRl/Q==",
+ "dependencies": {
+ "call-bind": "^1.0.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
+ "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
+ "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
+ "dependencies": {
+ "side-channel": "^1.0.6"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "node_modules/send": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.19.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
+ },
+ "node_modules/side-channel": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
+ "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.4",
+ "object-inspect": "^1.13.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ }
+ }
+}
diff --git a/k8s/images/node-app-4/package.json b/k8s/images/node-app-4/package.json
new file mode 100644
index 0000000..844a7d7
--- /dev/null
+++ b/k8s/images/node-app-4/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "dockernode",
+ "version": "1.0.0",
+ "description": "docker-ize a very simple express.js app",
+ "main": "server.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "Apache Software Foundation",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "express": "^4.21.2"
+ }
+}
diff --git a/k8s/images/node-app-4/server.js b/k8s/images/node-app-4/server.js
new file mode 100644
index 0000000..597251d
--- /dev/null
+++ b/k8s/images/node-app-4/server.js
@@ -0,0 +1,51 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+'use strict';
+
+const express = require('express');
+const https = require('https');
+const fs = require('fs');
+const path = require('path');
+
+// Constants
+const PORT = 8443;
+const HOST = '0.0.0.0';
+
+// Read TLS cert/key mounted from k8s secret at /etc/ats/ssl/server2
+const options = {
+ cert: fs.readFileSync('origin.crt'),
+ key: fs.readFileSync('origin.key'),
+};
+
+const app = express();
+
+// serve static HTML file(s) from project dir (assumes hello.html exists)
+app.get('/', (req, res) => {
+ res.sendFile(path.join(__dirname, 'hello.html'));
+});
+
+app.get('/test', (req, res) => {
+ res.sendFile(path.join(__dirname, 'hello.html'));
+});
+
+app.get('/node-app4', (req, res) => {
+ res.sendFile(path.join(__dirname, 'hello.html'));
+});
+
+// simple healthcheck used by k8s probes
+app.get('/health', (req, res) => res.status(200).send('OK'));
+
+https.createServer(options, app).listen(PORT, HOST, () => {
+ console.log(`HTTPS server running on https://${HOST}:${PORT}`);
+});
+
diff --git a/k8s/images/node-app-4/yaml/app4-deploy.yaml b/k8s/images/node-app-4/yaml/app4-deploy.yaml
new file mode 100644
index 0000000..7f7b327
--- /dev/null
+++ b/k8s/images/node-app-4/yaml/app4-deploy.yaml
@@ -0,0 +1,66 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: backend
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: node-app-4
+ namespace: backend
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: node-app-4
+ template:
+ metadata:
+ labels:
+ app: node-app-4
+ spec:
+ containers:
+ - name: node-app-4-container
+ image: node-app-4:latest
+ ports:
+ - containerPort: 8443
+ name: https
+ protocol: TCP
+ volumeMounts:
+ - name: https-tls
+ mountPath: /etc/ats/ssl/server2
+ readOnly: true
+ readinessProbe:
+ httpGet:
+ path: /health
+ port: 8443
+ scheme: HTTPS
+ initialDelaySeconds: 5
+ periodSeconds: 10
+ failureThreshold: 3
+ livenessProbe:
+ httpGet:
+ path: /health
+ port: 8443
+ scheme: HTTPS
+ initialDelaySeconds: 15
+ periodSeconds: 20
+ volumes:
+ - name: https-tls
+ secret:
+ secretName: app3-secret
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: appsvc4
+ namespace: backend
+spec:
+ selector:
+ app: node-app-4
+ ports:
+ - name: https
+ port: 8443
+ targetPort: 8443
+ protocol: TCP
+ type: ClusterIP
+
diff --git a/k8s/images/node-app-4/yaml/app4-ingress.yaml b/k8s/images/node-app-4/yaml/app4-ingress.yaml
new file mode 100644
index 0000000..626f8e1
--- /dev/null
+++ b/k8s/images/node-app-4/yaml/app4-ingress.yaml
@@ -0,0 +1,31 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: node-app
+ namespace: backend
+ annotations:
+ ats.ingress.kubernetes.io/origin-server-protocol: "https"
+spec:
+ ingressClassName: ats
+ tls:
+ - hosts:
+ - test.example.com
+ secretName: app3-secret
+ rules:
+ - host: test.example.com
+ http:
+ paths:
+ - path: /node-app4
+ pathType: Prefix
+ backend:
+ service:
+ name: appsvc4
+ port:
+ number: 8443
+ - path: /node-app3
+ pathType: Prefix
+ backend:
+ service:
+ name: appsvc3
+ port:
+ number: 8443
diff --git a/proxy/ats.go b/proxy/ats.go
index b23c702..e25eb35 100644
--- a/proxy/ats.go
+++ b/proxy/ats.go
@@ -28,6 +28,7 @@ type ATSManagerInterface interface {
ConfigSet(k, v string) (string, error)
ConfigGet(k string) (string, error)
CacheSet() (string, error)
+ SniSet() (string, error)
IncludeIngressClass(c string) bool
}
@@ -69,6 +70,16 @@ func (m *ATSManager) CacheSet() (msg string, err error) {
}
+func (m *ATSManager) SniSet() (msg string, err error) {
+ cmd := exec.Command("traffic_ctl", "config", "reload")
+ stdoutStderr, err := cmd.CombinedOutput()
+ if err != nil {
+ return "", fmt.Errorf("failed to execute: traffic_ctl config reload Error: %s", err.Error())
+ }
+ return fmt.Sprintf("Reload succesful --> stdoutStderr: %q", stdoutStderr), nil
+
+}
+
func (m *ATSManager) ConfigGet(k string) (msg string, err error) {
cmd := exec.Command("traffic_ctl", "config", "get", k)
stdoutStderr, err := cmd.CombinedOutput()
diff --git a/proxy/fakeATS.go b/proxy/fakeATS.go
index 14144db..b9bb647 100644
--- a/proxy/fakeATS.go
+++ b/proxy/fakeATS.go
@@ -40,9 +40,11 @@ func (m *FakeATSManager) IncludeIngressClass(c string) bool {
func (m *FakeATSManager) CacheSet() (msg string, err error) {
return "Config reload succesful", nil
-
}
+func (m *FakeATSManager) SniSet() (msg string, err error) {
+ return "Config reload succesful", nil
+}
func (m *FakeATSManager) ConfigSet(k, v string) (msg string, err error) {
m.Config[k] = v
return fmt.Sprintf("Ran p.Key: %s p.Val: %s", k, v), nil
diff --git a/tests/suite/test_ingress.py b/tests/suite/test_ingress.py
index 7c63fec..257336c 100644
--- a/tests/suite/test_ingress.py
+++ b/tests/suite/test_ingress.py
@@ -33,17 +33,156 @@ def kubectl_delete(resource):
os.system('kubectl delete ' + resource)
def misc_command(command):
- os.system(command)
+ rc = os.system(command)
+ if rc != 0:
+ # os.system returns a shell-dependent code; keep it simple:
+ raise RuntimeError(f"Command failed (rc={rc}): {command}")
+def create_certs():
+
+ # Work dir
+ misc_command('mkdir -p certs')
+
+ # Root CA
+ misc_command('openssl genrsa -out certs/rootCA.key 4096')
+ misc_command(
+ 'openssl req -x509 -new -key certs/rootCA.key -sha256 -days 3650 '
+ '-out certs/rootCA.crt '
+ '-subj "/C=US/ST=State/L=City/O=MyOrg/OU=MyUnit/CN=TestRootCA" '
+ '-addext "basicConstraints=critical,CA:TRUE" '
+ '-addext "keyUsage=critical,keyCertSign,cRLSign" '
+ '-addext "subjectKeyIdentifier=hash"'
+ )
+
+ #Self-Signed certificate for node-app-4
+ misc_command('openssl genrsa -out ../k8s/images/node-app-4/origin.key 4096')
+ misc_command(
+ 'openssl req -x509 -new -key ../k8s/images/node-app-4/origin.key -sha256 -days 3650 '
+ '-out ../k8s/images/node-app-4/origin.crt '
+ '-subj "/C=US/ST=State/L=City/O=MyOrg/OU=MyUnit/CN=test.example.com" '
+ )
+
+ # Backend CA
+ misc_command('openssl genrsa -out ../k8s/images/node-app-3/backend.key 2048')
+ misc_command(
+ 'openssl req -new -key ../k8s/images/node-app-3/backend.key '
+ '-out ../k8s/images/node-app-3/backend.csr '
+ '-subj "/C=US/ST=State/L=City/O=TestOrg/CN=test.example.com.backend.svc.cluster.local" '
+ )
+ misc_command(
+ 'openssl x509 -req -in ../k8s/images/node-app-3/backend.csr -CA certs/rootCA.crt -CAkey certs/rootCA.key -CAcreateserial '
+ '-out ../k8s/images/node-app-3/backend.crt '
+ '-days 365 -sha256 '
+ )
+
+ # Server key + CSR
+ misc_command('openssl genrsa -out certs/server.key 2048')
+ misc_command(
+ 'openssl req -new -key certs/server.key -out certs/server.csr '
+ '-subj "/C=US/ST=State/L=City/O=MyOrg/OU=MyUnit/CN=test.edge.com" '
+ '-addext "subjectAltName=DNS:test.edge.com"'
+ )
+
+ # Server ext file
+ server_ext = textwrap.dedent("""\
+ [ v3_server ]
+ basicConstraints = CA:FALSE
+ keyUsage = digitalSignature, keyEncipherment
+ extendedKeyUsage = serverAuth
+ subjectAltName = @alt_names
+
+ [ alt_names ]
+ DNS.1 = test.edge.com
+ """)
+ with open("certs/server_ext.cnf", "w", encoding="utf-8") as f:
+ f.write(server_ext)
+
+ # Sign server CSR
+ misc_command(
+ 'openssl x509 -req -in certs/server.csr '
+ '-CA certs/rootCA.crt -CAkey certs/rootCA.key -CAcreateserial '
+ '-out certs/server.crt -days 365 -sha256 '
+ '-extfile certs/server_ext.cnf -extensions v3_server'
+ )
+ misc_command('openssl verify -CAfile certs/rootCA.crt certs/server.crt')
+
+ # Client key + CSR
+ misc_command('openssl genrsa -out certs/client1.key 2048')
+ misc_command(
+ 'openssl req -new -key certs/client1.key -out certs/client1.csr '
+ '-subj "/C=US/ST=State/L=City/O=MyOrg/OU=QA/CN=client1"'
+ )
+ # Client ext file
+ client_ext = textwrap.dedent("""\
+ [ v3_client ]
+ basicConstraints = CA:FALSE
+ keyUsage = digitalSignature, keyEncipherment
+ extendedKeyUsage = clientAuth
+ """)
+ with open("certs/client_ext.cnf", "w", encoding="utf-8") as f:
+ f.write(client_ext)
+
+ # Sign client CSR
+ misc_command(
+ 'openssl x509 -req -in certs/client1.csr '
+ '-CA certs/rootCA.crt -CAkey certs/rootCA.key -CAcreateserial '
+ '-out certs/client1.crt -days 365 -sha256 '
+ '-extfile certs/client_ext.cnf -extensions v3_client'
+ )
+ misc_command('openssl verify -CAfile certs/rootCA.crt certs/client1.crt')
+
+ # Server2 key + CSR
+ misc_command('openssl genrsa -out certs/server2.key 2048')
+ misc_command(
+ 'openssl req -new -key certs/server2.key -out certs/server2.csr '
+ '-subj "/C=US/ST=State/L=City/O=MyOrg/OU=MyUnit/CN=test.example.com" '
+ '-addext "subjectAltName=DNS:test.example.com"'
+ )
+
+ # Server2 ext file
+ server2_ext = textwrap.dedent("""\
+ [ v3_server2 ]
+ basicConstraints = CA:FALSE
+ keyUsage = digitalSignature, keyEncipherment
+ extendedKeyUsage = serverAuth
+ subjectAltName = @alt_names
+
+ [ alt_names ]
+ DNS.1 = test.example.com
+ """)
+ with open("certs/server2_ext.cnf", "w", encoding="utf-8") as f:
+ f.write(server2_ext)
+
+ # Sign server CSR
+ misc_command(
+ 'openssl x509 -req -in certs/server2.csr '
+ '-CA certs/rootCA.crt -CAkey certs/rootCA.key -CAcreateserial '
+ '-out certs/server2.crt -days 365 -sha256 '
+ '-extfile certs/server2_ext.cnf -extensions v3_server2'
+ )
+ misc_command('openssl verify -CAfile certs/rootCA.crt certs/server2.crt')
def setup_module(module):
+ create_certs()
misc_command('openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -keyout tls.key -out tls.crt -subj "/CN=atssvc/O=atssvc"')
-
kubectl_create('namespace trafficserver-test')
+ kubectl_create('namespace backend')
+ kubectl_create('secret tls app3-secret --key certs/server2.key --cert certs/server2.crt -n backend --dry-run=client -o yaml | kubectl apply -f -')
kubectl_create('secret tls tls-secret --key tls.key --cert tls.crt -n trafficserver-test --dry-run=client -o yaml | kubectl apply -f -')
+ kubectl_create('secret tls server-secret --key certs/server.key --cert certs/server.crt -n trafficserver-test --dry-run=client -o yaml | kubectl apply -f -')
+ kubectl_create('secret tls ca-secret --key certs/rootCA.key --cert certs/rootCA.crt -n trafficserver-test --dry-run=client -o yaml | kubectl apply -f -')
+ kubectl_create('secret tls server2-secret --key certs/server2.key --cert certs/server2.crt -n trafficserver-test --dry-run=client -o yaml | kubectl apply -f -')
kubectl_apply('data/setup/configmaps/')
kubectl_apply('data/setup/traffic-server/')
kubectl_apply('data/setup/apps/')
kubectl_apply('data/setup/ingresses/')
+ kubectl_apply('../k8s/images/node-app-3/yaml/')
+ kubectl_apply('../k8s/images/node-app-4/yaml/')
+
+ #Sni crd
+ kubectl_apply('../ats_sni/ats-snipolicy-role.yaml')
+ kubectl_apply('../ats_sni/ats-snipolicy-binding.yaml')
+ kubectl_apply('../ats_sni/crd-atssnipolicy.yaml')
+ #kubectl_apply('data/setup/ats_sni/atssnipolicy.yaml')
#Applying here as it takes some time for controller to get notification from kubernetes.
kubectl_apply('../ats_caching/ats-cachingpolicy-role.yaml')
@@ -52,7 +191,7 @@ def setup_module(module):
kubectl_apply('../ats_caching/atscachingpolicy.yaml')
kubectl_apply('data/caching-app/')
- time.sleep(90)
+ time.sleep(60)
misc_command('kubectl get all -A')
misc_command('kubectl get pod -A -o wide')
misc_command('kubectl logs $(kubectl get pod -n trafficserver-test-2 -o name | head -1) -n trafficserver-test-2')
@@ -67,15 +206,42 @@ def setup_module(module):
misc_command('kubectl exec $(kubectl get pod -n trafficserver-test -o name) -n trafficserver-test -- curl -v $(kubectl get service/appsvc1 -n trafficserver-test-2 -o jsonpath={.spec.clusterIP}):8080/app1')
# misc_command('kubectl exec $(kubectl get pod -n trafficserver-test -o name) -n trafficserver-test -- curl -v $(kubectl get service/appsvc2 -n trafficserver-test-2 -o jsonpath={.spec.clusterIP}):8080/app1')
-def teardown_module(module):
- kubectl_delete('crd atscachingpolicies.k8s.trafficserver.apache.com')
- kubectl_delete('-f ../ats_caching/ats-cachingpolicy-role.yaml')
- kubectl_delete('-f ../ats_caching/ats-cachingpolicy-binding.yaml')
+
+def teardown_module(module):
kubectl_delete('namespace trafficserver-test-3')
kubectl_delete('namespace trafficserver-test-2')
kubectl_delete('namespace trafficserver-test')
kubectl_delete('namespace cache-test-ns')
+ kubectl_delete('namespace backend')
+ misc_command('rm -rf certs')
+ misc_command('rm ../k8s/images/node-app-3/backend.csr')
+ misc_command('rm ../k8s/images/node-app-3/backend.crt')
+ misc_command('rm ../k8s/images/node-app-3/backend.key')
+ misc_command('rm ../k8s/images/node-app-4/origin.crt')
+ misc_command('rm ../k8s/images/node-app-4/origin.key')
+
+
+
+def get_expected_response_http2_disabled():
+ resp="""<HTML>
+ <HEAD>
+ <TITLE>Not Found on Accelerator</TITLE>
+ </HEAD>
+
+ <BODY BGCOLOR="white" FGCOLOR="black">
+ <H1>Not Found on Accelerator</H1>
+ <HR>
+
+ <FONT FACE="Helvetica,Arial"><B>
+ Description: Your request on the specified host was not found.
+ Check the location and try again.
+ </B></FONT>
+ <HR>
+ </BODY>"""
+
+ return ' '.join(resp.split())
+
def get_expected_response_app1():
resp = """<!DOCTYPE html>
<HTML>
@@ -135,6 +301,7 @@ class TestIngress:
def test_basic_routing_edge_app1(self, minikubeip):
req_url = "http://" + minikubeip + ":30080/app1"
resp = requests.get(req_url, headers={"host": "test.edge.com"})
+ time.sleep(5)
misc_command('kubectl exec $(kubectl get pod -n trafficserver-test -o name) -n trafficserver-test -- cat /opt/ats/var/log/trafficserver/squid.log')
assert resp.status_code == 200,\
@@ -176,7 +343,7 @@ class TestIngress:
assert resp.status_code == 200,\
f"Expected: 200 response code for test_basic_routing"
assert ' '.join(resp.text.split()) == get_expected_response_app2()
-
+
def test_cache_app1(self, minikubeip):
command = f'curl -i -v -H "Host: test.media.com" http://{minikubeip}:30080/cache-test'
response_1 = subprocess.run(command, shell=True, capture_output=True, text=True)
@@ -198,9 +365,31 @@ class TestIngress:
mod_time2 = resp
assert mod_time1 == mod_time2 and age1 != age2, "Expected Date provided by both responses to be same and the Age mentioned in second response to be more than 0"
+ def test_cache_https_node_app3(self, minikubeip):
+ command = f'curl -i -v --cacert certs/rootCA.crt --resolve test.example.com:30443:10.63.20.30 https://test.example.com:30443/node-app3'
+ response_1 = subprocess.run(command, shell=True, capture_output=True, text=True)
+ response1 = response_1.stdout.strip()
+ response1_list = response1.split('\n')
+ for res in response1_list:
+ if res.__contains__("age"):
+ age1 = res
+ if res.__contains__("date"):
+ mod_time1 = res
+ time.sleep(5)
+ response_2 = subprocess.run(command, shell=True, capture_output=True, text=True)
+ response2 = response_2.stdout.strip()
+ response2_list = response2.split('\n')
+ for resp in response2_list:
+ if resp.__contains__("age"):
+ age2 = resp
+ if resp.__contains__("date"):
+ mod_time2 = resp
+ assert mod_time1 == mod_time2 and age1 != age2, "Expected Date provided by both responses to be same and the Age mentioned in second response to be more than 0"
+
+
def test_cache_app1_beyond_ttl(self, minikubeip):
# waiting for cache from previous test case to expire
- time.sleep(13)
+ time.sleep(20)
command = f'curl -i -v -H "Host: test.media.com" http://{minikubeip}:30080/cache-test'
response_1 = subprocess.run(command, shell=True, capture_output=True, text=True)
@@ -219,12 +408,11 @@ class TestIngress:
if resp.__contains__("Age"):
age2 = resp
if resp.__contains__("Date"):
- mod_time2 = resp
+ mod_time2 = resp
expected_age = "Age: 0"
assert mod_time1 != mod_time2 and age1 == age2 and age2 == expected_age, "Expected Date provided by both responses should be different and the Age mentioned in both responses should be 0"
def test_cache_app2(self, minikubeip):
- # here caching will not work because url will not match with information in caching crd
command = f'curl -i -v -H "Host: test.edge.com" http://{minikubeip}:30080/app2'
response_1 = subprocess.run(command, shell=True, capture_output=True, text=True)
response1 = response_1.stdout.strip()
@@ -243,9 +431,9 @@ class TestIngress:
age2 = resp
if resp.__contains__("Date"):
mod_time2 = resp
+ kubectl_delete('crd atscachingpolicies.k8s.trafficserver.apache.com')
assert mod_time1 != mod_time2 and age1 == age2, "Expected Date provided by both the responses to be different and the Age to be 0 in both the responses"
-
def test_updating_ingress_media_app2(self, minikubeip):
kubectl_apply('data/ats-ingress-update.yaml')
req_url = "http://" + minikubeip + ":30080/app2"
@@ -284,5 +472,309 @@ class TestIngress:
assert resp.status_code == 301,\
f"Expected: 301 response code for test_snippet_edge_app2"
assert resp.headers['Location'] == 'https://test.edge.com/app2'
+
+ def test_https2_enabled(self, minikubeip):
+ kubectl_apply('../ats_sni/http2/on.yaml')
+ time.sleep(10) # wait for config changes propagate
+
+ cmd = f'curl --cacert certs/rootCA.crt -v --resolve test.edge.com:30443:{minikubeip} https://test.edge.com:30443/app2'
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
+ assert result.returncode == 0, f"Curl HTTPS/2 request failed: {result.stderr}"
+ assert "SSL connection using TLS" in result.stderr, "TLS handshake failed"
+ assert "HTTP/2 200" in result.stderr, "Expected HTTP/2 200 response"
+ expected = get_expected_response_app2()
+ actual = ' '.join(result.stdout.split()) # normalize whitespace
+ assert expected == actual, "Response body did not match expected"
+
+ def test_https2_disabled(self, minikubeip):
+ kubectl_apply('../ats_sni/http2/off.yaml')
+ time.sleep(5)
+ cmd = f'curl --cacert certs/rootCA.crt -v --resolve test.edge.com:30443:{minikubeip} https://test.edge.com:30443/app2'
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
+ assert result.returncode == 0, f"Curl HTTPS/2 request failed: {result.stderr}"
+ assert "SSL connection using TLS" in result.stderr, "TLS handshake failed"
+ assert "HTTP/1.1" in result.stderr, "Expected HTTP/1.1"
+ expected = get_expected_response_http2_disabled()
+ actual = ' '.join(result.stdout.split())
+ assert expected == actual, "Response body did not match expected"
+
+ def test_verify_client_none(self, minikubeip):
+ kubectl_apply('../ats_sni/verify-client/none.yaml')
+ time.sleep(7)
+ cmd = f'curl --cacert certs/rootCA.crt -v --resolve test.edge.com:30443:{minikubeip} https://test.edge.com:30443/app2'
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
+ assert result.returncode == 0, f"Curl failed: {result.stderr}"
+ assert "SSL connection using TLS" in result.stderr, "TLS handshake failed"
+ assert "HTTP/1.1 200 OK" in result.stderr or "HTTP/2 200" in result.stderr
+
+ def test_verify_client_moderate_without_crt(self, minikubeip):
+ kubectl_apply('../ats_sni/verify-client/moderate.yaml')
+ time.sleep(7)
+ cmd = f'curl --cacert certs/rootCA.crt -v --resolve test.edge.com:30443:{minikubeip} https://test.edge.com:30443/app2'
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
+ assert result.returncode == 0, f"Curl failed: {result.stderr}"
+ assert "SSL connection using TLS" in result.stderr, "TLS handshake failed"
+ assert "HTTP/1.1 200 OK" in result.stderr or "HTTP/2 200" in result.stderr
+
+ def test_verify_client_moderate_with_crt(self, minikubeip):
+ kubectl_apply('../ats_sni/verify-client/moderate.yaml')
+ time.sleep(7)
+ cmd = f'curl --cacert certs/rootCA.crt --cert certs/client1.crt --key certs/client1.key -v --resolve test.edge.com:30443:{minikubeip} https://test.edge.com:30443/app2'
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
+ assert result.returncode == 0, f"Curl failed: {result.stderr}"
+ assert "SSL connection using TLS" in result.stderr, "TLS handshake failed"
+ assert "HTTP/1.1 200 OK" in result.stderr or "HTTP/2 200" in result.stderr
+
+ def test_verify_client_strict_with_crt(self, minikubeip):
+ kubectl_apply('../ats_sni/verify-client/strict.yaml')
+ time.sleep(7)
+ cmd = f'curl --cacert certs/rootCA.crt --cert certs/client1.crt --key certs/client1.key -v --resolve test.edge.com:30443:{minikubeip} https://test.edge.com:30443/app2'
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
+ assert result.returncode == 0, f"Curl failed: {result.stderr}"
+ assert "SSL connection using TLS" in result.stderr, "TLS handshake failed"
+ assert "HTTP/1.1 200 OK" in result.stderr or "HTTP/2 200" in result.stderr
+
+ def test_verify_client_strict_without_crt(self, minikubeip):
+
+ kubectl_apply('../ats_sni/verify-client/strict.yaml')
+ time.sleep(7)
+ cmd = f'curl --cacert certs/rootCA.crt -v --resolve test.edge.com:30443:{minikubeip} https://test.edge.com:30443/app2'
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
+ assert result.returncode != 0, "Curl unexpectedly succeeded without client certificate"
+ expected_error = "tlsv13 alert certificate required"
+ assert expected_error in result.stderr, (
+ f"Expected TLS failure not found. stderr:\n{result.stderr}"
+ )
+
+ def test_host_sni_none(self, minikubeip):
+ kubectl_apply('../ats_sni/host-sni-policy/disabled.yaml')
+ time.sleep(7)
+ cmd = f'curl --cacert certs/rootCA.crt -v --resolve test.edge.com:30443:{minikubeip} https://test.edge.com:30443/app2'
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
+ assert result.returncode == 0, f"Curl failed: {result.stderr}"
+ assert "SSL connection using TLS" in result.stderr, "TLS handshake failed"
+ assert "HTTP/1.1 200 OK" in result.stderr or "HTTP/2 200" in result.stderr
+
+ def test_host_sni_match_enforced(self, minikubeip):
+ kubectl_apply('../ats_sni/host-sni-policy/enforced.yaml')
+ time.sleep(7)
+ cmd = f'curl --cacert certs/rootCA.crt -v --resolve test.edge.com:30443:{minikubeip} https://test.edge.com:30443/app2'
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
+ assert result.returncode == 0, f"Curl failed: {result.stderr}"
+ assert "SSL connection using TLS" in result.stderr, "TLS handshake failed"
+ assert "HTTP/1.1 200 OK" in result.stderr or "HTTP/2 200" in result.stderr
+
+ def test_host_sni_mismatch_enforced(self, minikubeip):
+ time.sleep(7)
+ cmd = (
+ f'curl -v --cacert certs/rootCA.crt '
+ f'--resolve test.example.com:30443:{minikubeip} '
+ f'https://test.example.com:30443/node-app3 '
+ f'-H "Host: test.edge.com"'
+ )
+ # Execute curl
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
+ # Validate TLS handshake and HTTP response
+ assert result.returncode == 0, f"Curl failed: {result.stderr}"
+ assert "SSL connection using TLS" in result.stderr, "TLS handshake failed"
+ assert "HTTP/2 403" in result.stderr or "HTTP/1.1 403" in result.stderr, "Expected 403 Access Denied"
+ log_cmd = (
+ "kubectl exec $(kubectl get pod -n trafficserver-test -o name | head -1) "
+ "-n trafficserver-test -- "
+ "grep -i 'SNI/hostname mismatch sni=test.example.com host=test.edge.com action=terminate' "
+ "/opt/ats/var/log/trafficserver/diags.log | sed 's/.*\\(SNI\\/hostname mismatch.*\\)/\\1/'"
+ )
+ log_result = subprocess.run(log_cmd, shell=True, capture_output=True, text=True)
+ assert "SNI/hostname mismatch sni=test.example.com host=test.edge.com action=terminate" in log_result.stdout, (
+ f"Expected log entry not found. Logs:\n{log_result.stdout}"
+ )
+
+ def test_host_sni_match_permissive(self, minikubeip):
+ kubectl_apply('../ats_sni/host-sni-policy/permissive.yaml')
+ time.sleep(7)
+ cmd = f'curl --cacert certs/rootCA.crt -v --resolve test.edge.com:30443:{minikubeip} https://test.edge.com:30443/app2'
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
+ assert result.returncode == 0, f"Curl failed: {result.stderr}"
+ assert "SSL connection using TLS" in result.stderr, "TLS handshake failed"
+ assert "HTTP/1.1 200 OK" in result.stderr or "HTTP/2 200" in result.stderr
+
+ def test_host_sni_mismatch_permissive(self, minikubeip):
+ time.sleep(7)
+ cmd = (
+ f'curl -v --cacert certs/rootCA.crt '
+ f'--resolve test.example.com:30443:{minikubeip} '
+ f'https://test.example.com:30443/node-app3 '
+ f'-H "Host: test.edge.com"'
+ )
+ # Execute curl
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
+ # Validate TLS handshake and HTTP response
+ assert result.returncode == 0, f"Curl failed: {result.stderr}"
+ assert "SSL connection using TLS" in result.stderr, "TLS handshake failed"
+ assert "HTTP/2 404" in result.stderr or "HTTP/1.1 404" in result.stderr
+ log_cmd = (
+ "kubectl exec $(kubectl get pod -n trafficserver-test -o name | head -1) "
+ "-n trafficserver-test -- "
+ "grep -i 'SNI/hostname mismatch sni=test.example.com host=test.edge.com action=continue' "
+ "/opt/ats/var/log/trafficserver/diags.log | sed 's/.*\\(SNI\\/hostname mismatch.*\\)/\\1/'"
+ )
+ log_result = subprocess.run(log_cmd, shell=True, capture_output=True, text=True)
+ assert "SNI/hostname mismatch sni=test.example.com host=test.edge.com action=continue" in log_result.stdout, (
+ f"Expected log entry not found. Logs:\n{log_result.stdout}"
+ )
+
+ # ==================== ENFORCED MODE ====================
+
+ def test_verify_server_enforced_with_valid_cert(self, minikubeip):
+ """Test ENFORCED mode with valid certificate (backend.crt) - should succeed with 200 OK"""
+ kubectl_apply('../ats_sni/verify-server-policy/enforced.yaml')
+ time.sleep(7)
+
+ cmd = f'curl -v --cacert certs/rootCA.crt --resolve test.example.com:30443:{minikubeip} https://test.example.com:30443/node-app3'
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
+ full_output = result.stdout + result.stderr
+
+ # Should succeed with 200 OK (valid cert passes strict verification)
+ assert result.returncode == 0, f"Curl failed: {result.stderr}"
+ assert "SSL connection using TLS" in full_output, "TLS handshake failed"
+ assert "HTTP/1.1 200 OK" in full_output or "HTTP/2 200" in full_output or "200 OK" in full_output, \
+ f"Expected 200 OK. Got: {result.stdout[:200]}"
+
+ print("ENFORCED mode with valid cert: 200 OK")
+
+ def test_verify_server_enforced_with_invalid_cert(self, minikubeip):
+ """Test ENFORCED mode with invalid backend certificate (origin.crt) - should fail with 502"""
+ kubectl_apply('../ats_sni/verify-server-policy/enforced.yaml')
+ time.sleep(7)
+
+ cmd = f'curl -v --cacert certs/rootCA.crt --resolve test.example.com:30443:{minikubeip} https://test.example.com:30443/node-app4'
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
+ full_output = result.stdout + result.stderr
+
+ # Should get 502 error (ENFORCED = rejects invalid cert)
+ assert result.returncode == 0, f"Curl command itself failed: {result.stderr}"
+
+ # Check for 502 error
+ has_502 = "HTTP/2 502" in full_output or "Could Not Connect" in full_output or "502 Bad Gateway" in full_output
+ assert has_502, f"Expected 502 error in ENFORCED mode with invalid cert. Got: {full_output[:500]}"
+
+ print("Got 502 error, now checking logs for Action=Terminate...")
+ # Wait for logs
+ time.sleep(10)
+ # Get pod name
+ get_pod_cmd = "kubectl get pods -n trafficserver-test -l app=trafficserver-test -o jsonpath='{.items[0].metadata.name}'"
+ pod_result = subprocess.run(get_pod_cmd, shell=True, capture_output=True, text=True)
+ pod_name = pod_result.stdout.strip().replace("'", "")
+
+ # Check logs for Action=Terminate
+ log_cmd = f"kubectl exec -n trafficserver-test {pod_name} -- grep 'Action=Terminate' /opt/ats/var/log/trafficserver/diags.log | tail -5"
+ log_result = subprocess.run(log_cmd, shell=True, capture_output=True, text=True)
+
+ if log_result.stdout:
+ print(f"Termination logs found:\n{log_result.stdout}")
+ assert "Action=Terminate" in log_result.stdout, "Expected Action=Terminate"
+
+ print("ENFORCED mode with invalid cert: 502 error (connection terminated)")
+
+ def test_verify_server_disabled_with_valid_cert(self, minikubeip):
+ """Test DISABLED mode with valid certificate (backend.crt) - should succeed with 200 OK"""
+ kubectl_apply('../ats_sni/verify-server-policy/disabled.yaml')
+ time.sleep(7)
+
+ cmd = f'curl -v --cacert certs/rootCA.crt --resolve test.example.com:30443:{minikubeip} https://test.example.com:30443/node-app3'
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
+ full_output = result.stdout + result.stderr
+
+ # Should succeed with 200 OK (DISABLED = no verification)
+ assert result.returncode == 0, f"Curl failed: {result.stderr}"
+ assert "SSL connection using TLS" in full_output, "TLS handshake failed"
+ assert "HTTP/1.1 200 OK" in full_output or "HTTP/2 200" in full_output or "200 OK" in full_output, \
+ f"Expected 200 OK. Got: {result.stdout[:200]}"
+
+ print("DISABLED mode with valid cert: 200 OK")
+
+ def test_verify_server_disabled_with_invalid_cert(self, minikubeip):
+ """Test DISABLED mode with invalid backend certificate (origin.crt) - should succeed with 200 OK"""
+ kubectl_apply('../ats_sni/verify-server-policy/disabled.yaml')
+ time.sleep(7)
+
+ cmd = f'curl -v --cacert certs/rootCA.crt --resolve test.example.com:30443:{minikubeip} https://test.example.com:30443/node-app4'
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
+ full_output = result.stdout + result.stderr
+
+ # Should ALSO succeed with 200 OK (DISABLED = no verification, accepts any cert)
+ assert result.returncode == 0, f"Curl failed: {result.stderr}"
+ assert "SSL connection using TLS" in full_output, "TLS handshake failed"
+ assert "HTTP/1.1 200 OK" in full_output or "HTTP/2 200" in full_output or "200 OK" in full_output, \
+ f"Expected 200 OK. Got: {result.stdout[:200]}"
+
+ print("DISABLED mode with invalid cert: 200 OK (no verification performed)")
+
+
+ # ==================== PERMISSIVE MODE ====================
+ def test_verify_server_permissive_with_valid_cert(self, minikubeip):
+ """Test PERMISSIVE mode with valid certificate (backend.crt) - should succeed with 200 OK"""
+ kubectl_apply('../ats_sni/verify-server-policy/permissive.yaml')
+ time.sleep(7)
+
+ cmd = f'curl -v --cacert certs/rootCA.crt --resolve test.example.com:30443:{minikubeip} https://test.example.com:30443/node-app3'
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
+ full_output = result.stdout + result.stderr
+
+ # Should succeed with 200 OK
+ assert result.returncode == 0, f"Curl failed: {result.stderr}"
+ assert "SSL connection using TLS" in full_output, "TLS handshake failed"
+ assert "HTTP/1.1 200 OK" in full_output or "HTTP/2 200" in full_output or "200 OK" in full_output, \
+ f"Expected 200 OK. Got: {result.stdout[:200]}"
+
+ print("PERMISSIVE mode with valid cert: 200 OK")
+
+
+ def test_verify_server_permissive_with_invalid_cert(self, minikubeip):
+ """Test PERMISSIVE mode with invalid backend certificate (origin.crt) - should succeed with 200 OK and log warnings"""
+ kubectl_apply('../ats_sni/verify-server-policy/permissive.yaml')
+ time.sleep(7)
+
+ # Connect to Flask on 8449 with self-signed origin.crt
+ cmd = f'curl -v --cacert certs/rootCA.crt --resolve test.example.com:30443:{minikubeip} https://test.example.com:30443/node-app4'
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
+ full_output = result.stdout + result.stderr
+
+ assert result.returncode == 0, f"Curl failed: {result.stderr}"
+ assert "SSL connection using TLS" in full_output, "TLS handshake failed"
+ assert "HTTP/1.1 200 OK" in full_output or "HTTP/2 200" in full_output or "200 OK" in full_output, \
+ f"Expected 200 OK. Got: {result.stdout[:200]}"
+
+ print("Got 200 OK, now checking logs for warnings...")
+
+ # Wait for logs to be written
+ time.sleep(10)
+
+ # Get pod name
+ get_pod_cmd = "kubectl get pods -n trafficserver-test -l app=trafficserver-test -o jsonpath='{.items[0].metadata.name}'"
+ pod_result = subprocess.run(get_pod_cmd, shell=True, capture_output=True, text=True)
+ pod_name = pod_result.stdout.strip().replace("'", "")
+
+ assert pod_name, f"TrafficServer pod not found"
+
+ # Search for warnings in logs
+ log_cmd = f"kubectl exec -n trafficserver-test {pod_name} -- grep -i 'Core server certificate verification failed' /opt/ats/var/log/trafficserver/diags.log | tail -100"
+ log_result = subprocess.run(log_cmd, shell=True, capture_output=True, text=True)
+
+ # Verify warnings are present
+ if not log_result.stdout:
+ recent_cmd = f"kubectl exec -n trafficserver-test {pod_name} -- tail -200 /opt/ats/var/log/trafficserver/diags.log"
+ recent_result = subprocess.run(recent_cmd, shell=True, capture_output=True, text=True)
+ pytest.fail(f"No warnings found in logs. Recent logs:\n{recent_result.stdout[-500:]}")
+
+ print(f"Certificate warnings found:\n{log_result.stdout}")
+
+ assert "Core server certificate verification failed" in log_result.stdout, \
+ "Expected certificate verification warning"
+ assert "Action=Continue" in log_result.stdout, \
+ "Expected 'Action=Continue' in PERMISSIVE mode"
+
+ print("PERMISSIVE mode with invalid cert: 200 OK with warnings logged")
+ kubectl_delete('crd atssnipolicies.trafficserver.apache.org')
diff --git a/watcher/handlerSni.go b/watcher/handlerSni.go
new file mode 100644
index 0000000..51a6757
--- /dev/null
+++ b/watcher/handlerSni.go
@@ -0,0 +1,282 @@
+package watcher
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "reflect"
+ "sort"
+ "sync"
+
+ "github.com/apache/trafficserver-ingress-controller/endpoint"
+ "gopkg.in/yaml.v3"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+)
+
+// AtsSniHandler handles Atssnipolicy CR events
+type AtsSniHandler struct {
+ ResourceName string
+ Ep *endpoint.Endpoint
+ FilePath string
+ mu sync.Mutex
+}
+
+// Constructor
+func NewAtsSniHandler(resource string, ep *endpoint.Endpoint, path string) *AtsSniHandler {
+ log.Println("Ats SNI Handler initialized")
+ return &AtsSniHandler{ResourceName: resource, Ep: ep, FilePath: path}
+}
+
+// SniEntry represents one fqdn entry in sni.yaml (flexible, dynamic)
+type SniEntry map[string]interface{}
+
+// nodeFromInterface creates a yaml.Node from any arbitrary Go value, preserving
+// sequences/maps/numbers/booleans instead of converting to a plain string.
+func nodeFromInterface(v interface{}) (*yaml.Node, error) {
+ var node yaml.Node
+ // First marshal the value to YAML bytes, then unmarshal into a Node to
+ // obtain a properly-typed node.
+ b, err := yaml.Marshal(v)
+ if err != nil {
+ return nil, err
+ }
+ if err := yaml.Unmarshal(b, &node); err != nil {
+ return nil, err
+ }
+ // When unmarshalling into a node the top-level is a DocumentNode; return its content.
+ if node.Kind == yaml.DocumentNode && len(node.Content) > 0 {
+ return node.Content[0], nil
+ }
+ return &node, nil
+}
+
+// Custom YAML marshaller to ensure fqdn appears first and to preserve native types
+func (s SniEntry) MarshalYAML() (interface{}, error) {
+ node := &yaml.Node{
+ Kind: yaml.MappingNode,
+ Tag: "!!map",
+ }
+
+ // Write fqdn first if present
+ if fqdn, ok := s["fqdn"]; ok {
+ keyNode := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "fqdn"}
+ valNode, err := nodeFromInterface(fqdn)
+ if err != nil {
+ // fallback to string scalar
+ valNode = &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: fmt.Sprintf("%v", fqdn)}
+ }
+ node.Content = append(node.Content, keyNode, valNode)
+ }
+
+ // Collect remaining keys (excluding fqdn)
+ keys := make([]string, 0, len(s))
+ for k := range s {
+ if k != "fqdn" {
+ keys = append(keys, k)
+ }
+ }
+
+ // Sort the other keys alphabetically for consistent output
+ sort.Strings(keys)
+
+ // Append other keys with proper YAML nodes (preserving arrays/maps/etc)
+ for _, k := range keys {
+ v := s[k]
+ keyNode := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: k}
+ valNode, err := nodeFromInterface(v)
+ if err != nil {
+ // fallback to string representation if marshalling fails
+ valNode = &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: fmt.Sprintf("%v", v)}
+ }
+ node.Content = append(node.Content, keyNode, valNode)
+ }
+
+ return node, nil
+}
+
+// SniFile represents the full sni.yaml structure
+type SniFile struct {
+ Sni []SniEntry `yaml:"sni,omitempty"`
+}
+
+// Add handles creation of Atssnipolicy
+func (h *AtsSniHandler) Add(obj interface{}) {
+ h.mu.Lock()
+ defer h.mu.Unlock()
+
+ u := obj.(*unstructured.Unstructured)
+ log.Printf("[ADD] Ats Sni Policy: #%v", u)
+
+ newSni, found, err := unstructured.NestedSlice(u.Object, "spec", "sni")
+ if err != nil || !found {
+ log.Printf("Add: sni not found or error: %v", err)
+ return
+ }
+
+ sniFile := h.loadSniFile()
+
+ for _, entry := range newSni {
+ entryMap, ok := entry.(map[string]interface{})
+ if !ok {
+ continue
+ }
+ fqdn, ok := entryMap["fqdn"].(string)
+ if !ok || fqdn == "" {
+ continue
+ }
+
+ updated := false
+ for i, existing := range sniFile.Sni {
+ if existingFqdn, _ := existing["fqdn"].(string); existingFqdn == fqdn {
+ if !reflect.DeepEqual(existing, entryMap) {
+ sniFile.Sni[i] = entryMap
+ }
+ updated = true
+ break
+ }
+ }
+ if !updated {
+ sniFile.Sni = append(sniFile.Sni, entryMap)
+ }
+ }
+
+ h.writeSniFile(sniFile)
+ h.reloadSni()
+}
+
+// Update handles updates of Atssnipolicy
+func (h *AtsSniHandler) Update(oldObj, newObj interface{}) {
+ h.mu.Lock()
+ defer h.mu.Unlock()
+
+ newU := newObj.(*unstructured.Unstructured)
+ log.Printf("[UPDATE] Atssnipolicy: #%v", newU)
+
+ newSni, found, err := unstructured.NestedSlice(newU.Object, "spec", "sni")
+ if err != nil || !found {
+ log.Printf("Update: sni not found or error: %v", err)
+ return
+ }
+
+ sniFile := h.loadSniFile()
+
+ newMap := make(map[string]SniEntry)
+ for _, entry := range newSni {
+ if m, ok := entry.(map[string]interface{}); ok {
+ if fqdn, ok := m["fqdn"].(string); ok && fqdn != "" {
+ newMap[fqdn] = m
+ }
+ }
+ }
+
+ log.Println("New Updated map in Update function ", newMap)
+ var updatedSni []SniEntry
+ seen := make(map[string]struct{})
+
+ for _, existing := range sniFile.Sni {
+ fqdn, _ := existing["fqdn"].(string)
+ if newEntry, ok := newMap[fqdn]; ok {
+ updatedSni = append(updatedSni, newEntry)
+ seen[fqdn] = struct{}{}
+ } else {
+ updatedSni = append(updatedSni, existing)
+ }
+ }
+
+ for fqdn, newEntry := range newMap {
+ if _, already := seen[fqdn]; !already {
+ updatedSni = append(updatedSni, newEntry)
+ }
+ }
+
+ sniFile.Sni = updatedSni
+ h.writeSniFile(sniFile)
+ h.reloadSni()
+}
+
+// Delete handles deletion of Atssnipolicy
+func (h *AtsSniHandler) Delete(obj interface{}) {
+ h.mu.Lock()
+ defer h.mu.Unlock()
+
+ u := obj.(*unstructured.Unstructured)
+ log.Printf("[DELETE] Atssnipolicy: #%v", u)
+
+ sniFile := h.loadSniFile()
+
+ sniList, found, err := unstructured.NestedSlice(u.Object, "spec", "sni")
+ if err != nil || !found {
+ log.Printf("Delete: sni not found or error: %v", err)
+ return
+ }
+
+ delMap := make(map[string]struct{})
+ for _, entry := range sniList {
+ if m, ok := entry.(map[string]interface{}); ok {
+ if fqdn, ok := m["fqdn"].(string); ok && fqdn != "" {
+ delMap[fqdn] = struct{}{}
+ }
+ }
+ }
+
+ var updatedSni []SniEntry
+ for _, existing := range sniFile.Sni {
+ fqdn, _ := existing["fqdn"].(string)
+ if _, toDelete := delMap[fqdn]; !toDelete {
+ updatedSni = append(updatedSni, existing)
+ }
+ }
+
+ sniFile.Sni = updatedSni
+ h.writeSniFile(sniFile)
+ h.reloadSni()
+}
+
+// loadSniFile reads existing sni.yaml
+func (h *AtsSniHandler) loadSniFile() SniFile {
+ var sniFile SniFile
+ data, err := os.ReadFile(h.FilePath)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return sniFile
+ }
+ log.Printf("Failed to read sni.yaml: %v", err)
+ return sniFile
+ }
+ if err := yaml.Unmarshal(data, &sniFile); err != nil {
+ log.Printf("Failed to unmarshal sni.yaml: %v", err)
+ }
+ return sniFile
+}
+
+// writeSniFile writes sni.yaml atomically
+func (h *AtsSniHandler) writeSniFile(sniFile SniFile) {
+ if len(sniFile.Sni) == 0 {
+ if err := os.WriteFile(h.FilePath, []byte{}, 0644); err != nil {
+ log.Printf("Failed to clear sni.yaml: %v", err)
+ }
+ return
+ }
+ data, err := yaml.Marshal(&sniFile)
+ if err != nil {
+ log.Printf("Failed to marshal sni.yaml: %v", err)
+ return
+ }
+ tmp := h.FilePath + ".tmp"
+ if err := os.WriteFile(tmp, data, 0644); err != nil {
+ log.Printf("Failed to write temp sni.yaml: %v", err)
+ return
+ }
+ _ = os.Rename(tmp, h.FilePath)
+}
+
+// reloadSni triggers ATS reload
+func (h *AtsSniHandler) reloadSni() {
+ if h.Ep != nil && h.Ep.ATSManager != nil {
+ if msg, err := h.Ep.ATSManager.SniSet(); err != nil {
+ log.Printf("Failed to reload ATS SNI: %v", err)
+ } else {
+ log.Printf("ATS SNI reloaded: %s", msg)
+ }
+ }
+}
diff --git a/watcher/handlerSni_test.go b/watcher/handlerSni_test.go
new file mode 100644
index 0000000..269dda3
--- /dev/null
+++ b/watcher/handlerSni_test.go
@@ -0,0 +1,247 @@
+package watcher
+
+import (
+ "os"
+ "path/filepath"
+ "reflect"
+ "testing"
+
+ "github.com/apache/trafficserver-ingress-controller/endpoint"
+ "github.com/apache/trafficserver-ingress-controller/proxy"
+ "gopkg.in/yaml.v3"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+)
+
+// newTestSniHandler creates a temporary AtsSniHandler for testing.
+// It overrides FilePath to point to a temp sni.yaml file.
+func newTestSniHandler(t *testing.T) (*AtsSniHandler, string) {
+ tmpDir := t.TempDir()
+ tmpFile := filepath.Join(tmpDir, "sni.yaml")
+
+ if err := os.WriteFile(tmpFile, []byte("sni:\n"), 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ ep := createExampleEndpointWithFakeATSSni()
+ h := NewAtsSniHandler("test-resource", &ep, tmpFile)
+ return h, tmpFile
+}
+
+// newSniConfig creates a CRD-like unstructured object for test
+func newSniConfig(name string, fqdns []string) *unstructured.Unstructured {
+ u := &unstructured.Unstructured{
+ Object: map[string]interface{}{
+ "apiVersion": "trafficserver.apache.org/v1alpha1",
+ "kind": "ATSSniPolicy",
+ "metadata": map[string]interface{}{
+ "name": name,
+ "namespace": "default",
+ },
+ "spec": map[string]interface{}{
+ "sni": []interface{}{},
+ },
+ },
+ }
+ var rules []interface{}
+ for _, fqdn := range fqdns {
+ rules = append(rules, map[string]interface{}{
+ "fqdn": fqdn,
+ "verify_client": "STRICT",
+ "host_sni_policy": "PERMISSIVE",
+ "valid_tls_versions_in": []interface{}{"TLSv1_2", "TLSv1_3"},
+ })
+ }
+ _ = unstructured.SetNestedSlice(u.Object, rules, "spec", "sni")
+ return u
+}
+
+// parseSniYaml parses the written YAML file into []map[string]interface{}
+func parseSniYaml(t *testing.T, path string) []map[string]interface{} {
+ data, err := os.ReadFile(path)
+ if err != nil {
+ t.Fatalf("failed to read %s: %v", path, err)
+ }
+ if len(data) == 0 {
+ return nil
+ }
+ var parsed struct {
+ Sni []map[string]interface{} `yaml:"sni"`
+ }
+ if err := yaml.Unmarshal(data, &parsed); err != nil {
+ t.Fatalf("failed to unmarshal YAML: %v\nData:\n%s", err, string(data))
+ }
+ return parsed.Sni
+}
+
+// TestAddSni verifies h.Add() adds fqdn entries into sni.yaml
+func TestAddSni(t *testing.T) {
+ h, tmpFile := newTestSniHandler(t)
+ obj := newSniConfig("my-sni-config", []string{"ats.test.com", "host-test.com"})
+
+ h.Add(obj)
+
+ entries := parseSniYaml(t, tmpFile)
+ if len(entries) != 2 {
+ t.Fatalf("expected 2 entries, got %d", len(entries))
+ }
+ for _, entry := range entries {
+ if _, ok := entry["fqdn"]; !ok {
+ t.Errorf("missing fqdn field in entry: %+v", entry)
+ }
+ }
+ verifyFqdnOrder(t, tmpFile)
+}
+
+// TestUpdateSni verifies h.Update() updates fqdn rules and removes old ones
+func TestUpdateSni(t *testing.T) {
+ h, tmpFile := newTestSniHandler(t)
+
+ oldObj := newSniConfig("my-sni-config", []string{"ats.test.com", "host-test.com"})
+ h.Add(oldObj)
+
+ newObj := newSniConfig("my-sni-config", []string{"ats.test.com", "new-host.com"})
+ h.Update(oldObj, newObj)
+
+ entries := parseSniYaml(t, tmpFile)
+ found := map[string]bool{}
+ for _, e := range entries {
+ fqdn := e["fqdn"].(string)
+ found[fqdn] = true
+ }
+
+ for _, fqdn := range []string{"ats.test.com", "new-host.com"} {
+ if !found[fqdn] {
+ t.Errorf("expected fqdn %q not found", fqdn)
+ }
+ }
+ if found["old-host.com"] {
+ t.Errorf("unexpected fqdn old-host.com found")
+ }
+ verifyFqdnOrder(t, tmpFile)
+}
+
+// TestDeleteSni verifies h.Delete() removes fqdn rules from sni.yaml
+func TestDeleteSni(t *testing.T) {
+ h, tmpFile := newTestSniHandler(t)
+
+ addObj := newSniConfig("my-sni-config", []string{"ats.test.com", "keep-me.com"})
+ h.Add(addObj)
+
+ delObj := newSniConfig("my-sni-config", []string{"ats.test.com"})
+ h.Delete(delObj)
+
+ entries := parseSniYaml(t, tmpFile)
+ if len(entries) != 1 {
+ t.Fatalf("expected 1 entry, got %d", len(entries))
+ }
+ if fqdn := entries[0]["fqdn"]; fqdn != "keep-me.com" {
+ t.Errorf("expected keep-me.com to remain, got %v", fqdn)
+ }
+
+ delObj2 := newSniConfig("my-sni-config", []string{"keep-me.com"})
+ h.Delete(delObj2)
+
+ data, _ := os.ReadFile(tmpFile)
+ if len(data) != 0 {
+ t.Errorf("expected empty sni.yaml, got:\n%s", string(data))
+ }
+}
+
+// TestLoadWriteSniFile verifies roundtrip of writeSniFile and loadSniFile
+func TestLoadWriteSniFile(t *testing.T) {
+ h, tmpFile := newTestSniHandler(t)
+
+ expected := SniFile{
+ Sni: []SniEntry{
+ {"fqdn": "abc.com", "verify_client": "STRICT"},
+ },
+ }
+ h.writeSniFile(expected)
+
+ got := h.loadSniFile()
+ if !reflect.DeepEqual(expected.Sni, got.Sni) {
+ t.Errorf("expected %+v, got %+v", expected.Sni, got.Sni)
+ }
+
+ h.writeSniFile(SniFile{})
+ data, _ := os.ReadFile(tmpFile)
+ if len(data) != 0 {
+ t.Errorf("expected empty file, got: %s", string(data))
+ }
+}
+
+// TestArrayPreservation verifies that arrays (e.g. valid_tls_versions_in) are preserved as native YAML sequences
+func TestArrayPreservation(t *testing.T) {
+ h, tmpFile := newTestSniHandler(t)
+
+ obj := newSniConfig("array-test", []string{"arr.test.com"})
+ h.Add(obj)
+
+ entries := parseSniYaml(t, tmpFile)
+ if len(entries) != 1 {
+ t.Fatalf("expected 1 entry, got %d", len(entries))
+ }
+
+ v, ok := entries[0]["valid_tls_versions_in"]
+ if !ok {
+ t.Fatalf("missing valid_tls_versions_in in entry: %+v", entries[0])
+ }
+
+ arr, ok := v.([]interface{})
+ if !ok {
+ t.Fatalf("expected valid_tls_versions_in to be []interface{}, got %T", v)
+ }
+ if len(arr) != 2 {
+ t.Fatalf("expected 2 elements in valid_tls_versions_in, got %d", len(arr))
+ }
+ if arr[0].(string) != "TLSv1_2" || arr[1].(string) != "TLSv1_3" {
+ t.Errorf("unexpected values in valid_tls_versions_in: %v", arr)
+ }
+}
+
+// verifyFqdnOrder ensures fqdn is the first key in each entry
+func verifyFqdnOrder(t *testing.T, path string) {
+ data, _ := os.ReadFile(path)
+ var root yaml.Node
+ if err := yaml.Unmarshal(data, &root); err != nil {
+ t.Fatalf("failed to parse yaml nodes: %v", err)
+ }
+
+ // Descend to the top-level mapping node (document -> mapping)
+ var mappingNode *yaml.Node
+ if root.Kind == yaml.DocumentNode && len(root.Content) > 0 {
+ mappingNode = root.Content[0]
+ } else {
+ mappingNode = &root
+ }
+
+ // mappingNode.Content contains key/value node pairs
+ for i := 0; i < len(mappingNode.Content); i += 2 {
+ keyNode := mappingNode.Content[i]
+ valNode := mappingNode.Content[i+1]
+ if keyNode.Value == "sni" && valNode.Kind == yaml.SequenceNode {
+ // iterate sequence items (each item should be a mapping node)
+ for _, item := range valNode.Content {
+ if item.Kind != yaml.MappingNode || len(item.Content) == 0 {
+ continue
+ }
+ // mapping node Content is key/value pairs; first key is at index 0
+ firstKey := item.Content[0].Value
+ if firstKey != "fqdn" {
+ t.Errorf("expected fqdn as first key, got %q in %v", firstKey, item)
+ }
+ }
+ }
+ }
+}
+
+// Fake ATS Endpoint
+func createExampleEndpointWithFakeATSSni() endpoint.Endpoint {
+ return endpoint.Endpoint{
+ ATSManager: &proxy.FakeATSManager{
+ Namespace: "default",
+ IngressClass: "",
+ Config: make(map[string]string),
+ },
+ }
+}
diff --git a/watcher/watcher.go b/watcher/watcher.go
index 1c1b956..c6b8ad2 100644
--- a/watcher/watcher.go
+++ b/watcher/watcher.go
@@ -40,6 +40,7 @@ import (
)
const CACHE_PATH string = "/opt/ats/etc/trafficserver/cache.config"
+const SNI_PATH string = "/opt/ats/etc/trafficserver/sni.yaml"
// FIXME: watching all namespace does not work...
@@ -93,6 +94,11 @@ func (w *Watcher) Watch() error {
if err := w.WatchAtsCachingPolicy(CACHE_PATH); err != nil {
return err
}
+
+ log.Println("calling the Watch Ats Sni Policy function")
+ if err := w.WatchAtsSniPolicy(SNI_PATH); err != nil {
+ return err
+ }
return nil
}
@@ -183,7 +189,7 @@ func (w *Watcher) WatchAtsCachingPolicy(path string) error {
})
if err != nil {
- return fmt.Errorf("failed to add event handler: %v\n", err)
+ return fmt.Errorf("failed to add event handler: %v", err)
}
go informer.Run(w.StopChan)
@@ -193,3 +199,26 @@ func (w *Watcher) WatchAtsCachingPolicy(path string) error {
log.Println("ATSCachingPolicy informer running and synced")
return nil
}
+
+func (w *Watcher) WatchAtsSniPolicy(path string) error {
+ gvr := schema.GroupVersionResource{Group: "trafficserver.apache.org", Version: "v1alpha1", Resource: "atssnipolicies"}
+ dynamicFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(w.DynamicClient, w.ResyncPeriod, metav1.NamespaceAll, nil)
+ informer := dynamicFactory.ForResource(gvr).Informer()
+ snihandler := NewAtsSniHandler("atssnipolicy", w.Ep, path)
+ _, err := informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
+ AddFunc: snihandler.Add,
+ UpdateFunc: snihandler.Update,
+ DeleteFunc: snihandler.Delete,
+ })
+
+ if err != nil {
+ return fmt.Errorf("failed to add event handler: %v", err)
+ }
+
+ go informer.Run(w.StopChan)
+ if !cache.WaitForCacheSync(w.StopChan, informer.HasSynced) {
+ return fmt.Errorf("failed to sync ATSSNIPolicy informer")
+ }
+ log.Println("ATSSNIPolicy informer running and synced")
+ return nil
+}
diff --git a/watcher/watcher_test.go b/watcher/watcher_test.go
index 0e8ebf9..4aa26eb 100644
--- a/watcher/watcher_test.go
+++ b/watcher/watcher_test.go
@@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"reflect"
+ "strings"
"testing"
"time"
@@ -622,3 +623,189 @@ func TestWatchAtsCachingPolicy_Delete(t *testing.T) {
t.Errorf("expected non-empty CacheSet message after delete")
}
}
+
+func getTestWatcherForSni() Watcher {
+ scheme := runtime.NewScheme()
+ gvr := schema.GroupVersionResource{
+ Group: "trafficserver.apache.org",
+ Version: "v1alpha1",
+ Resource: "atssnipolicies",
+ }
+ gvrToListKind := map[schema.GroupVersionResource]string{
+ gvr: "ATSSniPolicyList",
+ }
+ dynClient := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind)
+ clientset := fake.NewSimpleClientset()
+ exampleEndpoint := createExampleEndpointWithFakeATSSni()
+ stopChan := make(chan struct{})
+
+ sniWatcher := Watcher{
+ Cs: clientset,
+ DynamicClient: dynClient,
+ ATSNamespace: "trafficserver-test-2",
+ Ep: &exampleEndpoint,
+ StopChan: stopChan,
+ ResyncPeriod: 0,
+ }
+
+ return sniWatcher
+}
+
+func tempSniFile(t *testing.T) string {
+ tmpDir := t.TempDir()
+ tmpFile := filepath.Join(tmpDir, "sni.yaml")
+ if err := os.WriteFile(tmpFile, []byte{}, 0644); err != nil {
+ t.Fatal(err)
+ }
+ return tmpFile
+}
+
+func newSniCR(name, fqdn string) *unstructured.Unstructured {
+ return &unstructured.Unstructured{
+ Object: map[string]interface{}{
+ "apiVersion": "trafficserver.apache.org/v1alpha1",
+ "kind": "ATSSniPolicy",
+ "metadata": map[string]interface{}{
+ "name": name,
+ "namespace": "default",
+ },
+ "spec": map[string]interface{}{
+ "sni": []interface{}{
+ map[string]interface{}{
+ "fqdn": fqdn,
+ "verify_client": "STRICT",
+ "host_sni_policy": "PERMISSIVE",
+ "valid_tls_versions_in": []interface{}{
+ "TLSv1_2",
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+// --- TESTS ---
+
+func TestWatchAtsSniPolicy_Add(t *testing.T) {
+ w := getTestWatcherForSni()
+ path := tempSniFile(t)
+
+ err := w.WatchAtsSniPolicy(path)
+ if err != nil {
+ t.Fatalf("failed to start watcher: %v", err)
+ }
+
+ gvr := schema.GroupVersionResource{
+ Group: "trafficserver.apache.org",
+ Version: "v1alpha1",
+ Resource: "atssnipolicies",
+ }
+ dynClient := w.DynamicClient.Resource(gvr).Namespace("default")
+
+ // Create CR
+ cr := newSniCR("policy-add", "ats.test.com")
+ _, err = dynClient.Create(context.TODO(), cr, meta_v1.CreateOptions{})
+ if err != nil {
+ t.Fatalf("failed to create SNI CR: %v", err)
+ }
+ time.Sleep(200 * time.Millisecond)
+
+ // File must contain fqdn
+ data, _ := os.ReadFile(path)
+ if !strings.Contains(string(data), "ats.test.com") {
+ t.Errorf("expected fqdn ats.test.com in sni.yaml, got:\n%s", string(data))
+ }
+}
+
+func TestWatchAtsSniPolicy_Update(t *testing.T) {
+ w := getTestWatcherForSni()
+ path := tempSniFile(t)
+
+ err := w.WatchAtsSniPolicy(path)
+ if err != nil {
+ t.Fatalf("failed to start watcher: %v", err)
+ }
+
+ gvr := schema.GroupVersionResource{
+ Group: "trafficserver.apache.org",
+ Version: "v1alpha1",
+ Resource: "atssnipolicies",
+ }
+ dynClient := w.DynamicClient.Resource(gvr).Namespace("default")
+
+ // Create CR with fqdn
+ cr := newSniCR("policy-update", "ats.test.com")
+ _, err = dynClient.Create(context.TODO(), cr, meta_v1.CreateOptions{})
+ if err != nil {
+ t.Fatalf("failed to create SNI CR: %v", err)
+ }
+
+ // Update CR: keep ats.test.com, add new-site.com
+ cr.Object["spec"] = map[string]interface{}{
+ "sni": []interface{}{
+ map[string]interface{}{
+ "fqdn": "ats.test.com",
+ "verify_client": "NONE",
+ "host_sni_policy": "ENFORCE",
+ },
+ map[string]interface{}{
+ "fqdn": "new-site.com",
+ "verify_client": "NONE",
+ "host_sni_policy": "ENFORCE",
+ },
+ },
+ }
+ _, err = dynClient.Update(context.TODO(), cr, meta_v1.UpdateOptions{})
+ if err != nil {
+ t.Fatalf("failed to update SNI CR: %v", err)
+ }
+ time.Sleep(200 * time.Millisecond)
+
+ // File should contain ats.test.com and new-site.com, but not host-test.com
+ data, _ := os.ReadFile(path)
+ content := string(data)
+ if !strings.Contains(content, "ats.test.com") {
+ t.Errorf("expected fqdn ats.test.com in sni.yaml after update, got:\n%s", content)
+ }
+ if !strings.Contains(content, "new-site.com") {
+ t.Errorf("expected fqdn new-site.com in sni.yaml after update, got:\n%s", content)
+ }
+}
+
+func TestWatchAtsSniPolicy_Delete(t *testing.T) {
+ w := getTestWatcherForSni()
+ path := tempSniFile(t)
+
+ err := w.WatchAtsSniPolicy(path)
+ if err != nil {
+ t.Fatalf("failed to start watcher: %v", err)
+ }
+
+ gvr := schema.GroupVersionResource{
+ Group: "trafficserver.apache.org",
+ Version: "v1alpha1",
+ Resource: "atssnipolicies",
+ }
+ dynClient := w.DynamicClient.Resource(gvr).Namespace("default")
+
+ // Create CR with fqdn
+ cr := newSniCR("policy-delete", "ats.test.com")
+ _, err = dynClient.Create(context.TODO(), cr, meta_v1.CreateOptions{})
+ if err != nil {
+ t.Fatalf("failed to create SNI CR: %v", err)
+ }
+
+ // Delete CR
+ err = dynClient.Delete(context.TODO(), "policy-delete", meta_v1.DeleteOptions{})
+ if err != nil {
+ t.Fatalf("failed to delete SNI CR: %v", err)
+ }
+ time.Sleep(200 * time.Millisecond)
+
+ // File should be empty, because both fqdn entries came from the deleted CR
+ data, _ := os.ReadFile(path)
+ if len(strings.TrimSpace(string(data))) != 0 {
+ t.Errorf("expected sni.yaml to be empty after delete, got:\n%s", string(data))
+ }
+}