package main import ( "context" "encoding/json" "fmt" "log" "os" "os/exec" "os/signal" "path/filepath" "strings" "syscall" "time" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/client" ) const ( notifyScript = "/notify.sh" logDir = "/log" ) type ContainerDiff struct { ID string Image string Labels map[string]string } type FileChange struct { Path string Kind string } func getRunningContainers(cli *client.Client) ([]ContainerDiff, error) { containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{}) if err != nil { return nil, err } var diffs []ContainerDiff for _, container := range containers { if _, ok := container.Labels["oculus.cname"]; ok { diffs = append(diffs, ContainerDiff{ ID: container.ID, Image: container.Image, Labels: container.Labels, }) } } return diffs, nil } func saveDiffs(diffs []FileChange, filePath string) error { data, err := json.Marshal(diffs) if err != nil { return err } return os.WriteFile(filePath, data, 0644) } func loadDiffs(filePath string) ([]FileChange, error) { data, err := os.ReadFile(filePath) if err != nil { return nil, err } var diffs []FileChange err = json.Unmarshal(data, &diffs) return diffs, err } func shouldIgnore(change string, ignoreList []string) bool { for _, ignore := range ignoreList { if strings.Contains(change, ignore) { return true } } return false } func logDetection(containerID, message string) { logFilePath := filepath.Join(logDir, fmt.Sprintf("%s.log", containerID)) f, err := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { log.Printf("Error opening log file: %v", err) return } defer f.Close() log.SetOutput(f) log.Println(message) sendToGlitchtip(logFilePath) } func sendToGlitchtip(logFilePath string) { if dsn := os.Getenv("GLITCHTIP_DSN"); dsn != "" { cmd := exec.Command("go-glitch", logFilePath) err := cmd.Run() if err != nil { log.Printf("Error sending to Go Glitch: %v", err) } } } func compareContainers(cli *client.Client, mode string) { currentDiffs, err := getRunningContainers(cli) if err != nil { log.Fatalf("Error getting current containers: %v", err) } for _, current := range currentDiffs { if current.Labels["oculus.mode"] == mode { filePath := filepath.Join(logDir, fmt.Sprintf("%s.json", current.Labels["oculus.cname"])) savedDiffs, err := loadDiffs(filePath) if err != nil { log.Printf("Error loading saved diffs for %s: %v", current.ID, err) savedDiffs = []FileChange{} // Initialize to an empty slice } ignoreList := strings.Split(current.Labels["oculus.ignorelist"], ",") currentChanges, err := cli.ContainerDiff(context.Background(), current.ID) if err != nil { log.Printf("Error getting container diff for %s: %v", current.ID, err) continue } var changes []FileChange for _, change := range currentChanges { path := change.Path kind := "" switch change.Kind { case 0: kind = "Modified" case 1: kind = "Added" case 2: kind = "Deleted" } if !shouldIgnore(path, ignoreList) { changes = append(changes, FileChange{ Path: path, Kind: kind, }) } } if mode == "monitor" { err = saveDiffs(changes, filePath) if err != nil { log.Fatalf("Error saving diffs: %v", err) } } else if mode == "report" { for _, change := range changes { message := fmt.Sprintf("Container %s changed: %s %s", current.ID, change.Kind, change.Path) logDetection(current.Labels["oculus.cname"], message) runNotifyScript(filepath.Join(logDir, fmt.Sprintf("%s.log", current.Labels["oculus.cname"]))) } } } } } func runNotifyScript(logFilePath string) { cmd := exec.Command(notifyScript, logFilePath) err := cmd.Run() if err != nil { log.Printf("Error running notify script: %v", err) } } func watchDockerEvents(cli *client.Client) { eventFilter := filters.NewArgs() eventFilter.Add("type", "container") eventFilter.Add("event", "start") eventFilter.Add("event", "stop") eventFilter.Add("event", "destroy") messages, errs := cli.Events(context.Background(), types.EventsOptions{ Filters: eventFilter, }) for { select { case event := <-messages: log.Printf("Docker event received: %s for container %s", event.Action, event.ID) compareContainers(cli, "monitor") case err := <-errs: log.Printf("Error watching Docker events: %v", err) return } } } func monitorContainers(cli *client.Client, interval time.Duration) { ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ticker.C: compareContainers(cli, "monitor") } } } func reportContainers(cli *client.Client, interval time.Duration) { ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ticker.C: compareContainers(cli, "report") } } } func main() { cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { log.Fatalf("Error creating Docker client: %v", err) } go watchDockerEvents(cli) // Get running containers and set up monitoring intervals containers, err := getRunningContainers(cli) if err != nil { log.Fatalf("Error getting running containers: %v", err) } for _, container := range containers { if mode, ok := container.Labels["oculus.mode"]; ok { intervalStr := container.Labels["oculus.interval"] interval, err := time.ParseDuration(intervalStr) if err != nil { log.Fatalf("Invalid interval format for container %s: %v", container.ID, err) } if mode == "monitor" { go monitorContainers(cli, interval) } else if mode == "report" { go reportContainers(cli, interval) } } } // Graceful shutdown handling sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) <-sigs log.Println("Shutting down...") }