Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import (
"fmt"
"os"
"path/filepath"
"strings"

// Load CoreDNS + p2p-forge plugins
_ "github.com/ipshipyard/p2p-forge/plugins"
Expand Down Expand Up @@ -38,13 +41,108 @@
clog.Debugf("updated directives: %v", dnsserver.Directives)
}

// shouldShowUsageGuidance detects when Corefile is missing p2p-forge plugins
func shouldShowUsageGuidance() bool {
return shouldShowUsageGuidanceWithOptions(os.Args[1:], ".")
}

// shouldShowUsageGuidanceWithOptions is a testable version that accepts custom args and working directory
func shouldShowUsageGuidanceWithOptions(args []string, workDir string) bool {
// Check if -conf flag is explicitly provided
confFile := getConfigFileFromArgs(args)
if confFile == "" {
// No explicit config, check if default Corefile exists
defaultCorefile := filepath.Join(workDir, "Corefile")
if _, err := os.Stat(defaultCorefile); os.IsNotExist(err) {
// No Corefile at all - show guidance
return true
}
confFile = defaultCorefile
} else if !filepath.IsAbs(confFile) {
// Make relative path absolute based on working directory
confFile = filepath.Join(workDir, confFile)
}

// Check if the config file is missing required p2p-forge plugins
return isMissingP2PForgePlugins(confFile)
}

// getConfigFile returns the config file path from command line args
func getConfigFile() string {

Check failure on line 71 in main.go

View workflow job for this annotation

GitHub Actions / go-check / All

func getConfigFile is unused (U1000)

Check failure on line 71 in main.go

View workflow job for this annotation

GitHub Actions / go-check / All

func getConfigFile is unused (U1000)
return getConfigFileFromArgs(os.Args[1:])
}

// getConfigFileFromArgs is a testable version that accepts custom args
func getConfigFileFromArgs(args []string) string {
for i, arg := range args {
if arg == "-conf" && i+1 < len(args) {
return args[i+1]
}
}
return ""
}

// isMissingP2PForgePlugins checks if the config file is missing acme or ipparser plugins
func isMissingP2PForgePlugins(filename string) bool {
content, err := os.ReadFile(filename)
if err != nil {
// If we can't read the file, let CoreDNS handle the error
return false
}

configStr := string(content)

// Simple check for plugins not in comments
// Split by lines and check each line for plugins not preceded by #
hasAcme := false
hasIPParser := false

lines := strings.Split(configStr, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
// Skip comment lines
if strings.HasPrefix(line, "#") {
continue
}
// Check for plugins (look for word boundaries to avoid partial matches)
if strings.Contains(line, "acme") {
hasAcme = true
}
if strings.Contains(line, "ipparser") {
hasIPParser = true
}
}

// Show guidance if missing either essential plugin
return !hasAcme || !hasIPParser
}

func main() {
fmt.Printf("%s %s\n", name, version) // always print version
registerVersionMetric()
err := godotenv.Load()
if err == nil {
fmt.Println(".env found and loaded")
}

// Check for common misconfiguration before running CoreDNS
if shouldShowUsageGuidance() {
fmt.Fprintf(os.Stderr, "\nError: Configuration issue detected.\n\n")
fmt.Fprintf(os.Stderr, "p2p-forge requires a Corefile with 'acme' and 'ipparser' plugins.\n")
fmt.Fprintf(os.Stderr, "This error occurs when running without a proper Corefile or with\n")
fmt.Fprintf(os.Stderr, "a generic CoreDNS config missing p2p-forge-specific plugins.\n\n")
fmt.Fprintf(os.Stderr, "For detailed usage instructions, see: https://github.com/ipshipyard/p2p-forge#usage\n\n")
fmt.Fprintf(os.Stderr, "Local development:\n")
fmt.Fprintf(os.Stderr, " ./p2p-forge -conf Corefile.local-dev -dns.port 5354\n\n")
fmt.Fprintf(os.Stderr, "Local Docker development:\n")
fmt.Fprintf(os.Stderr, " docker build -t p2p-forge-dev . && docker run --rm -it --net=host p2p-forge-dev\n")
fmt.Fprintf(os.Stderr, " docker run --rm -it --net=host -v ./Corefile.local-dev:/p2p-forge/Corefile.local-dev p2p-forge-dev -conf /p2p-forge/Corefile.local-dev -dns.port 5354\n\n")
fmt.Fprintf(os.Stderr, "Production deployment:\n")
fmt.Fprintf(os.Stderr, " ./p2p-forge -conf Corefile\n")
fmt.Fprintf(os.Stderr, " (Note: Use production-appropriate Corefile, not Corefile.local-dev)\n\n")
os.Exit(1)
}

coremain.Run()
}

Expand Down
260 changes: 260 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
package main

import (
"os"
"path/filepath"
"testing"
)

