198 lines
5.2 KiB
Go
198 lines
5.2 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"net/http"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
SearXNGContainerName = "searxng-mcp-local"
|
|
SearXNGImage = "searxng/searxng:latest"
|
|
SearXNGInternalPort = "8080"
|
|
)
|
|
|
|
type DockerManager struct {
|
|
searxngPort string
|
|
searxngImage string
|
|
autostart bool
|
|
stopOnExit bool
|
|
}
|
|
|
|
func NewDockerManager(searxngPort, searxngImage string, autostart, stopOnExit bool) *DockerManager {
|
|
return &DockerManager{
|
|
searxngPort: searxngPort,
|
|
searxngImage: searxngImage,
|
|
autostart: autostart,
|
|
stopOnExit: stopOnExit,
|
|
}
|
|
}
|
|
|
|
func (dm *DockerManager) Close() error {
|
|
if dm.stopOnExit {
|
|
return dm.StopSearXNG()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (dm *DockerManager) runDocker(args ...string) (string, string, error) {
|
|
cmd := exec.Command("docker", args...)
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
err := cmd.Run()
|
|
return stdout.String(), stderr.String(), err
|
|
}
|
|
|
|
func (dm *DockerManager) IsDockerAvailable() bool {
|
|
_, _, err := dm.runDocker("version")
|
|
return err == nil
|
|
}
|
|
|
|
func (dm *DockerManager) IsSearXNGRunning() bool {
|
|
stdout, _, err := dm.runDocker("ps", "-q", "-f", "name="+SearXNGContainerName)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return strings.TrimSpace(stdout) != ""
|
|
}
|
|
|
|
func (dm *DockerManager) GetSearXNGHealth() bool {
|
|
url := fmt.Sprintf("http://127.0.0.1:%s/config", dm.searxngPort)
|
|
client := &http.Client{Timeout: 2 * time.Second}
|
|
resp, err := client.Get(url)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer resp.Body.Close()
|
|
return resp.StatusCode == http.StatusOK
|
|
}
|
|
|
|
func (dm *DockerManager) StartSearXNG() error {
|
|
if !dm.autostart {
|
|
return fmt.Errorf("auto-start disabled")
|
|
}
|
|
|
|
// Validate port is within valid range
|
|
var portNum int
|
|
_, err := fmt.Sscanf(dm.searxngPort, "%d", &portNum)
|
|
if err != nil || portNum < 1 || portNum > 65535 {
|
|
return fmt.Errorf("invalid port: must be between 1 and 65535, got %s", dm.searxngPort)
|
|
}
|
|
|
|
// Check if container exists but stopped
|
|
stdout, _, _ := dm.runDocker("ps", "-a", "-q", "-f", "name="+SearXNGContainerName)
|
|
if strings.TrimSpace(stdout) != "" {
|
|
// Container exists, start it
|
|
_, _, err := dm.runDocker("start", SearXNGContainerName)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to start existing SearXNG container: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Pull image in background (don't wait)
|
|
go dm.pullImage()
|
|
|
|
// Create and start new container with a random secret
|
|
// Generate a random secret (SearXNG requires at least 16 chars)
|
|
secret := fmt.Sprintf("mcp-search-%d-secret-key-local", time.Now().Unix())
|
|
|
|
// Create and start new container with JSON API enabled
|
|
// Use environment variables to configure SearXNG
|
|
_, stderr, err := dm.runDocker(
|
|
"run",
|
|
"-d",
|
|
"--name", SearXNGContainerName,
|
|
"-p", "127.0.0.1:"+dm.searxngPort+":"+SearXNGInternalPort,
|
|
"--restart", "unless-stopped",
|
|
"-e", "SEARXNG_SECRET="+secret,
|
|
dm.searxngImage,
|
|
)
|
|
|
|
if err != nil {
|
|
// Check if it's a port conflict
|
|
if strings.Contains(stderr, "Bind for 127.0.0.1") {
|
|
return fmt.Errorf("port %s is already in use. Please stop the existing service or use a different port", dm.searxngPort)
|
|
}
|
|
return fmt.Errorf("failed to create SearXNG container: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (dm *DockerManager) pullImage() {
|
|
dm.runDocker("pull", dm.searxngImage)
|
|
}
|
|
|
|
func (dm *DockerManager) StopSearXNG() error {
|
|
_, _, _ = dm.runDocker("stop", SearXNGContainerName)
|
|
return nil
|
|
}
|
|
|
|
func (dm *DockerManager) RemoveSearXNG() error {
|
|
_, _, _ = dm.runDocker("rm", "-f", SearXNGContainerName)
|
|
return nil
|
|
}
|
|
|
|
func (dm *DockerManager) EnsureSearXNG() error {
|
|
if dm.IsSearXNGRunning() && dm.GetSearXNGHealth() {
|
|
return nil
|
|
}
|
|
|
|
if !dm.IsDockerAvailable() {
|
|
return fmt.Errorf("Docker is not available. Please install Docker or start SearXNG manually at http://127.0.0.1:%s", dm.searxngPort)
|
|
}
|
|
|
|
if err := dm.StartSearXNG(); err != nil {
|
|
return fmt.Errorf("failed to start SearXNG: %w", err)
|
|
}
|
|
|
|
// Configure SearXNG for JSON API access
|
|
if err := dm.configureSearXNG(); err != nil {
|
|
// Log but don't fail - container might already be configured
|
|
fmt.Printf("Note: SearXNG configuration: %v\n", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// configureSearXNG enables JSON format for API access
|
|
func (dm *DockerManager) configureSearXNG() error {
|
|
// Wait a bit for container to be fully ready
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// Enable JSON format in settings
|
|
// First uncomment the formats line, then add json to the list
|
|
_, _, err := dm.runDocker("exec", SearXNGContainerName, "sh", "-c",
|
|
`sed -i 's/# formats:/formats:/' /etc/searxng/settings.yml && sed -i '/formats:/,/^[a-z]/ { s/ - html/ - html\n - json/; }' /etc/searxng/settings.yml`)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to enable JSON format: %w", err)
|
|
}
|
|
|
|
// Restart to apply changes
|
|
_, _, err = dm.runDocker("restart", SearXNGContainerName)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to restart SearXNG: %w", err)
|
|
}
|
|
|
|
// Wait for restart
|
|
time.Sleep(3 * time.Second)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (dm *DockerManager) WaitForSearXNGReady(timeout time.Duration) error {
|
|
deadline := time.Now().Add(timeout)
|
|
for time.Now().Before(deadline) {
|
|
if dm.GetSearXNGHealth() {
|
|
return nil
|
|
}
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
return fmt.Errorf("SearXNG failed to become ready within %v", timeout)
|
|
}
|