enhance: streamline sandbox configuration and include handling #1421

This commit is contained in:
0xJacky
2025-11-11 02:02:40 +00:00
parent c4bd485c4f
commit d6cdf476a9
2 changed files with 300 additions and 368 deletions

View File

@@ -2,6 +2,7 @@ package nginx
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"regexp"
@@ -12,16 +13,6 @@ import (
"github.com/uozi-tech/cosy/logger"
)
// Site represents minimal site info needed for sandbox testing
type SandboxSite struct {
Path string
}
// Stream represents minimal stream info needed for sandbox testing
type SandboxStream struct {
Path string
}
// NamespaceInfo represents minimal namespace info for sandbox
type NamespaceInfo struct {
ID uint64
@@ -44,6 +35,11 @@ func SandboxTestConfigWithPaths(namespace *NamespaceInfo, sitePaths, streamPaths
return "Config validation skipped for remote-only namespace", nil
}
// If namespace is nil, directly test in real directory (no sandbox)
if namespace == nil {
return TestConfig()
}
// Create sandbox and test
sandbox, err := createSandbox(namespace, sitePaths, streamPaths)
if err != nil {
@@ -81,14 +77,31 @@ func createSandbox(namespace *NamespaceInfo, sitePaths, streamPaths []string) (*
Namespace: namespace,
}
// Copy necessary directories to sandbox for complete isolation
if err := copySandboxDependencies(tempDir); err != nil {
// Copy full nginx conf directory to sandbox, excluding sites-* and streams-*
if err := copyConfigBaseExceptSitesStreams(tempDir); err != nil {
os.RemoveAll(tempDir)
return nil, fmt.Errorf("failed to copy sandbox dependencies: %w", err)
return nil, fmt.Errorf("failed to copy base configs: %w", err)
}
// Ensure sandbox sub-directories exist for selected includes
if err := os.MkdirAll(filepath.Join(tempDir, "sites-enabled"), 0755); err != nil {
os.RemoveAll(tempDir)
return nil, fmt.Errorf("failed to create sandbox sites-enabled: %w", err)
}
if err := os.MkdirAll(filepath.Join(tempDir, "streams-enabled"), 0755); err != nil {
os.RemoveAll(tempDir)
return nil, fmt.Errorf("failed to create sandbox streams-enabled: %w", err)
}
// Collect and copy only enabled sites/streams for the given namespace
siteFiles, streamFiles, err := collectAndCopyNamespaceEnabled(namespace, sitePaths, streamPaths, tempDir)
if err != nil {
os.RemoveAll(tempDir)
return nil, fmt.Errorf("failed to collect/copy namespace configs: %w", err)
}
// Generate sandbox nginx.conf
configContent, err := generateSandboxConfig(namespace, sitePaths, streamPaths, tempDir)
configContent, err := generateSandboxConfig(namespace, siteFiles, streamFiles, tempDir)
if err != nil {
os.RemoveAll(tempDir)
return nil, fmt.Errorf("failed to generate sandbox config: %w", err)
@@ -105,73 +118,6 @@ func createSandbox(namespace *NamespaceInfo, sitePaths, streamPaths []string) (*
return sandbox, nil
}
// copySandboxDependencies copies necessary config directories to sandbox
func copySandboxDependencies(sandboxDir string) error {
confBase := GetConfPath()
// Directories to copy for complete isolation
dirsToCopy := []string{
"conf.d",
"modules-enabled",
"snippets", // Common nginx snippets directory
}
for _, dir := range dirsToCopy {
srcDir := filepath.Join(confBase, dir)
dstDir := filepath.Join(sandboxDir, dir)
// Check if source directory exists
if !helper.FileExists(srcDir) {
continue // Skip non-existent directories
}
// Create destination directory
if err := os.MkdirAll(dstDir, 0755); err != nil {
return fmt.Errorf("failed to create %s: %w", dir, err)
}
// Copy all files from source to destination
entries, err := os.ReadDir(srcDir)
if err != nil {
logger.Warnf("Failed to read %s: %v, skipping", srcDir, err)
continue
}
for _, entry := range entries {
if entry.IsDir() {
continue // Skip subdirectories for now
}
srcFile := filepath.Join(srcDir, entry.Name())
dstFile := filepath.Join(dstDir, entry.Name())
content, err := os.ReadFile(srcFile)
if err != nil {
logger.Warnf("Failed to read %s: %v, skipping", srcFile, err)
continue
}
if err := os.WriteFile(dstFile, content, 0644); err != nil {
logger.Warnf("Failed to write %s: %v, skipping", dstFile, err)
continue
}
}
logger.Debugf("Copied %s to sandbox", dir)
}
// Also copy mime.types if exists
mimeTypes := filepath.Join(confBase, "mime.types")
if helper.FileExists(mimeTypes) {
content, err := os.ReadFile(mimeTypes)
if err == nil {
os.WriteFile(filepath.Join(sandboxDir, "mime.types"), content, 0644)
}
}
return nil
}
// Cleanup removes the sandbox directory
func (s *Sandbox) Cleanup() {
if s.Dir != "" {
@@ -184,7 +130,7 @@ func (s *Sandbox) Cleanup() {
}
// generateSandboxConfig generates a minimal nginx.conf that only includes configs from specified paths
func generateSandboxConfig(namespace *NamespaceInfo, sitePaths, streamPaths []string, sandboxDir string) (string, error) {
func generateSandboxConfig(namespace *NamespaceInfo, siteFiles, streamFiles []string, sandboxDir string) (string, error) {
// Read the main nginx.conf to get basic structure
mainConfPath := GetConfEntryPath()
mainConf, err := os.ReadFile(mainConfPath)
@@ -195,42 +141,24 @@ func generateSandboxConfig(namespace *NamespaceInfo, sitePaths, streamPaths []st
mainConfStr := string(mainConf)
// Generate include patterns based on provided paths
var includePatterns []string
// Add site includes
for _, sitePath := range sitePaths {
siteEnabledPath := GetConfPath("sites-enabled", filepath.Base(sitePath))
if helper.FileExists(siteEnabledPath) {
includePatterns = append(includePatterns, fmt.Sprintf(" include %s;", siteEnabledPath))
}
siteIncludeLines := make([]string, 0, len(siteFiles))
for _, f := range siteFiles {
siteIncludeLines = append(siteIncludeLines, fmt.Sprintf(" include %s;", filepath.Join(sandboxDir, "sites-enabled", f)))
}
// Add stream includes
for _, streamPath := range streamPaths {
streamEnabledPath := GetConfPath("streams-enabled", filepath.Base(streamPath))
if helper.FileExists(streamEnabledPath) {
includePatterns = append(includePatterns, fmt.Sprintf(" include %s;", streamEnabledPath))
}
}
// If no paths provided, test all enabled configs (original behavior)
if len(includePatterns) == 0 {
sitesEnabledDir := GetConfPath("sites-enabled")
streamsEnabledDir := GetConfPath("streams-enabled")
includePatterns = append(includePatterns, fmt.Sprintf(" include %s/*;", sitesEnabledDir))
includePatterns = append(includePatterns, fmt.Sprintf(" include %s/*;", streamsEnabledDir))
streamIncludeLines := make([]string, 0, len(streamFiles))
for _, f := range streamFiles {
streamIncludeLines = append(streamIncludeLines, fmt.Sprintf(" include %s;", filepath.Join(sandboxDir, "streams-enabled", f)))
}
// Replace include directives with sandbox-specific ones
sandboxConf := replaceIncludeDirectives(mainConfStr, includePatterns, sandboxDir)
sandboxConf := replaceIncludeDirectives(mainConfStr, sandboxDir, siteIncludeLines, streamIncludeLines)
return sandboxConf, nil
}
// replaceIncludeDirectives replaces only sites-enabled and streams-enabled includes
// Rewrites other includes (conf.d, mime.types, etc.) to use sandbox paths
func replaceIncludeDirectives(mainConf string, includePatterns []string, sandboxDir string) string {
// Rewrites other includes to point to copied files under sandboxDir, preserving isolation.
func replaceIncludeDirectives(mainConf string, sandboxDir string, siteIncludeLines, streamIncludeLines []string) string {
lines := strings.Split(mainConf, "\n")
var result []string
insideHTTP := false
@@ -263,28 +191,22 @@ func replaceIncludeDirectives(mainConf string, includePatterns []string, sandbox
// Add our sandbox-specific includes at the first occurrence
if insideHTTP && isSitesEnabled && !httpIncludesAdded {
result = append(result, " # Sandbox-specific includes (generated for isolated testing)")
for _, pattern := range includePatterns {
if strings.Contains(pattern, "sites-enabled") {
result = append(result, pattern)
}
}
result = append(result, siteIncludeLines...)
httpIncludesAdded = true
}
if insideStream && isStreamsEnabled && !streamIncludesAdded {
result = append(result, " # Sandbox-specific includes (generated for isolated testing)")
for _, pattern := range includePatterns {
if strings.Contains(pattern, "streams-enabled") {
result = append(result, pattern)
}
}
result = append(result, streamIncludeLines...)
streamIncludesAdded = true
}
continue // Skip the original include line
}
// Rewrite other includes to use sandbox paths
rewrittenLine := rewriteIncludePath(line, sandboxDir)
result = append(result, rewrittenLine)
// Rewrite includes to sandbox paths
normalized := rewriteIncludeLineToSandbox(line, sandboxDir)
if normalized != "" {
result = append(result, normalized)
}
continue
}
@@ -294,11 +216,7 @@ func replaceIncludeDirectives(mainConf string, includePatterns []string, sandbox
// Add includes before closing http block if not added yet
if !httpIncludesAdded {
result = append(result, " # Sandbox-specific includes (generated for isolated testing)")
for _, pattern := range includePatterns {
if strings.Contains(pattern, "sites-enabled") {
result = append(result, pattern)
}
}
result = append(result, siteIncludeLines...)
httpIncludesAdded = true
}
insideHTTP = false
@@ -307,11 +225,7 @@ func replaceIncludeDirectives(mainConf string, includePatterns []string, sandbox
// Add includes before closing stream block if not added yet
if !streamIncludesAdded {
result = append(result, " # Sandbox-specific includes (generated for isolated testing)")
for _, pattern := range includePatterns {
if strings.Contains(pattern, "streams-enabled") {
result = append(result, pattern)
}
}
result = append(result, streamIncludeLines...)
streamIncludesAdded = true
}
insideStream = false
@@ -324,37 +238,207 @@ func replaceIncludeDirectives(mainConf string, includePatterns []string, sandbox
return strings.Join(result, "\n")
}
// rewriteIncludePath rewrites include paths to use sandbox directory
func rewriteIncludePath(line, sandboxDir string) string {
// Extract the include path using regex
// Match: include /path/to/file; or include /path/*.conf;
includeRegex := regexp.MustCompile(`include\s+([^;]+);`)
// rewriteIncludeLineToSandbox rewrites include lines to point to files/directories inside sandboxDir.
// If an include path is relative, it will be rewritten relative to the nginx conf dir inside sandbox.
func rewriteIncludeLineToSandbox(line string, sandboxDir string) string {
includeRegex := regexp.MustCompile(`(?i)include\s+([^;#]+);`)
matches := includeRegex.FindStringSubmatch(line)
if len(matches) < 2 {
return line // No match, return original
return line
}
path := strings.TrimSpace(matches[1])
origPath := strings.TrimSpace(matches[1])
confBase := GetConfPath()
var rewritten string
if filepath.IsAbs(path) {
// If absolute under confBase, map to sandbox
if helper.IsUnderDirectory(path, confBase) {
rel, err := filepath.Rel(confBase, path)
if err == nil {
rewritten = filepath.Join(sandboxDir, rel)
}
}
} else {
// Relative includes should point inside sandbox conf root
rewritten = filepath.Join(sandboxDir, path)
}
if rewritten == "" {
rewritten = path
}
trimmed := includeRegex.ReplaceAllString(line, "include "+rewritten+";")
return trimmed
}
// Paths to rewrite to sandbox
rewritePaths := map[string]string{
filepath.Join(confBase, "conf.d"): filepath.Join(sandboxDir, "conf.d"),
filepath.Join(confBase, "modules-enabled"): filepath.Join(sandboxDir, "modules-enabled"),
filepath.Join(confBase, "snippets"): filepath.Join(sandboxDir, "snippets"),
filepath.Join(confBase, "mime.types"): filepath.Join(sandboxDir, "mime.types"),
// collectAndCopyNamespaceEnabled collects and copies enabled site/stream configs based on provided paths.
// It rewrites relative includes to absolute, and writes them into sandboxDir/{sites-enabled,streams-enabled}.
// Returns the written file names.
func collectAndCopyNamespaceEnabled(_ *NamespaceInfo, sitePaths, streamPaths []string, sandboxDir string) (siteFiles, streamFiles []string, err error) {
// Helper to process and write a single config by kind and name
readSourceAndWrite := func(kind, name string) (writtenName string, wErr error) {
var enabledCandidates []string
switch kind {
case "site":
enabledCandidates = []string{
GetConfSymlinkPath(GetConfPath("sites-enabled", name)),
GetConfPath("sites-enabled", name),
}
case "stream":
enabledCandidates = []string{
GetConfSymlinkPath(GetConfPath("streams-enabled", name)),
GetConfPath("streams-enabled", name),
}
}
var enabledPath string
for _, cand := range enabledCandidates {
if helper.FileExists(cand) {
enabledPath = cand
break
}
}
if enabledPath == "" {
return "", nil // not enabled, skip silently
}
// Determine source file: prefer the symlink target if possible; fallback to *-available
srcPath := enabledPath
if fi, lErr := os.Lstat(enabledPath); lErr == nil && (fi.Mode()&os.ModeSymlink) != 0 {
if target, rErr := os.Readlink(enabledPath); rErr == nil {
// If target is relative, resolve against enabled dir
if !filepath.IsAbs(target) {
target = filepath.Join(filepath.Dir(enabledPath), target)
}
srcPath = target
}
}
if kind == "site" && !helper.FileExists(srcPath) {
srcPath = GetConfPath("sites-available", name)
}
if kind == "stream" && !helper.FileExists(srcPath) {
srcPath = GetConfPath("streams-available", name)
}
content, rErr := os.ReadFile(srcPath)
if rErr != nil {
return "", fmt.Errorf("read %s content %s: %w", kind, srcPath, rErr)
}
// Rewrite include lines to sandbox paths (resolve relative to source dir first)
absRewriter := regexp.MustCompile(`(?m)^[ \t]*include\s+([^;#]+);`)
rewritten := absRewriter.ReplaceAllStringFunc(string(content), func(m string) string {
return normalizeIncludeLineRelativeTo(m, filepath.Dir(srcPath), sandboxDir)
})
// Compute destination file name respecting platform symlink naming
var destName string
switch kind {
case "site":
destName = filepath.Base(GetConfSymlinkPath(GetConfPath("sites-enabled", name)))
case "stream":
destName = filepath.Base(GetConfSymlinkPath(GetConfPath("streams-enabled", name)))
}
destDir := filepath.Join(sandboxDir, kind+"s-enabled")
if err := os.WriteFile(filepath.Join(destDir, destName), []byte(rewritten), 0644); err != nil {
return "", fmt.Errorf("write sandbox %s: %w", kind, err)
}
return destName, nil
}
// Check if path starts with any of the rewrite paths
newPath := origPath
for oldPrefix, newPrefix := range rewritePaths {
if strings.HasPrefix(origPath, oldPrefix) {
newPath = strings.Replace(origPath, oldPrefix, newPrefix, 1)
break
// Process sites based on provided sitePaths
for _, sp := range sitePaths {
name := filepath.Base(sp)
if written, wErr := readSourceAndWrite("site", name); wErr != nil {
return nil, nil, wErr
} else if written != "" {
siteFiles = append(siteFiles, written)
}
}
// Replace in the original line
return strings.Replace(line, origPath, newPath, 1)
// Process streams based on provided streamPaths
for _, st := range streamPaths {
name := filepath.Base(st)
if written, wErr := readSourceAndWrite("stream", name); wErr != nil {
return nil, nil, wErr
} else if written != "" {
streamFiles = append(streamFiles, written)
}
}
return siteFiles, streamFiles, nil
}
// normalizeIncludeLineRelativeTo rewrites a single include line:
// - resolves relative paths against baseDir
// - if the resolved path is under confBase, map to sandboxDir mirror; else keep as is
func normalizeIncludeLineRelativeTo(line, baseDir, sandboxDir string) string {
includeRegex := regexp.MustCompile(`(?i)include\s+([^;#]+);`)
matches := includeRegex.FindStringSubmatch(line)
if len(matches) < 2 {
return line
}
path := strings.TrimSpace(matches[1])
// If relative, make absolute to source file dir
resolved := path
if !filepath.IsAbs(resolved) {
resolved = filepath.Clean(filepath.Join(baseDir, resolved))
}
confBase := GetConfPath()
if helper.IsUnderDirectory(resolved, confBase) {
if rel, err := filepath.Rel(confBase, resolved); err == nil {
resolved = filepath.Join(sandboxDir, rel)
}
}
return includeRegex.ReplaceAllString(line, "include "+resolved+";")
}
// copyConfigBaseExceptSitesStreams copies the entire nginx conf directory into sandboxDir,
// excluding any paths under sites-* and streams-* and skipping the entry nginx.conf (we generate our own).
func copyConfigBaseExceptSitesStreams(sandboxDir string) error {
confBase := GetConfPath()
entry := GetConfEntryPath()
copyFile := func(src, dst string, mode fs.FileMode) error {
parent := filepath.Dir(dst)
if err := os.MkdirAll(parent, 0755); err != nil {
return err
}
data, err := os.ReadFile(src)
if err != nil {
return err
}
return os.WriteFile(dst, data, 0644)
}
return filepath.WalkDir(confBase, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
rel, rErr := filepath.Rel(confBase, path)
if rErr != nil {
return rErr
}
if rel == "." {
return nil
}
// Skip blacklisted directories
if d.IsDir() {
base := filepath.Base(path)
if strings.HasPrefix(base, "sites-") || strings.HasPrefix(base, "streams-") {
return filepath.SkipDir
}
// Create directory in sandbox
return os.MkdirAll(filepath.Join(sandboxDir, rel), 0755)
}
// Skip entry nginx.conf to avoid overwriting generated file
if path == entry && filepath.Base(path) == "nginx.conf" {
return nil
}
// Copy regular file (follow symlinks by reading content)
dst := filepath.Join(sandboxDir, rel)
info, sErr := os.Lstat(path)
if sErr != nil {
return sErr
}
return copyFile(path, dst, info.Mode())
})
}

View File

@@ -1,254 +1,102 @@
package nginx
import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
func TestCreateSandbox(t *testing.T) {
namespaceInfo := &NamespaceInfo{
ID: 1,
Name: "test-namespace",
func TestNormalizeIncludeLineRelativeTo(t *testing.T) {
baseDir := "/etc/nginx/sites-available"
if runtime.GOOS == "windows" {
// keep test portable; filepath.Join will use OS-specific separator
baseDir = `C:\nginx\conf\sites-available`
}
sandboxDir := "/tmp/sbx"
sitePaths := []string{"site1.conf", "site2.conf"}
streamPaths := []string{"stream1.conf"}
sandbox, err := createSandbox(namespaceInfo, sitePaths, streamPaths)
if err != nil {
t.Fatalf("Failed to create sandbox: %v", err)
}
defer sandbox.Cleanup()
// Verify sandbox directory exists
if _, err := os.Stat(sandbox.Dir); os.IsNotExist(err) {
t.Errorf("Sandbox directory does not exist: %s", sandbox.Dir)
}
// Verify config file exists
if _, err := os.Stat(sandbox.ConfigPath); os.IsNotExist(err) {
t.Errorf("Sandbox config file does not exist: %s", sandbox.ConfigPath)
}
// Verify namespace info
if sandbox.Namespace.ID != 1 {
t.Errorf("Expected namespace ID 1, got %d", sandbox.Namespace.ID)
}
}
func TestSandboxCleanup(t *testing.T) {
sandbox, err := createSandbox(nil, []string{}, []string{})
if err != nil {
t.Fatalf("Failed to create sandbox: %v", err)
}
sandboxDir := sandbox.Dir
// Cleanup
sandbox.Cleanup()
// Verify directory is removed
if _, err := os.Stat(sandboxDir); !os.IsNotExist(err) {
t.Errorf("Sandbox directory still exists after cleanup: %s", sandboxDir)
}
}
func TestGenerateSandboxConfig(t *testing.T) {
// Skip this test as it requires mocking GetConfEntryPath
// The logic is tested in TestReplaceIncludeDirectives instead
t.Skip("Skipping - requires dependency injection refactoring")
}
func TestReplaceIncludeDirectives(t *testing.T) {
tests := []struct {
name string
mainConf string
includePatterns []string
expectContains []string
expectNotContain []string
name string
in string
wantPrefix string
}{
{
name: "Replace HTTP includes",
mainConf: `http {
include /etc/nginx/sites-enabled/*;
}`,
includePatterns: []string{
" include /etc/nginx/sites-enabled/site1.conf;",
" include /etc/nginx/sites-enabled/site2.conf;",
},
expectContains: []string{
"include /etc/nginx/sites-enabled/site1.conf",
"include /etc/nginx/sites-enabled/site2.conf",
"Sandbox-specific includes",
},
expectNotContain: []string{
"include /etc/nginx/sites-enabled/*",
},
name: "relative simple file",
in: " include mime.types;",
wantPrefix: " include ",
},
{
name: "Replace Stream includes",
mainConf: `stream {
include /etc/nginx/streams-enabled/*;
}`,
includePatterns: []string{
" include /etc/nginx/streams-enabled/stream1.conf;",
},
expectContains: []string{
"include /etc/nginx/streams-enabled/stream1.conf",
"Sandbox-specific includes",
},
expectNotContain: []string{
"include /etc/nginx/streams-enabled/*",
},
},
{
name: "Rewrite other includes to sandbox",
mainConf: `http {
include /etc/nginx/mime.types;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}`,
includePatterns: []string{
" include /etc/nginx/sites-enabled/site1.conf;",
},
expectContains: []string{
"include /tmp/test-sandbox/mime.types", // Rewritten to sandbox
"include /tmp/test-sandbox/conf.d/*.conf", // Rewritten to sandbox
"include /etc/nginx/sites-enabled/site1.conf",
},
expectNotContain: []string{
"include /etc/nginx/sites-enabled/*",
"include /etc/nginx/mime.types", // Should be rewritten
"include /etc/nginx/conf.d/*.conf", // Should be rewritten
},
name: "relative path with subdir",
in: "include ../common/snippets/*.conf;",
wantPrefix: "include ",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sandboxDir := "/tmp/test-sandbox"
result := replaceIncludeDirectives(tt.mainConf, tt.includePatterns, sandboxDir)
for _, expected := range tt.expectContains {
if !strings.Contains(result, expected) {
t.Errorf("Expected result to contain %q, but it doesn't.\nResult:\n%s", expected, result)
}
out := normalizeIncludeLineRelativeTo(tt.in, baseDir, sandboxDir)
if out == "" {
t.Fatalf("expected non-empty include, got empty")
}
for _, notExpected := range tt.expectNotContain {
if strings.Contains(result, notExpected) {
t.Errorf("Expected result NOT to contain %q, but it does.\nResult:\n%s", notExpected, result)
if !strings.HasPrefix(out, tt.wantPrefix) {
t.Fatalf("unexpected prefix: %q, got %q", tt.wantPrefix, out)
}
// if relative input (first two cases), ensure absolute joined path appears
if tt.name == "relative simple file" || tt.name == "relative path with subdir" {
parts := strings.Split(out, "include ")
if len(parts) < 2 {
t.Fatalf("malformed include line: %q", out)
}
pathWithSemi := parts[1]
path := strings.TrimSuffix(pathWithSemi, ";")
if !filepath.IsAbs(path) {
t.Fatalf("expected absolute path, got %q", path)
}
}
})
}
}
func TestReplaceIncludeDirectivesEdgeCases(t *testing.T) {
t.Run("Empty include patterns", func(t *testing.T) {
mainConf := `http {
include /etc/nginx/sites-enabled/*;
}`
result := replaceIncludeDirectives(mainConf, []string{}, "/tmp/test-sandbox")
// Should still add comment but no includes
if !strings.Contains(result, "Sandbox-specific includes") {
t.Error("Expected sandbox comment even with empty patterns")
}
})
t.Run("No http or stream blocks", func(t *testing.T) {
mainConf := `events {
worker_connections 1024;
}`
includePatterns := []string{" include /etc/nginx/sites-enabled/site1.conf;"}
result := replaceIncludeDirectives(mainConf, includePatterns, "/tmp/test-sandbox")
// Should preserve original config
if !strings.Contains(result, "worker_connections 1024") {
t.Error("Original config not preserved when no http/stream blocks")
}
})
t.Run("Nested braces", func(t *testing.T) {
mainConf := `http {
server {
location / {
return 200;
}
}
include /etc/nginx/sites-enabled/*;
}`
includePatterns := []string{" include /etc/nginx/sites-enabled/site1.conf;"}
result := replaceIncludeDirectives(mainConf, includePatterns, "/tmp/test-sandbox")
// Should preserve nested structure
if !strings.Contains(result, "location /") {
t.Error("Nested location directive not preserved")
}
// Should replace include
if strings.Contains(result, "include /etc/nginx/sites-enabled/*") {
t.Error("Generic include should be replaced even with nested braces")
}
})
}
func TestSandboxTestConfigWithPaths(t *testing.T) {
// Skip this integration test - requires nginx installation and proper setup
t.Skip("Skipping integration test - requires nginx binary and proper configuration")
}
func BenchmarkCreateSandbox(b *testing.B) {
namespaceInfo := &NamespaceInfo{
ID: 1,
Name: "bench-namespace",
}
sitePaths := []string{"site1.conf", "site2.conf", "site3.conf"}
streamPaths := []string{"stream1.conf"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
sandbox, err := createSandbox(namespaceInfo, sitePaths, streamPaths)
if err != nil {
b.Fatalf("Failed to create sandbox: %v", err)
}
sandbox.Cleanup()
}
}
func BenchmarkReplaceIncludeDirectives(b *testing.B) {
func TestReplaceIncludeDirectives(t *testing.T) {
mainConf := `
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
server {
listen 80;
server_name default;
}
include mime.types;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
stream {
include /etc/nginx/streams-enabled/*;
}
`
includePatterns := []string{
" include /etc/nginx/sites-enabled/site1.conf;",
" include /etc/nginx/sites-enabled/site2.conf;",
" include /etc/nginx/sites-enabled/site3.conf;",
" include /etc/nginx/streams-enabled/stream1.conf;",
}
siteLines := []string{" include /tmp/sbx/sites-enabled/a.conf;"}
streamLines := []string{" include /tmp/sbx/streams-enabled/s1.conf;"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = replaceIncludeDirectives(mainConf, includePatterns, "/tmp/test-sandbox")
out := replaceIncludeDirectives(mainConf, "/tmp/sbx", siteLines, streamLines)
if strings.Contains(out, "/etc/nginx/sites-enabled/*") {
t.Fatal("sites-enabled wildcard should be replaced by sandbox files")
}
if !strings.Contains(out, "/tmp/sbx/sites-enabled/a.conf;") {
t.Fatal("sandbox site include missing")
}
if strings.Contains(out, "/etc/nginx/streams-enabled/*") {
t.Fatal("streams-enabled wildcard should be replaced by sandbox files")
}
if !strings.Contains(out, "/tmp/sbx/streams-enabled/s1.conf;") {
t.Fatal("sandbox stream include missing")
}
// mime.types should be kept (possibly normalized)
if !strings.Contains(strings.ToLower(out), "include") {
t.Fatal("expected include directives to remain")
}
}