func TestGetConfigFile(t *testing.T) {
t.Parallel()

tests := []struct {
name string
args []string
expected string
}{
{
name: "no arguments",
args: []string{},
expected: "",
},
{
name: "conf flag with file",
args: []string{"-conf", "Corefile.test"},
expected: "Corefile.test",
},
{
name: "conf flag in middle of args",
args: []string{"-dns.port", "5354", "-conf", "Corefile.local-dev", "-other", "flag"},
expected: "Corefile.local-dev",
},
{
name: "conf flag at end without value",
args: []string{"-dns.port", "5354", "-conf"},
expected: "",
},
{
name: "other flags only",
args: []string{"-dns.port", "5354", "-log.level", "debug"},
expected: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

result := getConfigFileFromArgs(tt.args)
if result != tt.expected {
t.Errorf("getConfigFileFromArgs() = %q, expected %q", result, tt.expected)
}
})
}
}

func TestIsMissingP2PForgePlugins(t *testing.T) {
t.Parallel()

tests := []struct {
name string
corefileContent string
expected bool
}{
{
name: "valid corefile with both plugins",
corefileContent: `libp2p.direct {
log
errors
ipparser libp2p.direct
acme libp2p.direct {
registration-domain registration.libp2p.direct
database-type badger test.db
}
}`,
expected: false,
},
{
name: "missing acme plugin",
corefileContent: `libp2p.direct {
log
errors
ipparser libp2p.direct
}`,
expected: true,
},
{
name: "missing ipparser plugin",
corefileContent: `libp2p.direct {
log
errors
acme libp2p.direct {
registration-domain registration.libp2p.direct
database-type badger test.db
}
}`,
expected: true,
},
{
name: "missing both plugins",
corefileContent: `libp2p.direct {
log
errors
}`,
expected: true,
},
{
name: "empty file",
corefileContent: "",
expected: true,
},
{
name: "plugins mentioned in comments only",
corefileContent: `libp2p.direct {
log
errors
# acme libp2p.direct
# ipparser libp2p.direct
}`,
expected: true,
},
{
name: "plugins in different server blocks",
corefileContent: `server1 {
ipparser example.com
}
server2 {
acme example.com
}`,
expected: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// Create temporary file
tmpDir := t.TempDir()
tmpFile := filepath.Join(tmpDir, "Corefile.test")

err := os.WriteFile(tmpFile, []byte(tt.corefileContent), 0644)
if err != nil {
t.Fatalf("Failed to create test file: %v", err)
}

result := isMissingP2PForgePlugins(tmpFile)
if result != tt.expected {
t.Errorf("isMissingP2PForgePlugins() = %v, expected %v", result, tt.expected)
}
})
}
}

func TestIsMissingP2PForgePlugins_NonExistentFile(t *testing.T) {
t.Parallel()

// Test with non-existent file - should return false to let CoreDNS handle the error
result := isMissingP2PForgePlugins("/path/that/does/not/exist")
if result != false {
t.Errorf("isMissingP2PForgePlugins() with non-existent file = %v, expected false", result)
}
}

func TestShouldShowUsageGuidance(t *testing.T) {
t.Parallel()

tests := []struct {
name string
args []string
createCorefile bool
corefileContent string
createDefaultFile bool
expected bool
}{
{
name: "no Corefile exists, no explicit config",
args: []string{},
createCorefile: false,
createDefaultFile: false,
expected: true,
},
{
name: "valid default Corefile exists",
args: []string{},
createCorefile: true,
createDefaultFile: false,
corefileContent: "libp2p.direct {\n ipparser libp2p.direct\n acme libp2p.direct\n}",
expected: false,
},
{
name: "invalid default Corefile exists",
args: []string{},
createCorefile: true,
createDefaultFile: false,
corefileContent: "libp2p.direct {\n log\n errors\n}",
expected: true,
},
{
name: "explicit valid config file",
args: []string{"-conf", "Corefile.test"},
createCorefile: false,
createDefaultFile: true,
corefileContent: "libp2p.direct {\n ipparser libp2p.direct\n acme libp2p.direct\n}",
expected: false,
},
{
name: "explicit invalid config file",
args: []string{"-conf", "Corefile.test"},
createCorefile: false,
createDefaultFile: true,
corefileContent: "libp2p.direct {\n log\n}",
expected: true,
},
{
name: "explicit non-existent config file",
args: []string{"-conf", "nonexistent.conf"},
createCorefile: false,
createDefaultFile: false,
expected: false, // Let CoreDNS handle the missing file error
},
{
name: "explicit absolute path config file",
args: []string{"-conf", "/absolute/path/Corefile"},
createCorefile: false,
createDefaultFile: false,
expected: false, // Absolute paths should not be modified
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

// Create temporary directory for test files
tmpDir := t.TempDir()

// Create files as needed using absolute paths in temp directory
if tt.createCorefile {
corefilePath := filepath.Join(tmpDir, "Corefile")
err := os.WriteFile(corefilePath, []byte(tt.corefileContent), 0644)
if err != nil {
t.Fatalf("Failed to create Corefile: %v", err)
}
}

if tt.createDefaultFile {
filename := filepath.Join(tmpDir, "Corefile.test") // matches the -conf argument in test cases
err := os.WriteFile(filename, []byte(tt.corefileContent), 0644)
if err != nil {
t.Fatalf("Failed to create test config file: %v", err)
}
}

// Use the new testable function with custom working directory
result := shouldShowUsageGuidanceWithOptions(tt.args, tmpDir)
if result != tt.expected {
t.Errorf("shouldShowUsageGuidanceWithOptions() = %v, expected %v", result, tt.expected)
}
})
}
}
Loading