main.go
1 package main 2 3 import ( 4 "context" 5 "fmt" 6 "log" 7 "net/http" 8 "os" 9 "strings" 10 "sync" 11 12 "github.com/docker/docker/api/types" 13 containertypes "github.com/docker/docker/api/types/container" 14 "github.com/docker/docker/client" 15 "github.com/robfig/cron" 16 ) 17 18 type ImageUpdate struct { 19 ImageName string 20 CurrentHash string 21 LatestHash string 22 UpdateAvailable bool 23 Architecture string 24 ImageCreated string 25 } 26 27 type Image struct { 28 Architecture string `json:"architecture"` 29 Digest string `json:"digest"` 30 } 31 32 type Repository struct { 33 Creator int `json:"creator"` 34 ID int `json:"id"` 35 Digest string `json:"digest"` 36 Images []Image `json:"images"` 37 } 38 39 var offlineImages = make(map[string]bool) 40 41 func checkOffline(container types.Container, namespace, repository, tag string) bool { 42 statusCode, _ := pingDockerhub(namespace, repository, tag) 43 if statusCode == http.StatusNotFound { 44 offlineImages[container.Image] = true 45 log.Printf("image %s is not available on dockerhub. likely local image.", container.Image) 46 return true 47 } 48 return false 49 } 50 51 func updates(containers []types.Container, ctx context.Context, cli *client.Client) []ImageUpdate { 52 var updates []ImageUpdate 53 var wg sync.WaitGroup 54 results := make(chan ImageUpdate, len(containers)) 55 56 for _, container := range containers { 57 wg.Add(1) 58 go func(container types.Container) { 59 defer wg.Done() 60 61 namespace, repository, tag := parseImageName(container.Image) 62 63 // check for offline images 64 for range offlineImages { 65 if offlineImages[container.Image] { 66 return 67 } 68 } 69 if checkOffline(container, namespace, repository, tag) { 70 return 71 } 72 73 imageName := fmt.Sprintf("%s/%s:%s", namespace, repository, tag) 74 currentHash, arch, imageCreated := getCurrentHash(ctx, cli, imageName) 75 76 log.Printf("checking updates for %s", imageName) 77 latestHash, err := getLatestHash(namespace, repository, tag, arch) 78 log.Printf("namespace: %s, repository: %s, tag: %s, arch: %s", namespace, repository, tag, arch) 79 80 log.Printf("current hash for %s is: %s", container.Image, currentHash) 81 log.Printf("latest hash for %s is: %s", container.Image, latestHash) 82 83 if err != nil { 84 log.Fatal(err) 85 } 86 87 updateAvailable := strings.SplitN(currentHash, ":", 2)[1] != strings.SplitN(latestHash, ":", 2)[1] 88 89 results <- ImageUpdate{ 90 ImageName: imageName, 91 CurrentHash: currentHash, 92 LatestHash: latestHash, 93 UpdateAvailable: updateAvailable, 94 Architecture: arch, 95 ImageCreated: imageCreated, 96 } 97 }(container) 98 } 99 100 go func() { 101 wg.Wait() 102 close(results) 103 }() 104 105 for result := range results { 106 updates = append(updates, result) 107 } 108 109 return updates 110 } 111 112 func cronJob(containers []types.Container, ctx context.Context, cli *client.Client) { 113 imageUpdate := updates(containers, ctx, cli) 114 generateRSSFeed(imageUpdate) 115 } 116 func main() { 117 ctx := context.Background() 118 cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) 119 if err != nil { 120 log.Fatalf("Error creating Docker client: %v", err) 121 } 122 defer cli.Close() 123 124 containers, err := cli.ContainerList(ctx, containertypes.ListOptions{}) 125 if err != nil { 126 log.Fatal("panic containers: ", err) 127 } 128 129 initFeed() 130 http.HandleFunc("/feed", feedHandler) 131 132 go func() { 133 log.Fatal(http.ListenAndServe("0.0.0.0:8083", nil)) 134 }() 135 136 log.Println("docker-rss server started at 0.0.0.0:8083...") 137 log.Printf("cronjob expression specified: %s", os.Getenv("UPDATE_SCHEDULE")) 138 139 c := cron.New() 140 141 c.AddFunc(os.Getenv("UPDATE_SCHEDULE"), func() { 142 cronJob(containers, ctx, cli) 143 }) 144 145 c.Start() 146 select {} 147 }