294 lines
7.8 KiB
Go
294 lines
7.8 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type ContainerInfo struct {
|
|
ID string
|
|
Name string
|
|
Interval time.Duration
|
|
Ignores []string
|
|
}
|
|
|
|
var notifiedChanges = struct {
|
|
sync.RWMutex
|
|
changes map[string]string
|
|
}{changes: make(map[string]string)}
|
|
|
|
var watchedContainers = struct {
|
|
sync.RWMutex
|
|
containers map[string]ContainerInfo
|
|
}{containers: make(map[string]ContainerInfo)}
|
|
|
|
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...")
|
|
|
|
go monitorContainerList()
|
|
|
|
for {
|
|
containers, err := getContainers()
|
|
if err != nil {
|
|
log.Printf("Error listing containers: %v", err)
|
|
time.Sleep(1 * time.Minute)
|
|
continue
|
|
}
|
|
|
|
for _, container := range containers {
|
|
watchedContainers.RLock()
|
|
_, watched := watchedContainers.containers[container.ID]
|
|
watchedContainers.RUnlock()
|
|
|
|
if !watched {
|
|
log.Printf("Monitoring container: %s (%s) every %s", container.Name, container.ID, container.Interval)
|
|
watchedContainers.Lock()
|
|
watchedContainers.containers[container.ID] = container
|
|
watchedContainers.Unlock()
|
|
go monitorContainer(container, true)
|
|
}
|
|
}
|
|
|
|
// Sleep for 1 minute before checking for new containers again
|
|
time.Sleep(1 * time.Minute)
|
|
}
|
|
}
|
|
|
|
func getContainers() ([]ContainerInfo, error) {
|
|
log.Println("Fetching container list...")
|
|
output, err := exec.Command("docker", "ps", "--format", "{{.ID}} {{.Label \"oculus.enable\"}} {{.Label \"oculus.interval\"}} {{.Label \"oculus.cname\"}} {{.Label \"oculus.ignores\"}}").Output()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
lines := strings.Split(string(output), "\n")
|
|
var containers []ContainerInfo
|
|
|
|
for _, line := range lines {
|
|
if line == "" {
|
|
continue
|
|
}
|
|
parts := strings.Fields(line)
|
|
if len(parts) < 2 || parts[1] == "" {
|
|
continue
|
|
}
|
|
|
|
id := parts[0]
|
|
intervalStr := "300s"
|
|
if len(parts) > 2 && parts[2] != "" {
|
|
intervalStr = parts[2]
|
|
}
|
|
interval, err := time.ParseDuration(intervalStr)
|
|
if err != nil {
|
|
log.Printf("Invalid interval format for container %s: %v", id, err)
|
|
continue
|
|
}
|
|
|
|
cname := id
|
|
if len(parts) > 3 && parts[3] != "" {
|
|
cname = parts[3]
|
|
}
|
|
|
|
ignores := []string{}
|
|
if len(parts) > 4 && parts[4] != "" {
|
|
ignores = strings.Split(parts[4], ",")
|
|
}
|
|
|
|
log.Printf("Container ID: %s, Name: %s, Interval: %s, Ignores: %v", id, cname, interval, ignores)
|
|
|
|
containers = append(containers, ContainerInfo{
|
|
ID: id,
|
|
Name: cname,
|
|
Interval: interval,
|
|
Ignores: ignores,
|
|
})
|
|
}
|
|
|
|
log.Printf("Found %d containers to monitor.", len(containers))
|
|
return containers, nil
|
|
}
|
|
|
|
func monitorContainer(container ContainerInfo, initial bool) {
|
|
ticker := time.NewTicker(container.Interval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
log.Printf("Checking diffs for container: %s (%s)", container.Name, container.ID)
|
|
diffOutput, err := getDiffOutput(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.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)
|
|
|
|
err = handleDiffOutput(container, filteredOutput, initial)
|
|
if err != nil {
|
|
log.Printf("Error handling diff output for container %s (%s): %v", container.Name, container.ID, err)
|
|
return
|
|
}
|
|
initial = false
|
|
}
|
|
}
|
|
}
|
|
|
|
func getDiffOutput(containerID string) (string, error) {
|
|
cmd := exec.Command("docker", "diff", containerID)
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return "", fmt.Errorf("docker diff failed: %w", err)
|
|
}
|
|
return string(output), nil
|
|
}
|
|
|
|
func handleDiffOutput(container ContainerInfo, filteredOutput string, initial bool) error {
|
|
if filteredOutput != "" {
|
|
// Calculate a hash of the filtered diff output
|
|
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 initial {
|
|
// For the initial check, just store the hash and don't send a notification
|
|
notifiedChanges.Lock()
|
|
notifiedChanges.changes[container.ID] = diffHash
|
|
notifiedChanges.Unlock()
|
|
log.Printf("Initial check, storing hash for container: %s (%s)", container.Name, container.ID)
|
|
} else if !notified || lastNotifiedHash != diffHash {
|
|
log.Printf("Writing diff output for container: %s (%s)", container.Name, container.ID)
|
|
// Write diff output to a file
|
|
filename := fmt.Sprintf("%s.diff", container.Name)
|
|
err := writeToFile(filename, filteredOutput)
|
|
if err != nil {
|
|
return fmt.Errorf("error writing diff to file: %w", err)
|
|
}
|
|
|
|
// Send notification using go-glitch
|
|
log.Printf("Sending notification for container: %s (%s)", container.Name, container.ID)
|
|
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)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func filterDiffOutput(diffOutput string, ignores []string) (string, error) {
|
|
tempFile, err := os.CreateTemp("", "diff_output")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer os.Remove(tempFile.Name())
|
|
|
|
_, err = tempFile.WriteString(diffOutput)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
tempFile.Close()
|
|
|
|
for _, ignore := range ignores {
|
|
cmd := exec.Command("sed", "-i", fmt.Sprintf("/%s/d", ignore), tempFile.Name())
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
return "", fmt.Errorf("error running sed command: %w", err)
|
|
}
|
|
}
|
|
|
|
filteredOutput, err := os.ReadFile(tempFile.Name())
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return string(filteredOutput), nil
|
|
}
|
|
|
|
func matchPath(path, pattern string) bool {
|
|
match, err := filepath.Match(pattern, filepath.Base(path))
|
|
if err != nil {
|
|
log.Printf("Error matching pattern: %s with path: %s, error: %v", pattern, path, err)
|
|
return false
|
|
}
|
|
log.Printf("Matching path %s with pattern %s: %v", path, pattern, match)
|
|
return match
|
|
}
|
|
|
|
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 monitorContainerList() {
|
|
for {
|
|
watchedContainers.Lock()
|
|
for id, container := watchedContainers.containers {
|
|
if !isContainerRunning(id) {
|
|
log.Printf("Removing stopped container from watch list: %s (%s)", container.Name, id)
|
|
delete(watchedContainers.containers, id)
|
|
}
|
|
}
|
|
watchedContainers.Unlock()
|
|
time.Sleep(1 * time.Minute)
|
|
}
|
|
}
|
|
|
|
func isContainerRunning(containerID string) bool {
|
|
cmd := exec.Command("docker", "inspect", "--format", "{{.State.Running}}", containerID)
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
log.Printf("Error inspecting container %s: %v", containerID, err)
|
|
return false
|
|
}
|
|
return strings.TrimSpace(string(output)) == "true"
|
|
}
|