package main import ( "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 } type MonitoredContainer struct { Info ContainerInfo LastChecked time.Time } var ( apiAddress string glitchtipDSN string notifiedChanges = make(map[string]string) monitoredContainers = make(map[string]*MonitoredContainer) mu sync.Mutex ) func init() { apiAddress = os.Getenv("API_ADDRESS") if apiAddress == "" { apiAddress = "http://localhost:8080" } glitchtipDSN = os.Getenv("GLITCHTIP_DSN") if glitchtipDSN == "" { log.Fatal("GLITCHTIP_DSN environment variable is not set") } } func main() { log.Println("Starting Oculus...") // Ensure the diffs directory exists err := os.MkdirAll("./diffs", os.ModePerm) if err != nil { log.Fatalf("Error creating diffs directory: %v", err) } for { err := fetchAndMonitorContainers() if err != nil { log.Printf("Error in fetching and monitoring containers: %v", err) } time.Sleep(1 * time.Second) } } func fetchAndMonitorContainers() error { containers, err := fetchContainers() if err != nil { return err } for _, container := range containers { mu.Lock() if _, exists := monitoredContainers[container.ID]; !exists { interval, err := time.ParseDuration(container.Interval) if err != nil { log.Printf("Invalid interval for container %s (%s): %v", container.Name, container.ID, err) mu.Unlock() continue } monitoredContainers[container.ID] = &MonitoredContainer{ Info: container, LastChecked: time.Now().Add(-interval), } } mu.Unlock() } var wg sync.WaitGroup for _, monitoredContainer := range monitoredContainers { wg.Add(1) go func(mc *MonitoredContainer) { defer wg.Done() interval, err := time.ParseDuration(mc.Info.Interval) if err != nil { log.Printf("Invalid interval for container %s (%s): %v", mc.Info.Name, mc.Info.ID, err) return } mu.Lock() if time.Since(mc.LastChecked) >= interval { mc.LastChecked = time.Now() mu.Unlock() checkAndNotify(mc.Info) } else { mu.Unlock() } }(monitoredContainer) } wg.Wait() return nil } 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) mu.Lock() lastNotifiedHash, notified := notifiedChanges[container.ID] mu.Unlock() 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 { mu.Lock() notifiedChanges[container.ID] = diffHash mu.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) } // Construct the filter command args := append([]string{filename}, ignores...) cmd := exec.Command("filter", args...) fullCommand := fmt.Sprintf("filter %s", strings.Join(cmd.Args, " ")) log.Printf("Running command: %s", fullCommand) fmt.Printf("Running command: %s\n", fullCommand) // Print the command to stdout for debugging output, err := cmd.CombinedOutput() if err != nil { return "", fmt.Errorf("error running filter command: %s, output: %s", err, output) } // Read the filtered output filteredOutput, err := ioutil.ReadFile(filename) if err != nil { return "", fmt.Errorf("error reading filtered diff file: %s", err) } return string(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 }