Skip to content

Commit 69bb649

Browse files
authored
add config file watch + Helm chart enhancements (#1)
* add two ways of watching config file changes * one using the watchdog module based on filesystem events * and another one based on polling with Path.stat() to make it work with K8S mounted configmap's * extend Helm chart functionality * add secret with public Certificate Authorities * add additionalTrustedCA values to extend list of trusted CA * add ability to specify list of environment variables * disable livenessProbe and readynessProbe * adjust pod resource limits * few additional minor fixes
1 parent 07cf6e2 commit 69bb649

9 files changed

Lines changed: 3598 additions & 46 deletions

File tree

chart/Chart.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ type: application
1515
# This is the chart version. This version number should be incremented each time you make changes
1616
# to the chart and its templates, including the app version.
1717
# Versions are expected to follow Semantic Versioning (https://semver.org/)
18-
version: 0.0.1
18+
version: 0.0.6
1919

2020
# This is the version number of the application being deployed. This version number should be
2121
# incremented each time you make changes to the application. Versions are not expected to
2222
# follow Semantic Versioning. They should reflect the version the application is using.
2323
# It is recommended to use it with quotes.
24-
appVersion: "0.0.1"
24+
appVersion: "master"

chart/files/cacert.pem

Lines changed: 3480 additions & 0 deletions
Large diffs are not rendered by default.

chart/templates/configmap.yaml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@ apiVersion: v1
22
kind: ConfigMap
33
metadata:
44
name: {{ .Release.Name }}-config
5-
namespace: {{ .Release.Namespace }}
65
{{- if .Values.config.inlineData }}
76
data:
8-
{{.Values.config.inlineData | indent 2}}
7+
{{ .Values.config.inlineData | indent 2 }}
98
{{- end }}
109
{{- if .Values.config.inlineBinaryData }}
1110
binaryData:
12-
{{.Values.config.inlineBinaryData | indent 2}}
11+
{{ .Values.config.inlineBinaryData | indent 2 }}
1312
{{- end }}

chart/templates/deployment.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
apiVersion: apps/v1
22
kind: Deployment
33
metadata:
4-
name: {{ include "nsi-auth.fullname" . }}
4+
name: {{ .Release.Name }}
55
labels:
66
{{- include "nsi-auth.labels" . | nindent 4 }}
77
spec:
@@ -46,12 +46,12 @@ spec:
4646
{{- toYaml .Values.readinessProbe | nindent 12 }}
4747
resources:
4848
{{- toYaml .Values.resources | nindent 12 }}
49-
{{- with .Values.volumeMounts }}
5049
envFrom:
5150
{{- if .Values.env }}
5251
- configMapRef:
53-
name: { { .Release.Name } }-environment
52+
name: {{ .Release.Name }}-environment
5453
{{- end }}
54+
{{- with .Values.volumeMounts }}
5555
volumeMounts:
5656
{{- toYaml . | nindent 12 }}
5757
{{- end }}

chart/templates/environment-configmap.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ metadata:
99
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
1010
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
1111
name: {{ .Release.Name }}-environment
12-
namespace: {{ .Release.Namespace }}
1312
{{- with .Values.env }}
1413
data:
1514
{{- toYaml . | nindent 8 }}

chart/templates/secret.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
apiVersion: v1
2+
kind: Secret
3+
type: Opaque
4+
metadata:
5+
name: {{ .Release.Name }}-ca
6+
data:
7+
ca.crt: {{ printf "%s%s" (.Files.Get "files/cacert.pem") .Values.config.additionalTrustedCA | b64enc }}

chart/templates/service.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
apiVersion: v1
22
kind: Service
33
metadata:
4-
name: {{ include "nsi-auth.fullname" . }}
5-
namespace: {{ .Release.Namespace }}
4+
name: {{ .Release.Name }}
65
labels:
76
{{- include "nsi-auth.labels" . | nindent 4 }}
87
spec:

chart/values.yaml

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -66,20 +66,20 @@ resources: {}
6666
# resources, such as Minikube. If you do want to specify resources, uncomment the following
6767
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
6868
# limits:
69-
# cpu: 100m
70-
# memory: 128Mi
69+
# cpu: 10m
70+
# memory: 64Mi
7171
# requests:
72-
# cpu: 100m
72+
# cpu: 1000m
7373
# memory: 128Mi
7474

7575
livenessProbe:
76-
httpGet:
77-
path: /validate
78-
port: http
76+
#httpGet:
77+
# path: /validate
78+
# port: http
7979
readinessProbe:
80-
httpGet:
81-
path: /validate
82-
port: http
80+
#httpGet:
81+
# path: /validate
82+
# port: http
8383

8484
autoscaling:
8585
enabled: false
@@ -89,17 +89,17 @@ autoscaling:
8989
# targetMemoryUtilizationPercentage: 80
9090

9191
# Additional volumes on the output Deployment definition.
92-
volumes: []
93-
# - name: foo
94-
# secret:
95-
# secretName: mysecret
96-
# optional: false
92+
volumes:
93+
- name: config
94+
configMap:
95+
name: test-config
96+
optional: false
9797

9898
# Additional volumeMounts on the output Deployment definition.
99-
volumeMounts: []
100-
# - name: foo
101-
# mountPath: "/etc/foo"
102-
# readOnly: true
99+
volumeMounts:
100+
- name: config
101+
mountPath: "/config"
102+
readOnly: true
103103

104104
nodeSelector: {}
105105

@@ -119,3 +119,29 @@ config:
119119
CN=CertB,OU=Dept Y,O=Company 2,C=NL
120120
CN=CertC,OU=Dept Z,O=Company 3,C=NL
121121
inlineBinaryData:
122+
# additionalTrustedCA: |-
123+
#
124+
# subject=C=AU, ST=Some-State, O=My Organisation, CN=My self signed root CA
125+
# =========================================================================
126+
# -----BEGIN CERTIFICATE-----
127+
# MIIDmzCCAoOgAwIBAgIUYzV2fb5GNklmz1Wb/tcn3GprN0MwDQYJKoZIhvcNAQEL
128+
# BQAwXTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxGDAWBgNVBAoM
129+
# D015IE9yZ2FuaXNhdGlvbjEfMB0GA1UEAwwWTXkgc2VsZiBzaWduZWQgcm9vdCBD
130+
# QTAeFw0yNTA4MDcxMTM0MTlaFw0zNTA4MDcxMTM0MTlaMF0xCzAJBgNVBAYTAkFV
131+
# MRMwEQYDVQQIDApTb21lLVN0YXRlMRgwFgYDVQQKDA9NeSBPcmdhbmlzYXRpb24x
132+
# HzAdBgNVBAMMFk15IHNlbGYgc2lnbmVkIHJvb3QgQ0EwggEiMA0GCSqGSIb3DQEB
133+
# AQUAA4IBDwAwggEKAoIBAQCl6IIW7UwZbzzEqVy4ACpAAbcu20dBYZB/+ApLl6QP
134+
# Sq+P2UkP4MqJ/JxAjeTzHpyBbQlMnmsA9OFIblR1Gj2EjMfwYp1+QapbppEjzYEJ
135+
# KihVl8Mo+mQ7GBCdmVMKtvxM8yeafO07LwArVDDjA92EfdonO/8cYStQWtvcTPd8
136+
# ZRm4udx7sYap2X0bGJALBqbPDpiPqc9BY8ZaGLtIyE97VR/S0786dLzWgKocwPOD
137+
# U1ESjJq31WmsWASs2UNmHE1sdzn1vq08umoumGfkuchRSlDDsgz++WoKnZVtEop1
138+
# YlmNU9bFlw1OfLCosCfyjLDRM8hSm5EaKWe0KRXMtJiFAgMBAAGjUzBRMB0GA1Ud
139+
# DgQWBBTOqbjm/j/R+UeHKqSPl3TvUOjzhTAfBgNVHSMEGDAWgBTOqbjm/j/R+UeH
140+
# KqSPl3TvUOjzhTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA9
141+
# PglxaqamA3nyyenhAQX0JMB7LkUVrWNWOlxVAG86U1dN5HZUhzd16A/5HMxIHQSJ
142+
# 3nQueVSF3nI6gR3NcMcFs/VLpXkeJGeQSQ46ZpqAv6MHkrmFuBmAyf20wixD3Osp
143+
# KSBhR21ouU5GWhJhsl+FnGg9R+/HbrjjU3lJiIDkHZr8Bv3t0qhzat3CA8ea6wId
144+
# smP/bZtO0PL7iRAec/TnIXaWOfP3t8PwM9sWPjEf+g2IAIlPSp0MHSMobh2IwNxV
145+
# Qg2gs7DxSatssgBvTjyAG6zlpWidjfhvIvq3tsa9wDhe3xoWFrZPiN7Ela3Mc2nj
146+
# VwXOCCu9/2/MxG79jRDJ
147+
# -----END CERTIFICATE-----

nsi_auth.py

Lines changed: 59 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@
1111
# See the License for the specific language governing permissions and
1212
# limitations under the License.
1313
"""Verify DN from HTTP header against list of allowed DN's."""
14-
14+
import threading
1515
from logging.config import dictConfig
16+
from typing import Callable
1617

1718
from flask import Flask, request
1819
from pydantic import BaseModel, FilePath
1920
from pydantic_settings import BaseSettings
20-
from watchdog.events import DirModifiedEvent, FileModifiedEvent, FileSystemEventHandler
21+
from watchdog.events import FileModifiedEvent, FileSystemEvent, FileSystemEventHandler
2122
from watchdog.observers import Observer
2223

2324

@@ -29,6 +30,8 @@ class Settings(BaseSettings):
2930

3031
allowed_client_subject_dn_path: FilePath = FilePath("/config/allowed_client_dn.txt")
3132
ssl_client_subject_dn_header: str = "ssl-client-subject-dn"
33+
use_watchdog: bool = False
34+
log_level: str = "INFO"
3235

3336

3437
class State(BaseModel):
@@ -59,7 +62,7 @@ def init_app() -> Flask:
5962
}
6063
)
6164
app = Flask(__name__)
62-
app.logger.setLevel("DEBUG")
65+
app.logger.setLevel(settings.log_level)
6366

