Skip to content

Commit e0cec2f

Browse files
committed
feat(nginx): add support for dynamically loaded modules and clear modules cache #1136
1 parent fe8953d commit e0cec2f

File tree

4 files changed

+149
-5
lines changed

4 files changed

+149
-5
lines changed

.devcontainer/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ RUN apt-get update && \
55
apt-get install -y --no-install-recommends curl gnupg2 ca-certificates lsb-release ubuntu-keyring jq cloc software-properties-common && \
66
\
77
# Add PPA repository for nginx-extras
8-
add-apt-repository -y ppa:ondrej/nginx-mainline && \
8+
add-apt-repository -y ppa:ondrej/nginx && \
99
\
1010
# Update package information and install Nginx-extras
1111
apt-get update && \

.github/workflows/build.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,16 @@ jobs:
274274
files: |
275275
${{ env.DIST }}.tar.gz
276276
${{ env.DIST }}.tar.gz.digest
277+
278+
- name: Set up nodejs
279+
uses: actions/setup-node@v4
280+
with:
281+
node-version: current
282+
283+
- name: Install dependencies
284+
run: |
285+
corepack enable
286+
corepack prepare pnpm@latest --activate
277287
278288
- name: Upload to R2
279289
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/dev'

internal/nginx/modules.go

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ func clearModulesCache() {
4242
lastPIDSize = 0
4343
}
4444

45+
// ClearModulesCache clears the modules cache (public version for external use)
46+
func ClearModulesCache() {
47+
clearModulesCache()
48+
}
49+
4550
// isPIDFileChanged checks if the PID file has changed since the last check
4651
func isPIDFileChanged() bool {
4752
pidPath := GetPIDPath()
@@ -82,6 +87,43 @@ func updatePIDFileInfo() {
8287
}
8388
}
8489

