package main import ( "bufio" "crypto/md5" "encoding/json" "fmt" "io/ioutil" "log" "net/http" "os" "os/exec" "path/filepath" "strings" "sync" "time" ) type ContainerInfo struct { ID string Name string CName string Interval string Ignores []string } var notifiedChanges = struct { sync.RWMutex changes map[string]string }{changes: make(map[string]string)} var apiAddress string func init() { apiAddress = os.Getenv("API_ADDRESS") if apiAddress == "" { apiAddress = "http://localhost:8080" } } func main() { // Check if GLITCHTIP_DSN environment variable is set glitchtipDSN := os.Getenv("GLITCHTIP_DSN") if glitchtipDSN == "" { log.Fatal("GLITCHTIP_DSN environment variable is not set") } log.Println("Starting Oculus...") // Ensure the diffs directory exists err := ensureDiffsDirectory() if err != nil { log.Fatalf("Error creating diffs directory: %v", err) } for { containers, err := fetchContainers() if err != nil { log.Printf("Error fetching containers: %v", err) time.Sleep(1 * time.Minute) continue } var wg sync.WaitGroup for _, container := range containers { wg.Add(1) go func(container ContainerInfo) { defer wg.Done() checkAndNotify(container) }(container) } wg.Wait() cleanupHashes(containers) // Sleep for 1 minute before checking for new containers again time.Sleep(1 * time.Minute) } } func fetchContainers() ([]ContainerInfo, error) { resp, err := http.Get(fmt.Sprintf("%s/containers", apiAddress)) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("error fetching containers: %s", resp.Status) } var containers []ContainerInfo if err := json.NewDecoder(resp.Body).Decode(&containers); err != nil { return nil, err } return containers, nil } func fetchDiffOutput(containerID string) (string, error) { resp, err := http.Get(fmt.Sprintf("%s/diff?id=%s", apiAddress, containerID)) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("error fetching diff: %s", resp.Status) } body, err := ioutil.ReadAll(resp.Body) if err != nil { return "", err } return string(body), nil } func checkAndNotify(container ContainerInfo) { diffOutput, err := fetchDiffOutput(container.ID) if err != nil { log.Printf("Error getting diffs for container %s (%s): %v", container.Name, container.ID, err) return } filteredOutput, err := filterDiffOutput(diffOutput, container.CName, container.Ignores) if err != nil { log.Printf("Error filtering diffs for container %s (%s): %v", container.Name, container.ID, err) return } log.Printf("Filtered diff output for container %s: %s", container.Name, filteredOutput) if filteredOutput != "" { diffHash := fmt.Sprintf("%x", md5.Sum([]byte(filteredOutput))) log.Printf("Diff hash for container %s: %s", container.Name, diffHash) notifiedChanges.RLock() lastNotifiedHash, notified := notifiedChanges.changes[container.ID] notifiedChanges.RUnlock() if !notified || lastNotifiedHash != diffHash { filename := filepath.Join("diffs", fmt.Sprintf("%s.diff", container.CName)) err := writeToFile(filename, filteredOutput) if err != nil { log.Printf("Error writing diff to file: %v", err) return } err = sendNotification(filteredOutput) if err != nil { log.Printf("Error sending notification for container %s: %v", container.ID, err) } else { notifiedChanges.Lock() notifiedChanges.changes[container.ID] = diffHash notifiedChanges.Unlock() log.Printf("Notification sent and hash updated for container: %s (%s)", container.Name, container.ID) } } else { log.Printf("No new changes detected for container: %s (%s)", container.Name, container.ID) } } else { log.Printf("No significant changes detected for container: %s (%s)", container.Name, container.ID) } } func filterDiffOutput(diffOutput, cname string, ignores []string) (string, error) { filename := filepath.Join("diffs", fmt.Sprintf("%s.diff", cname)) // Write the diff output to the file err := ioutil.WriteFile(filename, []byte(diffOutput), 0644) if err != nil { return "", fmt.Errorf("error writing diff to file: %s", err) } // Read and filter the diff output var filteredLines []string file, err := os.Open(filename) if err != nil { return "", fmt.Errorf("error opening diff file: %s", err) } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() ignore := false for _, pattern := range ignores { match, err := filepath.Match(pattern, line) if err != nil { return "", fmt.Errorf("error matching pattern: %s", err) } if match { ignore = true break } } if !ignore { filteredLines = append(filteredLines, line) } } if err := scanner.Err(); err != nil { return "", fmt.Errorf("error reading diff file: %s", err) } filteredOutput := strings.Join(filteredLines, "\n") err = ioutil.WriteFile(filename, []byte(filteredOutput), 0644) if err != nil { return "", fmt.Errorf("error writing filtered diff to file: %s", err) } return filteredOutput, nil } func writeToFile(filename, content string) error { file, err := os.Create(filename) if err != nil { return err } defer file.Close() _, err = file.WriteString(content) return err } func sendNotification(content string) error { cmd := exec.Command("go-glitch") cmd.Stdin = strings.NewReader(content) output, err := cmd.CombinedOutput() if err != nil { log.Printf("go-glitch output: %s", output) return err } return nil } func ensureDiffsDirectory() error { return os.MkdirAll("./diffs", os.ModePerm) } func cleanupHashes(containers []ContainerInfo) { notifiedChanges.Lock() defer notifiedChanges.Unlock() activeContainers := make(map[string]bool) for _, container := range containers { activeContainers[container.ID] = true } for id := range notifiedChanges.changes { if !activeContainers[id] { delete(notifiedChanges.changes, id) } } }