6467
return app
6568

@@ -83,23 +86,66 @@ def validate() -> tuple[str, int]:
8386

8487

8588
#
86-
# Load DN from file plus watchdog for auto reload.
89+
# File watch based on watchdog.
8790
#
8891
class FileChangeHandler(FileSystemEventHandler):
8992
"""On filesystem event, call load_allowed_client_dn() when `filepath` is modified."""
9093

91-
def __init__(self, filepath: FilePath) -> None:
94+
def __init__(self, filepath: FilePath, callback: Callable[[FilePath], None]) -> None:
9295
"""Set the filepath of the file to watch."""
93-
self.filepath = filepath.resolve()
96+
self.filepath = filepath
97+
self.callback = callback
9498
load_allowed_client_dn(self.filepath)
9599
app.logger.info(f"watch {self.filepath} for changes")
96100

97-
def on_modified(self, event: DirModifiedEvent | FileModifiedEvent) -> None:
101+
def on_modified(self, event: FileSystemEvent) -> None:
98102
"""Call load_allowed_client_dn() when `filepath` is modified."""
99-
if FilePath(str(event.src_path)).resolve() == self.filepath:
100-
load_allowed_client_dn(self.filepath)
103+
app.logger.debug(f"on_modified {event} {FilePath(str(event.src_path)).resolve()} {self.filepath.resolve()}")
104+
if FilePath(str(event.src_path)).resolve() == self.filepath.resolve():
105+
self.callback(self.filepath)
106+
107+
108+
def watchdog_file(filepath: FilePath, callback: Callable[[FilePath], None]) -> None:
109+
"""Setup watchdog to watch directory that the file resides in and call handler on change."""
110+
observer = Observer()
111+
observer.schedule(
112+
FileChangeHandler(filepath, callback),
113+
path=str(filepath.parent),
114+
recursive=True,
115+
event_filter=[FileModifiedEvent],
116+
)
117+
observer.start()
118+
119+
120+
#
121+
# File watch based on Path.stat().
122+
#
123+
def watch_file(filepath: FilePath, callback: Callable[[FilePath], None]) -> None:
124+
"""Watch modification time of `filepath` in a thread and call `callback` on change."""
125+
126+
def watch() -> None:
127+
"""If modification time of `filepath` changes call `callback`."""
128+
last_modified = 0
129+
app.logger.info(f"watch {filepath} for changes")
130+
while True:
131+
app.logger.debug(f"check modification time of {filepath}")
132+
try:
133+
modified = filepath.stat().st_mtime_ns
134+
except FileNotFoundError as e:
135+
app.logger.error(f"cannot get last modification time of {filepath}: {e!s}")
136+
else:
137+
if last_modified < modified:
138+
last_modified = modified
139+
callback(filepath)
140+
event.wait(5)
141+
142+
event = threading.Event()
143+
threading.Thread(target=watch, daemon=True).start()
101144

102145

146+
#
147+
# Load DN from file.
148+
#
103149
def load_allowed_client_dn(filepath: FilePath) -> None:
104150
"""Load list of allowed client DN from file."""
105151
try:
@@ -113,11 +159,7 @@ def load_allowed_client_dn(filepath: FilePath) -> None:
113159
app.logger.info(f"load {len(new_allowed_client_subject_dn)} DN from {filepath}")
114160

115161

116-
def watch_file(file_to_watch: FilePath) -> None:
117-
"""Setup watchdog to watch directory that the file resides in and call handler on change."""
118-
observer = Observer()
119-
observer.schedule(FileChangeHandler(file_to_watch), path=str(file_to_watch.parent), recursive=False)
120-
observer.start()
121-
122-
123-
watch_file(settings.allowed_client_subject_dn_path)
162+
if settings.use_watchdog:
163+
watchdog_file(settings.allowed_client_subject_dn_path, load_allowed_client_dn)
164+
else:
165+
watch_file(settings.allowed_client_subject_dn_path, load_allowed_client_dn)

0 commit comments

Comments
 (0)