90+
// addLoadedDynamicModules discovers modules loaded via load_module statements
91+
// that might not be present in the configure arguments (e.g., externally installed modules)
92+
func addLoadedDynamicModules() {
93+
// Get nginx -T output to find load_module statements
94+
out := getNginxT()
95+
if out == "" {
96+
return
97+
}
98+
99+
// Use the shared regex function to find loaded dynamic modules
100+
loadModuleRe := GetLoadModuleRegex()
101+
matches := loadModuleRe.FindAllStringSubmatch(out, -1)
102+
103+
modulesCacheLock.Lock()
104+
defer modulesCacheLock.Unlock()
105+
106+
for _, match := range matches {
107+
if len(match) > 1 {
108+
// Extract the module name from load_module statement and normalize it
109+
loadModuleName := match[1]
110+
normalizedName := normalizeModuleNameFromLoadModule(loadModuleName)
111+
112+
// Check if this module is already in our cache
113+
if _, exists := modulesCache.Get(normalizedName); !exists {
114+
// This is a module that's loaded but not in configure args
115+
// Add it as a dynamic module that's loaded
116+
modulesCache.Set(normalizedName, &Module{
117+
Name: normalizedName,
118+
Params: "",
119+
Dynamic: true, // Loaded via load_module, so it's dynamic
120+
Loaded: true, // We found it in load_module statements, so it's loaded
121+
})
122+
}
123+
}
124+
}
125+
}
126+
85127
// updateDynamicModulesStatus checks which dynamic modules are actually loaded in the running Nginx
86128
func updateDynamicModulesStatus() {
87129
modulesCacheLock.Lock()
@@ -279,6 +321,9 @@ func GetModules() *orderedmap.OrderedMap[string, *Module] {
279321

280322
modulesCacheLock.Unlock()
281323

324+
// Also check for modules loaded via load_module statements that might not be in configure args
325+
addLoadedDynamicModules()
326+
282327
// Update dynamic modules status by checking if they're actually loaded
283328
updateDynamicModulesStatus()
284329

@@ -290,10 +335,8 @@ func GetModules() *orderedmap.OrderedMap[string, *Module] {
290335

291336
// IsModuleLoaded checks if a module is loaded in Nginx
292337
func IsModuleLoaded(module string) bool {
293-
// Ensure modules are in the cache
294-
if modulesCache.Len() == 0 {
295-
GetModules()
296-
}
338+
// Get fresh modules to ensure we have the latest state
339+
GetModules()
297340

298341
modulesCacheLock.RLock()
299342
defer modulesCacheLock.RUnlock()

internal/nginx/modules_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,97 @@ func TestRealWorldModuleMapping(t *testing.T) {
284284
}
285285
}
286286

287+
func TestAddLoadedDynamicModules(t *testing.T) {
288+
// Test scenario: modules loaded via load_module but not in configure args
289+
// This simulates the real-world case where external modules are installed
290+
// and loaded dynamically without being compiled into nginx
291+
292+
// We can't directly test addLoadedDynamicModules since it depends on getNginxT()
293+
// But we can test the logic by simulating the behavior
294+
295+
testLoadModuleOutput := `
296+
# Configuration file /etc/nginx/modules-enabled/50-mod-stream.conf:
297+
load_module modules/ngx_stream_module.so;
298+
# Configuration file /etc/nginx/modules-enabled/70-mod-stream-geoip2.conf:
299+
load_module modules/ngx_stream_geoip2_module.so;
300+
load_module "modules/ngx_http_geoip2_module.so";
301+
`
302+
303+
// Test the regex and normalization logic
304+
loadModuleRe := GetLoadModuleRegex()
305+
matches := loadModuleRe.FindAllStringSubmatch(testLoadModuleOutput, -1)
306+
307+
expectedModules := map[string]bool{
308+
"stream": false,
309+
"stream_geoip2": false,
310+
"http_geoip2": false,
311+
}
312+
313+
t.Logf("Found %d load_module matches", len(matches))
314+
315+
for _, match := range matches {
316+
if len(match) > 1 {
317+
loadModuleName := match[1]
318+
normalizedName := normalizeModuleNameFromLoadModule(loadModuleName)
319+
320+
t.Logf("Load module: %s -> normalized: %s", loadModuleName, normalizedName)
321+
322+
if _, expected := expectedModules[normalizedName]; expected {
323+
expectedModules[normalizedName] = true
324+
} else {
325+
t.Errorf("Unexpected module found: %s (from %s)", normalizedName, loadModuleName)
326+
}
327+
}
328+
}
329+
330+
// Check that all expected modules were found
331+
for moduleName, found := range expectedModules {
332+
if !found {
333+
t.Errorf("Expected module %s was not found", moduleName)
334+
}
335+
}
336+
}
337+
338+
func TestExternalModuleDiscovery(t *testing.T) {
339+
// Test the complete normalization pipeline for external modules
340+
testCases := []struct {
341+
name string
342+
loadModuleName string
343+
expectedResult string
344+
}{
345+
{
346+
name: "stream_geoip2 module",
347+
loadModuleName: "ngx_stream_geoip2_module",
348+
expectedResult: "stream_geoip2",
349+
},
350+
{
351+
name: "http_geoip2 module",
352+
loadModuleName: "ngx_http_geoip2_module",
353+
expectedResult: "http_geoip2",
354+
},
355+
{
356+
name: "custom third-party module",
357+
loadModuleName: "ngx_http_custom_module",
358+
expectedResult: "http_custom",
359+
},
360+
{
361+
name: "simple module name",
362+
loadModuleName: "ngx_custom_module",
363+
expectedResult: "custom",
364+
},
365+
}
366+
367+
for _, tc := range testCases {
368+
t.Run(tc.name, func(t *testing.T) {
369+
result := normalizeModuleNameFromLoadModule(tc.loadModuleName)
370+
if result != tc.expectedResult {
371+
t.Errorf("normalizeModuleNameFromLoadModule(%s) = %s, expected %s",
372+
tc.loadModuleName, result, tc.expectedResult)
373+
}
374+
})
375+
}
376+
}
377+
287378
func TestGetModuleMapping(t *testing.T) {
288379
// This test verifies that GetModuleMapping function works without errors
289380
// Since it depends on nginx being available, we'll just test that it doesn't panic

0 commit comments

Comments
 (0)