-
Notifications
You must be signed in to change notification settings - Fork 0
/
gitserver.go
441 lines (391 loc) · 13.3 KB
/
gitserver.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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
// Package gitserver provides a smart Git HTTP server that can also set and
// remove hooks. The server is lightweight (<7M compiled with a ~2M footprint)
// and can mirror remote repositories in a containerized environment.
package gitserver
import (
"fmt"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"github.com/AaronO/go-git-http"
"github.com/AaronO/go-git-http/auth"
"github.com/golang/glog"
"github.com/prometheus/client_golang/prometheus"
kapi "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/errors"
"k8s.io/kubernetes/pkg/client/unversioned/clientcmd"
"k8s.io/kubernetes/pkg/healthz"
authapi "github.com/openshift/origin/pkg/authorization/api"
"github.com/openshift/origin/pkg/client"
"github.com/openshift/origin/pkg/generate/git"
)
const (
initialClonePrefix = "GIT_INITIAL_CLONE_"
EnvironmentHelp = `Supported environment variables:
GIT_HOME
directory containing Git repositories; defaults to current directory
PUBLIC_URL
the url of this server for constructing URLs that point to this repository
INTERNAL_URL
the url of this server that can be used internally by build configs in the cluster
GIT_PATH
path to Git binary; defaults to location of 'git' in PATH
HOOK_PATH
path to a directory containing hooks for all repositories; if not set no global hooks will be used
ALLOW_GIT_PUSH
if 'false', pushes will be not be accepted; defaults to true
ALLOW_ANON_GIT_PULL
if 'true', pulls may be made without authorization; defaults to false
ALLOW_GIT_HOOKS
if 'false', hooks cannot be read or set; defaults to true
ALLOW_LAZY_CREATE
if 'false', repositories will not automatically be initialized on push; defaults to true
REQUIRE_GIT_AUTH
a user/password combination required to access the repo of the form "<user>:<password>"; defaults to none
REQUIRE_SERVER_AUTH
a URL to an OpenShift server for verifying authorization credentials provided by a user. Requires
AUTOLINK_NAMESPACE to be set (the namespace that authorization will be checked in). Users must have
'get' on 'pods' to pull (be a viewer) and 'create' on 'pods' to push (be an editor)
GIT_FORCE_CLEAN
if 'true', any initial repository directories will be deleted prior to start; defaults to 'false'
WARNING: this is destructive and you will lose any data you have already pushed
GIT_INITIAL_CLONE_*=<url>[;<name>]
each environment variable in this pattern will be cloned when the process starts; failures will be logged
<name> must be [A-Z0-9_\-\.], the cloned directory name will be lowercased. If the name is invalid the
process will halt. If the repository already exists on disk, it will be updated from the remote.
AUTOLINK_KUBECONFIG
The location to read auth configuration from for autolinking.
If '-', use the service account token to link. The account
represented by this config must have the edit role on the
namespace.
AUTOLINK_NAMESPACE
The namespace to autolink
AUTOLINK_HOOK
The path to a script in the image to use as the default
post-receive hook - only set during link, so has no effect
on cloned repositories. See the "hooks" directory in the
image for examples.
`
)
var (
invalidCloneNameChars = regexp.MustCompile("[^a-zA-Z0-9_\\-\\.]")
reservedNames = map[string]struct{}{"_": {}}
eventCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "git_event_count",
Help: "Counter of events broken out for each repository and type",
},
[]string{"repository", "type"},
)
)
func init() {
prometheus.MustRegister(eventCounter)
}
// Config represents the configuration to use for running the server
type Config struct {
Home string
GitBinary string
URL *url.URL
InternalURL *url.URL
AllowHooks bool
AllowPush bool
AllowLazyCreate bool
HookDirectory string
MaxHookBytes int64
Listen string
AuthenticatorFn func(http http.Handler) http.Handler
CleanBeforeClone bool
InitialClones map[string]Clone
AuthMessage string
}
// Clone is a repository to clone
type Clone struct {
URL url.URL
Hooks map[string]string
}
type statusError struct {
*errors.StatusError
}
func (e *statusError) StatusCode() int {
return int(e.StatusError.Status().Code)
}
// NewDefaultConfig returns a default server config.
func NewDefaultConfig() *Config {
return &Config{
Home: "",
GitBinary: "git",
Listen: ":8080",
MaxHookBytes: 50 * 1024,
}
}
// NewEnvironmentConfig sets up the initial config from environment variables
// TODO break out the code that generates the handler functions so that they
// can be individually unit tested. Also separate out the code that generates
// the initial set of clones.
func NewEnvironmentConfig() (*Config, error) {
config := NewDefaultConfig()
home := os.Getenv("GIT_HOME")
if len(home) == 0 {
return nil, fmt.Errorf("GIT_HOME is required")
}
abs, err := filepath.Abs(home)
if err != nil {
return nil, fmt.Errorf("can't make %q absolute: %v", home, err)
}
if stat, err := os.Stat(abs); err != nil || !stat.IsDir() {
return nil, fmt.Errorf("GIT_HOME must be an existing directory: %v", err)
}
config.Home = home
if publicURL := os.Getenv("PUBLIC_URL"); len(publicURL) > 0 {
valid, err := url.Parse(publicURL)
if err != nil {
return nil, fmt.Errorf("PUBLIC_URL must be a valid URL: %v", err)
}
config.URL = valid
}
if internalURL := os.Getenv("INTERNAL_URL"); len(internalURL) > 0 {
valid, err := url.Parse(internalURL)
if err != nil {
return nil, fmt.Errorf("INTERNAL_URL must be a valid URL: %v", err)
}
config.InternalURL = valid
}
gitpath := os.Getenv("GIT_PATH")
if len(gitpath) == 0 {
path, err := exec.LookPath("git")
if err != nil {
return nil, fmt.Errorf("could not find 'git' in PATH; specify GIT_PATH or set your PATH")
}
gitpath = path
}
config.GitBinary = gitpath
config.AllowPush = os.Getenv("ALLOW_GIT_PUSH") != "false"
config.AllowHooks = os.Getenv("ALLOW_GIT_HOOKS") != "false"
config.AllowLazyCreate = os.Getenv("ALLOW_LAZY_CREATE") != "false"
if hookpath := os.Getenv("HOOK_PATH"); len(hookpath) != 0 {
path, err := filepath.Abs(hookpath)
if err != nil {
return nil, fmt.Errorf("HOOK_PATH was set but cannot be made absolute: %v", err)
}
if stat, err := os.Stat(path); err != nil || !stat.IsDir() {
return nil, fmt.Errorf("HOOK_PATH must be an existing directory if set: %v", err)
}
config.HookDirectory = path
}
allowAnonymousGet := os.Getenv("ALLOW_ANON_GIT_PULL") == "true"
serverAuth := os.Getenv("REQUIRE_SERVER_AUTH")
gitAuth := os.Getenv("REQUIRE_GIT_AUTH")
if len(serverAuth) > 0 && len(gitAuth) > 0 {
return nil, fmt.Errorf("only one of REQUIRE_SERVER_AUTH or REQUIRE_GIT_AUTH may be specified")
}
if len(serverAuth) > 0 {
namespace := os.Getenv("AUTH_NAMESPACE")
if len(namespace) == 0 {
return nil, fmt.Errorf("when REQUIRE_SERVER_AUTH is set, AUTH_NAMESPACE must also be specified")
}
if serverAuth == "-" {
serverAuth = ""
}
rules := clientcmd.NewDefaultClientConfigLoadingRules()
rules.ExplicitPath = serverAuth
kubeconfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, &clientcmd.ConfigOverrides{})
cfg, err := kubeconfig.ClientConfig()
if err != nil {
return nil, fmt.Errorf("could not create a client for REQUIRE_SERVER_AUTH: %v", err)
}
osc, err := client.New(cfg)
if err != nil {
return nil, fmt.Errorf("could not create a client for REQUIRE_SERVER_AUTH: %v", err)
}
config.AuthMessage = fmt.Sprintf("Authenticating against %s allow-push=%t anon-pull=%t", cfg.Host, config.AllowPush, allowAnonymousGet)
authHandlerFn := auth.Authenticator(func(info auth.AuthInfo) (bool, error) {
if !info.Push && allowAnonymousGet {
glog.V(5).Infof("Allowing pull because anonymous get is enabled")
return true, nil
}
req := &authapi.LocalSubjectAccessReview{
Action: authapi.Action{
Verb: "get",
Group: kapi.GroupName,
Resource: "pods",
},
}
if info.Push {
if !config.AllowPush {
return false, nil
}
req.Action.Verb = "create"
}
glog.V(5).Infof("Checking for %s permission on pods", req.Action.Verb)
res, err := osc.ImpersonateLocalSubjectAccessReviews(namespace, info.Password).Create(req)
if err != nil {
if se, ok := err.(*errors.StatusError); ok {
return false, &statusError{se}
}
return false, err
}
glog.V(5).Infof("server response allowed=%t message=%s", res.Allowed, res.Reason)
return res.Allowed, nil
})
if allowAnonymousGet {
authHandlerFn = anonymousHandler(authHandlerFn)
}
config.AuthenticatorFn = authHandlerFn
}
if len(gitAuth) > 0 {
parts := strings.Split(gitAuth, ":")
if len(parts) != 2 {
return nil, fmt.Errorf("REQUIRE_GIT_AUTH must be a username and password separated by a ':'")
}
config.AuthMessage = fmt.Sprintf("Authenticating against username/password allow-push=%t", config.AllowPush)
username, password := parts[0], parts[1]
authHandlerFn := auth.Authenticator(func(info auth.AuthInfo) (bool, error) {
if info.Push {
if !config.AllowPush {
glog.V(5).Infof("Denying push request because it is disabled in config.")
return false, nil
}
}
if info.Username != username || info.Password != password {
glog.V(5).Infof("Username or password doesn't match")
return false, nil
}
return true, nil
})
if allowAnonymousGet {
authHandlerFn = anonymousHandler(authHandlerFn)
}
config.AuthenticatorFn = authHandlerFn
}
if value := os.Getenv("GIT_LISTEN"); len(value) > 0 {
config.Listen = value
}
config.CleanBeforeClone = os.Getenv("GIT_FORCE_CLEAN") == "true"
clones := make(map[string]Clone)
for _, env := range os.Environ() {
if !strings.HasPrefix(env, initialClonePrefix) {
continue
}
parts := strings.SplitN(env, "=", 2)
if len(parts) != 2 {
continue
}
key, value := parts[0], parts[1]
part := key[len(initialClonePrefix):]
if len(part) == 0 {
continue
}
if len(value) == 0 {
return nil, fmt.Errorf("%s must not have an empty value", key)
}
defaultName := strings.Replace(strings.ToLower(part), "_", "-", -1)
values := strings.Split(value, ";")
var uri, name string
switch len(values) {
case 1:
uri, name = values[0], ""
case 2:
uri, name = values[0], values[1]
if len(name) == 0 {
return nil, fmt.Errorf("%s name may not be empty", key)
}
default:
return nil, fmt.Errorf("%s may only have two segments (<url> or <url>;<name>)", key)
}
url, err := git.ParseRepository(uri)
if err != nil {
return nil, fmt.Errorf("%s is not a valid repository URI: %v", key, err)
}
switch url.Scheme {
case "http", "https", "git", "ssh":
default:
return nil, fmt.Errorf("%s %q must be a http, https, git, or ssh URL", key, uri)
}
if len(name) == 0 {
if n, ok := git.NameFromRepositoryURL(url); ok {
name = n + ".git"
}
}
if len(name) == 0 {
name = defaultName + ".git"
}
if invalidCloneNameChars.MatchString(name) {
return nil, fmt.Errorf("%s name %q must be only letters, numbers, dashes, or underscores", key, name)
}
if _, ok := reservedNames[name]; ok {
return nil, fmt.Errorf("%s name %q is reserved (%v)", key, name, reservedNames)
}
glog.V(5).Infof("Adding initial clone to repo at %s", url.String())
clones[name] = Clone{
URL: *url,
}
}
config.InitialClones = clones
return config, nil
}
func anonymousHandler(f func(http.Handler) http.Handler) func(http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
authHandler := f(h)
anonymousHandler := func(w http.ResponseWriter, req *http.Request) {
// If anonymous read request, simply serve content
if req.FormValue("service") != "git-receive-pack" && len(req.Header.Get("Authorization")) == 0 {
h.ServeHTTP(w, req)
return
}
authHandler.ServeHTTP(w, req)
}
return http.HandlerFunc(anonymousHandler)
}
}
func handler(config *Config) http.Handler {
git := githttp.New(config.Home)
git.GitBinPath = config.GitBinary
git.UploadPack = config.AllowPush
git.ReceivePack = config.AllowPush
git.EventHandler = func(ev githttp.Event) {
path := ev.Dir
if strings.HasPrefix(path, config.Home+"/") {
path = path[len(config.Home)+1:]
}
eventCounter.WithLabelValues(path, ev.Type.String()).Inc()
}
handler := http.Handler(git)
if config.AllowLazyCreate {
handler = lazyInitRepositoryHandler(config, handler)
}
if config.AuthenticatorFn != nil {
handler = config.AuthenticatorFn(handler)
}
return handler
}
func Start(config *Config) error {
if err := clone(config); err != nil {
return err
}
handler := handler(config)
ops := http.NewServeMux()
if config.AllowHooks {
glog.V(5).Infof("Installing handler for the /_/hooks endpoint")
ops.Handle("/hooks/", prometheus.InstrumentHandler("hooks", http.StripPrefix("/hooks", hooksHandler(config))))
}
/*ops.Handle("/reflect/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
fmt.Fprintf(os.Stdout, "%s %s\n", r.Method, r.URL)
io.Copy(os.Stdout, r.Body)
}))*/
glog.V(5).Infof("Installing handler for the /_/metrics endpoint")
ops.Handle("/metrics", prometheus.UninstrumentedHandler())
healthz.InstallHandler(ops)
mux := http.NewServeMux()
mux.Handle("/", prometheus.InstrumentHandler("git", handler))
mux.Handle("/_/", http.StripPrefix("/_", ops))
if len(config.AuthMessage) > 0 {
glog.Infof("%s", config.AuthMessage)
}
glog.Infof("Serving %s on %s", config.Home, config.Listen)
return http.ListenAndServe(config.Listen, mux)
}