/
main.go
198 lines (162 loc) · 5.95 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"github.com/docker/distribution/notifications"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
"flag"
"github.com/golang/glog"
"io/ioutil"
"math/rand"
"strings"
"time"
)
// AppContext holds all relevant options needed in various places of the app
type AppContext struct {
Config *Config
Session *mgo.Session
c string
}
// NewAppContext creates an empty application context object
func NewAppContext() (*AppContext, error) {
return &AppContext{Session: nil}, nil
}
// The main function sets up the connection to the storage backend for
// aggregated events (e.g. MongoDB) and fires up an HTTPs server which acts as
// an endpoint for docker notifications.
func main() {
flag.Parse()
rand.Seed(time.Now().UnixNano())
glog.CopyStandardLogTo("INFO")
// Create our application context
ctx, _ := NewAppContext()
// Load config file given by first argument
configFilePath := flag.Arg(0)
if configFilePath == "" {
glog.Exit("Config file not specified")
}
c, err := LoadConfig(configFilePath)
if err != nil {
glog.Exit(err)
}
ctx.Config = c
// Connect to MongoDB
session, err := createMongoDbSession(c)
if err != nil {
glog.Exit(err)
}
defer session.Close()
ctx.Session = session
// Wait for errors on inserts and updates and for flushing changes to disk
session.SetSafe(&mgo.Safe{FSync: true})
collection := ctx.Session.DB(ctx.Config.DialInfo.DialInfo.Database).C(ctx.Config.Collection)
// The repository structure shall have a uniqe key on the repository's
// name field
index := mgo.Index{
Key: []string{"repositoryname"},
Unique: true,
DropDups: true,
Background: true,
Sparse: true,
}
if err = collection.EnsureIndex(index); err != nil {
glog.Exitf("It looks like your mongo database is incosinstent. ",
"Make sure you have no duplicate entries for repository names.")
}
// Setup HTTP endpoint
var httpConnectionString = ctx.Config.GetEndpointConnectionString()
glog.Infof("About to listen on \"%s%s\".", httpConnectionString, ctx.Config.Server.Route)
mux := http.NewServeMux()
appHandler := &appHandler{ctx: ctx}
mux.Handle(ctx.Config.Server.Route, appHandler)
err = http.ListenAndServeTLS(httpConnectionString, ctx.Config.Server.Ssl.Cert, ctx.Config.Server.Ssl.CertKey, mux)
if err != nil {
glog.Exit(err)
}
glog.Info("Exiting.")
}
func createMongoDbSession(c *Config) (*mgo.Session, error) {
// Connect to MongoDB
if c.DialInfo.PasswordFile != "" {
passBuf, err := ioutil.ReadFile(c.DialInfo.PasswordFile)
if err != nil {
return nil, fmt.Errorf("Failed to read password file \"%s\": %s", c.DialInfo.PasswordFile, err)
}
c.DialInfo.DialInfo.Password = strings.TrimSpace(string(passBuf))
}
glog.V(2).Infof("Creating MongoDB session (operation timeout %s)", c.DialInfo.DialInfo.Timeout)
session, err := mgo.DialWithInfo(&c.DialInfo.DialInfo)
if err != nil {
return nil, fmt.Errorf("Failed to create MongoDB session: %s", err)
}
return session, nil
}
type appHandler struct {
ctx *AppContext
}
// ServeHTTP has the ability to access our *appContext's fields (session,
// config, etc.) as well.
func (ah appHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// The docker registry sends events to HTTP endpoints and queues them up if
// the endpoint refuses to accept those events. We are only interested in
// manifest updates, therefore we ignore all others by answering with an HTTP
// 200 OK. This should prevent the docker registry from getting too full.
// A request needs to be made via POST
if req.Method != "POST" {
http.Error(w, fmt.Sprintf("Ignoring request. Required method is \"POST\" but got \"%s\".\n", req.Method), http.StatusOK)
return
}
// A request must have a body.
if req.Body == nil {
http.Error(w, "Ignoring request. Required non-empty request body.\n", http.StatusOK)
return
}
// Test for correct mimetype and reject all others
// Even the documentation on docker notfications says that we shouldn't be to
// picky about the mimetype. But we are and let the caller know this.
contentType := req.Header.Get("Content-Type")
if contentType != notifications.EventsMediaType {
http.Error(w, fmt.Sprintf("Ignoring request. Required mimetype is \"%s\" but got \"%s\"\n", notifications.EventsMediaType, contentType), http.StatusOK)
return
}
// Try to decode HTTP body as Docker notification envelope
decoder := json.NewDecoder(req.Body)
var envelope notifications.Envelope
err := decoder.Decode(&envelope)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to decode envelope: %s\n", err), http.StatusBadRequest)
return
}
var collection *mgo.Collection
collection = ah.ctx.Session.DB(ah.ctx.Config.DialInfo.DialInfo.Database).C(ah.ctx.Config.Collection)
for index, event := range envelope.Events {
glog.V(2).Infof("Processing event %d of %d\n", index+1, len(envelope.Events))
// Handle all three cases: push, pull, and delete
if event.Action == notifications.EventActionPull || event.Action == notifications.EventActionPush {
updateBson, err := ProcessEventPullOrPush(&event)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to process push or pull event. Error: %s\n", err), http.StatusBadRequest)
return
}
changeInfo, err := collection.Upsert(bson.M{"repositoryname": event.Target.Repository, "$isolated": true}, updateBson)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to update DB. Error: %s\n", err), http.StatusBadGateway)
return
}
log.Printf("Number of updated documents: %d", changeInfo.Updated)
} else if event.Action == notifications.EventActionDelete {
err := collection.Remove(bson.M{"repositoryname": event.Target.Repository})
if err != nil {
http.Error(w, fmt.Sprintf("Failed to delete document from DB. Error: %s\n", err), http.StatusBadGateway)
return
}
} else {
http.Error(w, fmt.Sprintf("Invalid event action: %s\n", event.Action), http.StatusBadRequest)
return
}
}
http.Error(w, fmt.Sprintf("Done\n"), http.StatusOK)
}