Browse code

git server: automatically launch builds on push

Cesar Wong authored on 2016/04/13 03:19:29
Showing 12 changed files
... ...
@@ -23,7 +23,7 @@ func main() {
23 23
 	}
24 24
 
25 25
 	basename := filepath.Base(os.Args[0])
26
-	command := gitserver.NewCommandGitServer(basename)
26
+	command := gitserver.CommandFor(basename)
27 27
 	if err := command.Execute(); err != nil {
28 28
 		os.Exit(1)
29 29
 	}
... ...
@@ -7,7 +7,12 @@ FROM openshift/origin
7 7
 
8 8
 ADD bin/gitserver /usr/bin/gitserver
9 9
 ADD hooks/ /var/lib/git-hooks/
10
-RUN mkdir -p /var/lib/git
10
+RUN mkdir -p /var/lib/git && \
11
+    mkdir -p /var/lib/gitconfig && \
12
+    chmod 777 /var/lib/gitconfig && \
13
+    ln -s /usr/bin/gitserver /usr/bin/gitrepo-buildconfigs
14
+ADD gitconfig /var/lib/gitconfig/.gitconfig
15
+ENV HOME=/var/lib/gitconfig
11 16
 VOLUME /var/lib/git
12 17
 
13 18
 ENTRYPOINT ["/usr/bin/gitserver"]
... ...
@@ -1,16 +1,214 @@
1 1
 Configurable Git Server
2 2
 =======================
3 3
 
4
-This example provides automatic mirroring of Git repositories, intended
5
-for use within a container or Kubernetes pod. It can clone repositories
6
-from remote systems on startup as well as remotely register hooks. It
7
-can automatically initialize and receive Git directories on push.
8
-
9
-In the more advanced modes, it can integrate with an OpenShift server to
10
-automatically perform actions when new repositories are created, like
11
-reading the build configs in the current namespace and performing
12
-automatic mirroring of their input, and creating new build-configs when
13
-content is pushed.
14
-
15
-The Dockerfile built by this example is published as
16
-openshift/origin-gitserver
17 4
\ No newline at end of file
5
+Overview
6
+--------
7
+
8
+This example git server is intended for use within a container or Kubernetes pod.
9
+It can clone repositories from remote systems on startup as well as automatically
10
+launch builds in an OpenShift project. It can automatically initialize and receive
11
+Git directories on push.
12
+
13
+Hooks can be customized to perform actions on push such as invoking `oc new-app` on a
14
+repository's source.
15
+
16
+The Dockerfile built by this example is published as openshift/origin-gitserver
17
+
18
+Quick Start
19
+-----------
20
+
21
+Prerequisites:
22
+
23
+* You have an OpenShift v3 server running
24
+* You are logged in and have access to a project
25
+* You have the `gitserver.yaml` from this directory
26
+* You can create externally accessible routes on your server
27
+
28
+### Deploy the Git Server
29
+
30
+1. Create the Git Server
31
+
32
+    ```sh
33
+    $ oc create -f gitserver.yaml
34
+    ```
35
+
36
+2. Grant `edit` access to the `git` service account
37
+
38
+    ```sh
39
+    $ oc policy add-role-to-user edit -z git
40
+    ```
41
+
42
+
43
+### Push code to the Git Server
44
+
45
+Code repositories can be initialized on the Git Server by either doing a 'git push' or
46
+a 'git clone' of the repository.
47
+
48
+
49
+1. Find your git server's URL
50
+
51
+   If you have a working router, determine the host name of your git server by getting its route:
52
+
53
+   ```sh
54
+   $ oc get route git
55
+   ```
56
+
57
+   In this case, the URL of your git server will be the host name used by the route; something like: 
58
+   
59
+   ```
60
+   http://git-myproject.infra.openshift.com
61
+   ```
62
+  
63
+   Alternatively, if your router is not functional, you can port-forward the git-server pod to your local machine.
64
+   In a separate shell window, execute the following: (You must leave the port-forward command running
65
+   for the port to be forwarded to your machine)
66
+
67
+   ```
68
+   $ oc get pods | grep git           ## get the git server pod
69
+   $ oc port-forward -p PODNAME 8080  ## start port-forward where PODNAME is the git server pod
70
+   ```
71
+
72
+   In this case, the URL of your git server will be your local host:
73
+
74
+   ```
75
+   http://localhost:8080
76
+   ```
77
+
78
+
79
+2. Setup your credentials
80
+
81
+   By default the Git Server will allow users or service accounts that can create pods in
82
+   the project namespace to create and push code to the Git Server. The easiest way to 
83
+   provide credentials to the Git Server is by using a custom credential helper that will 
84
+   send your OpenShift token by default to the server.
85
+   ```sh
86
+   $ git config --global credential.http://gitserver-myproject.infra.openshift.com.helper \
87
+         '!f() { echo "username=$(oc whoami)"; echo "password=$(oc whoami -t)"; }; f'
88
+   ```
89
+
90
+   **NOTE:** the config key is `credential.[git server URL].helper`
91
+
92
+3. Push a repository to your git server
93
+
94
+   In an existing repository, add a remote that points to the git server and push to it
95
+   
96
+   ```
97
+   # clone a public repository
98
+   $ git clone https://github.com/openshift/ruby-hello-world.git
99
+
100
+   # add a remote for your git server
101
+   $ cd ruby-hello-world
102
+   $ git remote add openshift http://git-myproject.infra.openshift.com/ruby-hello-world.git  
103
+
104
+   # push the code to the git server
105
+   $ git push openshift master
106
+   ```
107
+
108
+   **NOTE:** the ruby-hello-world.git repository does not exist before running these commands. 
109
+   By pushing to it, you are creating it in the Git Server.
110
+
111
+   On push the git server will invoke new-app on the code and create artifacts for it in 
112
+   OpenShift.
113
+
114
+
115
+### Secure the Git Server
116
+
117
+Beyond demo uses, it is recommended that communication with the Git server be encrypted with the TLS
118
+protocol to avoid transmission of source in plain text.
119
+
120
+1. Modify your route to use edge termination:
121
+
122
+   ```sh
123
+   $ oc edit route git
124
+   ```
125
+
126
+   Add tls -> termination -> edge to the route specification:
127
+
128
+   ```yaml
129
+   apiVersion: v1
130
+   kind: Route
131
+   metadata:
132
+     name: gitserver
133
+   spec:
134
+     host: gitserver-myproject.infra.openshift.com
135
+     tls:
136
+       termination: edge
137
+     to:
138
+       kind: Service
139
+       name: gitserver
140
+   ```
141
+
142
+2. If using a private certificate authority, configure your git client to use the private ca.crt file:
143
+
144
+   ```sh
145
+   $ git config --global http.https://gitserver-myproject.infra.openshift.com.sslCAInfo /path/to/ca.crt
146
+   ```
147
+
148
+   where the key is http.[git server URL].sslCAInfo
149
+
150
+3. Disable anonymous cloning. By default the gitserver will allow anonymous cloning to make it easier to 
151
+   run builds without having to specify a secret. You can disable this by setting the `ALLOW_ANON_GIT_PULL`
152
+   environment variable to `false`.
153
+
154
+   ```sh
155
+   $ oc set env dc/git ALLOW_ANON_GIT_PULL=false
156
+   ```
157
+
158
+   **NOTE:** changing environment variables on the git server will cause a redeploy of the git server. If using
159
+   the default ephemeral storage for it, all repositories that have been pushed previously will be wiped out.
160
+   They will need to be pushed to the server again to restore.
161
+
162
+Authentication
163
+--------------
164
+
165
+By default, the gitserver will authenticate using OpenShift user or service account credentials. For a user,
166
+the credentials are the user name, and the user's token (from `oc whoami -t`). For a service account, the user
167
+name is the service account name and the password is the service account token. The token can
168
+be one of the 2 tokens created with the service account and stored in the service account secrets. These can 
169
+be obtained by looking at the secrets that correspond to the service account (`oc get secrets`) and displaying
170
+the token in one of them: `oc describe secret NAME`.
171
+
172
+
173
+Authorization
174
+-------------
175
+
176
+Users or service accounts must be able to read pods in the current namespace in order to fetch repositories from
177
+the Git Server. Specifying ALLOW_ANON_GIT_PULL=true as an environment variable for the Git Server will allow anyone
178
+to fetch/clone content from the Git Server. To create/push content, users or service accounts must have the right 
179
+to create pods in the namespace.
180
+
181
+
182
+Automatically Cloning Public Repositories
183
+-----------------------------------------
184
+
185
+The Git Server can automatically clone a set of repositories on startup if it is going to be used for mirroring
186
+purposes. Repositories to be cloned can be specified using a set of environment variables that match
187
+`GIT_INITIAL_CLONE_[name]` where [name] must be unique. The value of the variable must be a URL to the remote
188
+repository to clone and an optional name for the clone.
189
+
190
+The following command will add an environment variable to clone the openshift/ruby-hello-world repository and give
191
+it a name of `helloworld` inside the Git Server.
192
+
193
+```sh
194
+$ oc set env dc/gitserver GIT_INITIAL_CLONE_1="https://github.com/openshift/ruby-hello-world.git;helloworld"
195
+```
196
+
197
+Automatically Starting Builds
198
+-----------------------------
199
+
200
+By default, whenever the git server receives a commit, it will look for a BuildConfig in the same namespace as the
201
+git server and the same name as the repository where the commit is being pushed.  If it finds a BuildConfig with 
202
+the same name, it will start a build for that BuildConfig. Alternatively, an annotation may be added to a 
203
+BuildConfig to explicitly link it to a repository on the git server:
204
+
205
+```yaml
206
+apiVersion: v1
207
+kind: BuildConfig
208
+metadata:
209
+  annotations:
210
+      openshift.io/git-repository: myrepo
211
+```
212
+
213
+**NOTE**: A build will be started for the BuildConfig matching the name of the repository and for any BuildConfig 
214
+that has an annotation pointing to the source repository. If there is a BuildConfig that has a matching name but
215
+has an annotation pointing to a different repository, a build will not be invoked for it.
18 216
new file mode 100644
... ...
@@ -0,0 +1,5 @@
0
+[url "http://127.0.0.1"]
1
+   insteadOf = http://git
2
+[user]
3
+   email = dev@openshift
4
+   name = Development
... ...
@@ -2,28 +2,28 @@ apiVersion: v1
2 2
 kind: List
3 3
 items:
4 4
 
5
-# The gitserver is deployed as a singleton pod and uses a very small amount
5
+# The git server is deployed as a singleton pod and uses a very small amount
6 6
 # of resources. It can host or transiently serve Git repositories, as well
7 7
 # as automatically integrate with builds in a namespace.
8 8
 - apiVersion: v1
9 9
   kind: DeploymentConfig
10 10
   metadata:
11
-    name: gitserver
11
+    name: git
12 12
     labels:
13
-      app: gitserver
13
+      app: git
14 14
   spec:
15
-    replicas: 1 # the gitserver is not HA and should not be scaled past 1
15
+    replicas: 1 # the git server is not HA and should not be scaled past 1
16 16
     selector:
17
-      run-container: gitserver
17
+      run-container: git
18 18
     template:
19 19
       metadata:
20 20
         labels:
21
-          run-container: gitserver
21
+          run-container: git
22 22
       spec:
23
-        serviceAccountName: gitserver
23
+        serviceAccountName: git
24 24
         containers:
25
-        - name: gitserver
26
-          image: openshift/origin-gitserver
25
+        - name: git
26
+          image: openshift/origin-gitserver:latest
27 27
           ports:
28 28
           - containerPort: 8080
29 29
 
... ...
@@ -51,7 +51,13 @@ items:
51 51
           # stored in this app.
52 52
           # TOOD: support HTTPS
53 53
           - name: PUBLIC_URL
54
-            value: http://gitserver.$(POD_NAMESPACE).svc.cluster.local:8080
54
+            value: http://git.$(POD_NAMESPACE).svc.cluster.local:8080
55
+          # If INTERNAL_URL is specified, then it's used to point
56
+          # BuildConfigs to the internal service address of the git
57
+          # server
58
+          - name: INTERNAL_URL
59
+            value: http://git:8080
60
+
55 61
           # The directory to store Git repositories in. If not backed
56 62
           # by a persistent volume, repositories will be lost when
57 63
           # deployments occur. Use INITIAL_GIT_CLONE and AUTOLINK_*
... ...
@@ -62,26 +68,39 @@ items:
62 62
           # The directory to use as the default hook directory for any
63 63
           # cloned or autolinked directories.
64 64
           - name: HOOK_PATH
65
-          # value: /var/lib/git-hooks
65
+            value: /var/lib/git-hooks
66
+
67
+          # If 'true' new-app will be invoked on push for repositories
68
+          # for which a matching BuildConfig is not found.
69
+          - name: GENERATE_ARTIFACTS
70
+            value: "true"
71
+
72
+          # The script to use for custom language detection on a
73
+          # repository. See hooks/detect-language for an example.
74
+          # To use new-app's default detection, leave this variable
75
+          # blank.
76
+          - name: DETECTION_SCRIPT
77
+          # value: detect-language
66 78
 
67 79
           # Authentication and authorization
68 80
 
69
-          # If 'yes', clients may push to the server with git push.
81
+          # If 'true', clients may push to the server with git push.
70 82
           - name: ALLOW_GIT_PUSH
71
-            value: "yes"
72
-          # If 'yes', clients may set hooks via the API. However, unless
83
+            value: "true"
84
+          # If 'true', clients may set hooks via the API. However, unless
73 85
           # the Git home is backed by a persistent volume, any deployment
74 86
           # will result in the hooks being lost.
75 87
           - name: ALLOW_GIT_HOOKS
76
-            value: "yes"
77
-          # If 'yes', clients can create new git repositories on demand
88
+            value: "true"
89
+          # If 'true', clients can create new git repositories on demand
78 90
           # by pushing. If the data on disk is not backed by a persistent
79 91
           # volume, the Git repo will be deleted if the deployment is
80 92
           # updated.
81 93
           - name: ALLOW_LAZY_CREATE
82
-            value: "yes"
83
-          # If 'yes', clients can pull without being authenticated.
94
+            value: "true"
95
+          # If 'true', clients can pull without being authenticated.
84 96
           - name: ALLOW_ANON_GIT_PULL
97
+            value: "true"
85 98
 
86 99
           # Provides the path to a kubeconfig file in the image that
87 100
           # should be used to authorize against the server. The value
... ...
@@ -102,7 +121,7 @@ items:
102 102
 
103 103
           # Autolinking:
104 104
           #
105
-          # The gitserver can automatically clone Git repositories
105
+          # The git server can automatically clone Git repositories
106 106
           # associated with a build config and replace the URL with
107 107
           # a link to the repo on PUBLIC_URL. The default post-receive
108 108
           # hook on the cloned repo will then trigger a build. You
... ...
@@ -110,8 +129,7 @@ items:
110 110
           # To autolink, the account the pod runs under must have 'edit'
111 111
           # on the AUTOLINK_NAMESPACE:
112 112
           #
113
-          #    oc policy add-role-to-user \
114
-          #      system:serviceaccount:${namespace}:gitserver edit
113
+          #    oc policy add-role-to-user -z git edit
115 114
           #
116 115
           # Links are checked every time the pod starts.
117 116
 
... ...
@@ -132,12 +150,6 @@ items:
132 132
           # image for examples.
133 133
           - name: AUTOLINK_HOOK
134 134
 
135
-          # The master service host is not signed with the service IP
136
-          # so we override with the consistent DNS name. Required for
137
-          # connections to the server.
138
-          - name: KUBERNETES_SERVICE_HOST
139
-            value: kubernetes.default
140
-
141 135
           volumeMounts:
142 136
           - mountPath: /var/lib/git/
143 137
             name: git
... ...
@@ -147,25 +159,36 @@ items:
147 147
     triggers:
148 148
     - type: ConfigChange
149 149
 
150
-# The gitserver service is required for DNS resolution
150
+# The git server service is required for DNS resolution
151 151
 - apiVersion: v1
152 152
   kind: Service
153 153
   metadata:
154
-    name: gitserver
154
+    name: git
155 155
     labels:
156
-      app: gitserver
156
+      app: git
157 157
   spec:
158 158
     ports:
159 159
     - port: 8080
160 160
       targetPort: 8080
161 161
     selector:
162
-      run-container: gitserver
162
+      run-container: git
163 163
 
164
-# The service account for the gitserver must be granted the edit role if
165
-# you wish to use autolinking.
164
+# The service account for the git server must be granted the view role to
165
+# automatically start builds, edit role to create objects and autolink
166 166
 - apiVersion: v1
167 167
   kind: ServiceAccount
168 168
   metadata:
169
-    name: gitserver
169
+    name: git
170 170
     labels:
171
-      app: gitserver
171
+      app: git
172
+
173
+# Default route for git service
174
+- apiVersion: v1
175
+  kind: Route
176
+  metadata:
177
+    labels:
178
+      app: git
179
+    name: git
180
+  spec:
181
+    to:
182
+      name: git
... ...
@@ -20,13 +20,13 @@ function key {
20 20
 
21 21
 prefix=${PREFIX:-openshift/}
22 22
 
23
-if has Gemfile; then
23
+if has Gemfile Rakefile config.ru; then
24 24
   echo "${prefix}ruby"
25 25
   exit 0
26 26
 fi
27 27
 
28
-if has requirements.txt; then
29
-  echo "${prefix}python"
28
+if has pom.xml; then
29
+  echo "${prefix}wildfly"
30 30
   exit 0
31 31
 fi
32 32
 
... ...
@@ -35,14 +35,19 @@ if has package.json app.json; then
35 35
   exit 0
36 36
 fi
37 37
 
38
-if has '*.go'; then
39
-  echo "${prefix}golang"
38
+if has index.php composer.json; then
39
+  echo "${prefix}php"
40 40
   exit 0
41 41
 fi
42 42
 
43
-if has index.php; then
44
-  echo "${prefix}php"
43
+if has requirements.txt setup.py; then
44
+  echo "${prefix}python"
45
+  exit 0
46
+fi
47
+
48
+if has index.pl cpanfile; then
49
+  echo "${prefix}perl"
45 50
   exit 0
46 51
 fi
47 52
 
48
-exit 1
49 53
\ No newline at end of file
54
+exit 1
... ...
@@ -7,48 +7,77 @@ set -o pipefail
7 7
 function key {
8 8
   git config --local --get "${1}"
9 9
 }
10
+
10 11
 function addkey {
11 12
   git config --local --add "${1}" "${2}"
12 13
 }
13 14
 
14 15
 function detect {
15
-  if detected=$(key openshift.io.detect); then
16
-    exit 0
17
-  fi
18
-  if ! url=$(key gitserver.self.url); then
19
-    echo "detect: no self url set"
20
-    exit 0
16
+  local repoURL="$1"
17
+  local repoName="$2"
18
+  local detectionScript=${DETECTION_SCRIPT:-}
19
+  # The purpose of using a custom detection script is that users can customize the 
20
+  # 'detect-language' script to return the image that's appropriate for their needs. 
21
+  # It is possible to just have new-app do code detection. In that case, just set
22
+  # the DETECTION_SCRIPT variable to an empty string.
23
+  if [[ -z $detectionScript ]]; then
24
+    oc new-app "${repoURL}"
25
+  else
26
+    if ! lang=$($(dirname $0)/${detectionScript}); then
27
+      return
28
+    fi
29
+    echo "detect: found language ${lang} for ${repoName}"
30
+    oc new-app "${lang}~${repoURL}"
21 31
   fi
32
+  # TODO: when a command to set a secret is available,
33
+  # set an optional secret on the resulting build configuration
34
+}
22 35
 
23
-  # TODO: make it easier to find the build config name created
24
-  # by oc new-app
25
-  name=$(basename "${url}")
26
-  name="${name%.*}"
36
+# If the 'oc' command is not found, exit
37
+if ! oc=$(which oc); then
38
+  echo "detect: oc is not installed"
39
+  exit 0
40
+fi
27 41
 
28
-  if ! lang=$($(dirname $0)/detect-language); then
29
-    exit 0
30
-  fi
31
-  echo "detect: found language ${lang} for ${name}"
42
+# If the current repository has no self.url, exit
43
+if ! url=$(key gitserver.self.url); then
44
+  echo "post-receive: no self url set"
45
+  exit 0
46
+fi
32 47
 
33
-  if ! oc=$(which oc); then
34
-    echo "detect: oc is not installed"
35
-    addkey openshift.io.detect 1
36
-    exit 0
37
-  fi
38
-  oc new-app "${lang}~${url}"
39
-  if webhook=$(oc start-build --list-webhooks="generic" "${name}" | head -n 1); then
40
-    addkey openshift.io.webhook "${webhook}"
41
-  fi
42
-  addkey openshift.io.detect 1
43
-}
48
+# Save received commits to a temporary file
49
+commits=$(mktemp)
50
+cat > ${commits}
44 51
 
45
-cat > /tmp/postreceived
52
+# Extract the name of the repository from the URL
53
+name=$(basename "${url}")
54
+name="${name%.*}"
46 55
 
47
-detect
56
+foundbc=false
57
+# Loop through build configurations that match this git repository
58
+while read bc ref
59
+do
60
+  foundbc=true
61
+  # If a generic webhook exists on the build config, determine if one of the received
62
+  # commits affects the reference in the build configuration
63
+  if webhook=$(oc start-build --list-webhooks="generic" "${bc}" | head -n 1); then
64
+    # Loop through the commits received by the hook. If the build configuration's git ref is an
65
+    # ancestor for the received commit, then start a new build for it.
66
+    while read old_commit new_commit ref_name
67
+    do
68
+      if $(git merge-base --is-ancestor "${ref}" "${new_commit}"); then
69
+        oc start-build --from-webhook="${webhook}" && echo "build: started on build configuration '${bc}'"
70
+        break
71
+      fi
72
+    done < "${commits}"
73
+  fi
74
+done < <(gitrepo-buildconfigs "${name}")
75
+
76
+if $foundbc; then
77
+  exit 0
78
+fi
48 79
 
49
-if webhook=$(key openshift.io.webhook); then
50
-  # TODO: print output from the server about the hook status
51
-  oc start-build --from-webhook="${webhook}"
52
-  # TODO: follow logs
53
-  echo "build: started"
80
+generate_artifacts=${GENERATE_ARTIFACTS:-}
81
+if [ "${generate_artifacts}" == "true" ]; then
82
+  detect "$url" "$name"
54 83
 fi
55 84
deleted file mode 100644
... ...
@@ -1,23 +0,0 @@
1
-package main
2
-
3
-import (
4
-	"fmt"
5
-	"log"
6
-	"os"
7
-
8
-	"github.com/openshift/origin/pkg/gitserver"
9
-)
10
-
11
-func main() {
12
-	if len(os.Args) != 1 {
13
-		fmt.Printf(`git-server - Expose Git repositories to the network
14
-
15
-%[1]s`, gitserver.EnvironmentHelp)
16
-		os.Exit(0)
17
-	}
18
-	config, err := gitserver.NewEnviromentConfig()
19
-	if err != nil {
20
-		log.Fatal(err)
21
-	}
22
-	log.Fatal(gitserver.Start(config))
23
-}
... ...
@@ -1,9 +1,12 @@
1 1
 package gitserver
2 2
 
3 3
 import (
4
+	"flag"
4 5
 	"fmt"
6
+	"io"
5 7
 	"log"
6 8
 	"net/url"
9
+	"os"
7 10
 
8 11
 	"github.com/spf13/cobra"
9 12
 	cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
... ...
@@ -20,6 +23,30 @@ and automatic creation of applications on push.
20 20
 
21 21
 %[1]s
22 22
 `
23
+const LogLevelEnv = "LOGLEVEL"
24
+const repositoryBuildConfigsDesc = `
25
+Retrieve build configs for a gitserver repository
26
+
27
+This command lists build configurations in the current namespace that correspond to a given git repository.
28
+`
29
+
30
+// CommandFor returns the appropriate command for this base name,
31
+// or the global OpenShift command
32
+func CommandFor(basename string) *cobra.Command {
33
+	var cmd *cobra.Command
34
+
35
+	out := os.Stdout
36
+
37
+	setLogLevel()
38
+
39
+	switch basename {
40
+	case "gitrepo-buildconfigs":
41
+		cmd = NewCommandRepositoryBuildConfigs(basename, out)
42
+	default:
43
+		cmd = NewCommandGitServer("gitserver")
44
+	}
45
+	return cmd
46
+}
23 47
 
24 48
 // NewCommandGitServer launches a Git server
25 49
 func NewCommandGitServer(name string) *cobra.Command {
... ...
@@ -59,3 +86,30 @@ func RunGitServer() error {
59 59
 	}
60 60
 	return gitserver.Start(config)
61 61
 }
62
+
63
+func NewCommandRepositoryBuildConfigs(name string, out io.Writer) *cobra.Command {
64
+	cmd := &cobra.Command{
65
+		Use:   fmt.Sprintf("%s REPOSITORY_NAME", name),
66
+		Short: "Retrieve build configs for a gitserver repository",
67
+		Long:  repositoryBuildConfigsDesc,
68
+		Run: func(c *cobra.Command, args []string) {
69
+			if len(args) != 1 {
70
+				err := cmdutil.UsageError(c, "This command takes a single argument - the name of the repository")
71
+				cmdutil.CheckErr(err)
72
+			}
73
+			repoName := args[0]
74
+			err := gitserver.GetRepositoryBuildConfigs(repoName, out)
75
+			cmdutil.CheckErr(err)
76
+		},
77
+	}
78
+	return cmd
79
+}
80
+
81
+func setLogLevel() {
82
+	logLevel := os.Getenv(LogLevelEnv)
83
+	if len(logLevel) > 0 {
84
+		if flag.CommandLine.Lookup("v") != nil {
85
+			flag.CommandLine.Set("v", logLevel)
86
+		}
87
+	}
88
+}
62 89
new file mode 100644
... ...
@@ -0,0 +1,68 @@
0
+package gitserver
1
+
2
+import (
3
+	"fmt"
4
+	"io"
5
+	"os"
6
+
7
+	kapi "k8s.io/kubernetes/pkg/api"
8
+	"k8s.io/kubernetes/pkg/client/restclient"
9
+
10
+	buildapi "github.com/openshift/origin/pkg/build/api"
11
+	"github.com/openshift/origin/pkg/client"
12
+)
13
+
14
+const gitRepositoryAnnotationKey = "openshift.io/git-repository"
15
+
16
+func GetRepositoryBuildConfigs(name string, out io.Writer) error {
17
+	client, err := getClient()
18
+	if err != nil {
19
+		return err
20
+	}
21
+
22
+	ns := os.Getenv("POD_NAMESPACE")
23
+	buildConfigList, err := client.BuildConfigs(ns).List(kapi.ListOptions{})
24
+	if err != nil {
25
+		return err
26
+	}
27
+
28
+	matchingBuildConfigs := []*buildapi.BuildConfig{}
29
+
30
+	for _, bc := range buildConfigList.Items {
31
+		repoAnnotation, hasAnnotation := bc.Annotations[gitRepositoryAnnotationKey]
32
+		if hasAnnotation {
33
+			if repoAnnotation == name {
34
+				matchingBuildConfigs = append(matchingBuildConfigs, &bc)
35
+			}
36
+			continue
37
+		}
38
+		if bc.Name == name {
39
+			matchingBuildConfigs = append(matchingBuildConfigs, &bc)
40
+		}
41
+	}
42
+
43
+	for _, bc := range matchingBuildConfigs {
44
+		var ref string
45
+		if bc.Spec.Source.Git != nil {
46
+			ref = bc.Spec.Source.Git.Ref
47
+		}
48
+		if ref == "" {
49
+			ref = "master"
50
+		}
51
+		fmt.Fprintf(out, "%s %s\n", bc.Name, ref)
52
+	}
53
+
54
+	return nil
55
+}
56
+
57
+func getClient() (client.Interface, error) {
58
+	clientConfig, err := restclient.InClusterConfig()
59
+	if err != nil {
60
+		return nil, fmt.Errorf("failed to get client config: %v", err)
61
+	}
62
+	osClient, err := client.New(clientConfig)
63
+	if err != nil {
64
+		return nil, fmt.Errorf("error obtaining OpenShift client: %v", err)
65
+	}
66
+	return osClient, nil
67
+}
... ...
@@ -5,7 +5,6 @@ package gitserver
5 5
 
6 6
 import (
7 7
 	"fmt"
8
-	"log"
9 8
 	"net/http"
10 9
 	"net/url"
11 10
 	"os"
... ...
@@ -16,6 +15,7 @@ import (
16 16
 
17 17
 	"github.com/AaronO/go-git-http"
18 18
 	"github.com/AaronO/go-git-http/auth"
19
+	"github.com/golang/glog"
19 20
 	"github.com/prometheus/client_golang/prometheus"
20 21
 
21 22
 	kapi "k8s.io/kubernetes/pkg/api"
... ...
@@ -35,31 +35,45 @@ GIT_HOME
35 35
   directory containing Git repositories; defaults to current directory
36 36
 PUBLIC_URL
37 37
   the url of this server for constructing URLs that point to this repository
38
+INTERNAL_URL
39
+  the url of this server that can be used internally by build configs in the cluster
38 40
 GIT_PATH
39 41
   path to Git binary; defaults to location of 'git' in PATH
40 42
 HOOK_PATH
41 43
   path to a directory containing hooks for all repositories; if not set no global hooks will be used
42 44
 ALLOW_GIT_PUSH
43
-  if 'no', pushes will be not be accepted; defaults to true
45
+  if 'false', pushes will be not be accepted; defaults to true
44 46
 ALLOW_ANON_GIT_PULL
45
-  if 'yes', pulls may be made without authorization; defaults to false
47
+  if 'true', pulls may be made without authorization; defaults to false
46 48
 ALLOW_GIT_HOOKS
47
-  if 'no', hooks cannot be read or set; defaults to true
49
+  if 'false', hooks cannot be read or set; defaults to true
48 50
 ALLOW_LAZY_CREATE
49
-  if 'no', repositories will not automatically be initialized on push; defaults to true
51
+  if 'false', repositories will not automatically be initialized on push; defaults to true
50 52
 REQUIRE_GIT_AUTH
51 53
   a user/password combination required to access the repo of the form "<user>:<password>"; defaults to none
52 54
 REQUIRE_SERVER_AUTH
53
-	a URL to an OpenShift server for verifying authorization credentials provided by a user. Requires
54
-	AUTOLINK_NAMESPACE to be set (the namespace that authorization will be checked in). Users must have
55
-	'get' on 'pods' to pull (be a viewer) and 'create' on 'pods' to push (be an editor)
55
+  a URL to an OpenShift server for verifying authorization credentials provided by a user. Requires
56
+  AUTOLINK_NAMESPACE to be set (the namespace that authorization will be checked in). Users must have
57
+  'get' on 'pods' to pull (be a viewer) and 'create' on 'pods' to push (be an editor)
56 58
 GIT_FORCE_CLEAN
57
-  if 'yes', any initial repository directories will be deleted prior to start; defaults to no
59
+  if 'true', any initial repository directories will be deleted prior to start; defaults to 'false'
58 60
   WARNING: this is destructive and you will lose any data you have already pushed
59 61
 GIT_INITIAL_CLONE_*=<url>[;<name>]
60 62
   each environment variable in this pattern will be cloned when the process starts; failures will be logged
61 63
   <name> must be [A-Z0-9_\-\.], the cloned directory name will be lowercased. If the name is invalid the
62 64
   process will halt. If the repository already exists on disk, it will be updated from the remote.
65
+AUTOLINK_KUBECONFIG
66
+  The location to read auth configuration from for autolinking.
67
+  If '-', use the service account token to link. The account
68
+  represented by this config must have the edit role on the
69
+  namespace.
70
+AUTOLINK_NAMESPACE
71
+  The namespace to autolink
72
+AUTOLINK_HOOK
73
+  The path to a script in the image to use as the default
74
+  post-receive hook - only set during link, so has no effect
75
+  on cloned repositories. See the "hooks" directory in the
76
+  image for examples.
63 77
 `
64 78
 )
65 79
 
... ...
@@ -82,9 +96,10 @@ func init() {
82 82
 
83 83
 // Config represents the configuration to use for running the server
84 84
 type Config struct {
85
-	Home      string
86
-	GitBinary string
87
-	URL       *url.URL
85
+	Home        string
86
+	GitBinary   string
87
+	URL         *url.URL
88
+	InternalURL *url.URL
88 89
 
89 90
 	AllowHooks      bool
90 91
 	AllowPush       bool
... ...
@@ -152,6 +167,14 @@ func NewEnviromentConfig() (*Config, error) {
152 152
 		config.URL = valid
153 153
 	}
154 154
 
155
+	if internalURL := os.Getenv("INTERNAL_URL"); len(internalURL) > 0 {
156
+		valid, err := url.Parse(internalURL)
157
+		if err != nil {
158
+			return nil, fmt.Errorf("INTERNAL_URL must be a valid URL: %v", err)
159
+		}
160
+		config.InternalURL = valid
161
+	}
162
+
155 163
 	gitpath := os.Getenv("GIT_PATH")
156 164
 	if len(gitpath) == 0 {
157 165
 		path, err := exec.LookPath("git")
... ...
@@ -162,9 +185,9 @@ func NewEnviromentConfig() (*Config, error) {
162 162
 	}
163 163
 	config.GitBinary = gitpath
164 164
 
165
-	config.AllowPush = os.Getenv("ALLOW_GIT_PUSH") != "no"
166
-	config.AllowHooks = os.Getenv("ALLOW_GIT_HOOKS") != "no"
167
-	config.AllowLazyCreate = os.Getenv("ALLOW_LAZY_CREATE") != "no"
165
+	config.AllowPush = os.Getenv("ALLOW_GIT_PUSH") != "false"
166
+	config.AllowHooks = os.Getenv("ALLOW_GIT_HOOKS") != "false"
167
+	config.AllowLazyCreate = os.Getenv("ALLOW_LAZY_CREATE") != "false"
168 168
 
169 169
 	if hookpath := os.Getenv("HOOK_PATH"); len(hookpath) != 0 {
170 170
 		path, err := filepath.Abs(hookpath)
... ...
@@ -177,7 +200,7 @@ func NewEnviromentConfig() (*Config, error) {
177 177
 		config.HookDirectory = path
178 178
 	}
179 179
 
180
-	allowAnonymousGet := os.Getenv("ALLOW_ANON_GIT_PULL") == "yes"
180
+	allowAnonymousGet := os.Getenv("ALLOW_ANON_GIT_PULL") == "true"
181 181
 	serverAuth := os.Getenv("REQUIRE_SERVER_AUTH")
182 182
 	gitAuth := os.Getenv("REQUIRE_GIT_AUTH")
183 183
 	if len(serverAuth) > 0 && len(gitAuth) > 0 {
... ...
@@ -206,8 +229,9 @@ func NewEnviromentConfig() (*Config, error) {
206 206
 		}
207 207
 
208 208
 		config.AuthMessage = fmt.Sprintf("Authenticating against %s allow-push=%t anon-pull=%t", cfg.Host, config.AllowPush, allowAnonymousGet)
209
-		config.AuthenticatorFn = auth.Authenticator(func(info auth.AuthInfo) (bool, error) {
209
+		authHandlerFn := auth.Authenticator(func(info auth.AuthInfo) (bool, error) {
210 210
 			if !info.Push && allowAnonymousGet {
211
+				glog.V(5).Infof("Allowing pull because anonymous get is enabled")
211 212
 				return true, nil
212 213
 			}
213 214
 			req := &authapi.LocalSubjectAccessReview{
... ...
@@ -223,6 +247,7 @@ func NewEnviromentConfig() (*Config, error) {
223 223
 				}
224 224
 				req.Action.Verb = "create"
225 225
 			}
226
+			glog.V(5).Infof("Checking for %s permission on pods", req.Action.Verb)
226 227
 			res, err := osc.ImpersonateLocalSubjectAccessReviews(namespace, info.Password).Create(req)
227 228
 			if err != nil {
228 229
 				if se, ok := err.(*errors.StatusError); ok {
... ...
@@ -230,9 +255,13 @@ func NewEnviromentConfig() (*Config, error) {
230 230
 				}
231 231
 				return false, err
232 232
 			}
233
-			//log.Printf("debug: server response allowed=%t message=%s", res.Allowed, res.Reason)
233
+			glog.V(5).Infof("server response allowed=%t message=%s", res.Allowed, res.Reason)
234 234
 			return res.Allowed, nil
235 235
 		})
236
+		if allowAnonymousGet {
237
+			authHandlerFn = anonymousHandler(authHandlerFn)
238
+		}
239
+		config.AuthenticatorFn = authHandlerFn
236 240
 	}
237 241
 
238 242
 	if len(gitAuth) > 0 {
... ...
@@ -245,13 +274,16 @@ func NewEnviromentConfig() (*Config, error) {
245 245
 		config.AuthenticatorFn = auth.Authenticator(func(info auth.AuthInfo) (bool, error) {
246 246
 			if info.Push {
247 247
 				if !config.AllowPush {
248
+					glog.V(5).Infof("Denying push request because it is disabled in config.")
248 249
 					return false, nil
249 250
 				}
250 251
 				if allowAnonymousGet {
252
+					glog.V(5).Infof("Allowing pull because anonymous get is enabled")
251 253
 					return true, nil
252 254
 				}
253 255
 			}
254 256
 			if info.Username != username || info.Password != password {
257
+				glog.V(5).Infof("Username or password doesn't match")
255 258
 				return false, nil
256 259
 			}
257 260
 			return true, nil
... ...
@@ -262,7 +294,7 @@ func NewEnviromentConfig() (*Config, error) {
262 262
 		config.Listen = value
263 263
 	}
264 264
 
265
-	config.CleanBeforeClone = os.Getenv("GIT_FORCE_CLEAN") == "yes"
265
+	config.CleanBeforeClone = os.Getenv("GIT_FORCE_CLEAN") == "true"
266 266
 
267 267
 	clones := make(map[string]Clone)
268 268
 	for _, env := range os.Environ() {
... ...
@@ -324,6 +356,8 @@ func NewEnviromentConfig() (*Config, error) {
324 324
 			return nil, fmt.Errorf("%s name %q is reserved (%v)", key, name, reservedNames)
325 325
 		}
326 326
 
327
+		glog.V(5).Infof("Adding initial clone to repo at %s", url.String())
328
+
327 329
 		clones[name] = Clone{
328 330
 			URL: *url,
329 331
 		}
... ...
@@ -333,6 +367,21 @@ func NewEnviromentConfig() (*Config, error) {
333 333
 	return config, nil
334 334
 }
335 335
 
336
+func anonymousHandler(f func(http.Handler) http.Handler) func(http.Handler) http.Handler {
337
+	return func(h http.Handler) http.Handler {
338
+		authHandler := f(h)
339
+		anonymousHandler := func(w http.ResponseWriter, req *http.Request) {
340
+			// If anonymous read request, simply serve content
341
+			if req.FormValue("service") != "git-receive-pack" && len(req.Header.Get("Authorization")) == 0 {
342
+				h.ServeHTTP(w, req)
343
+				return
344
+			}
345
+			authHandler.ServeHTTP(w, req)
346
+		}
347
+		return http.HandlerFunc(anonymousHandler)
348
+	}
349
+}
350
+
336 351
 func handler(config *Config) http.Handler {
337 352
 	git := githttp.New(config.Home)
338 353
 	git.GitBinPath = config.GitBinary
... ...
@@ -365,6 +414,7 @@ func Start(config *Config) error {
365 365
 
366 366
 	ops := http.NewServeMux()
367 367
 	if config.AllowHooks {
368
+		glog.V(5).Infof("Installing handler for the /_/hooks endpoint")
368 369
 		ops.Handle("/hooks/", prometheus.InstrumentHandler("hooks", http.StripPrefix("/hooks", hooksHandler(config))))
369 370
 	}
370 371
 	/*ops.Handle("/reflect/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
... ...
@@ -372,6 +422,7 @@ func Start(config *Config) error {
372 372
 		fmt.Fprintf(os.Stdout, "%s %s\n", r.Method, r.URL)
373 373
 		io.Copy(os.Stdout, r.Body)
374 374
 	}))*/
375
+	glog.V(5).Infof("Installing handler for the /_/metrics endpoint")
375 376
 	ops.Handle("/metrics", prometheus.UninstrumentedHandler())
376 377
 	healthz.InstallHandler(ops)
377 378
 
... ...
@@ -380,8 +431,8 @@ func Start(config *Config) error {
380 380
 	mux.Handle("/_/", http.StripPrefix("/_", ops))
381 381
 
382 382
 	if len(config.AuthMessage) > 0 {
383
-		log.Printf("%s", config.AuthMessage)
383
+		glog.Infof("%s", config.AuthMessage)
384 384
 	}
385
-	log.Printf("Serving %s on %s", config.Home, config.Listen)
385
+	glog.Infof("Serving %s on %s", config.Home, config.Listen)
386 386
 	return http.ListenAndServe(config.Listen, mux)
387 387
 }
... ...
@@ -12,6 +12,8 @@ import (
12 12
 	"regexp"
13 13
 	"strings"
14 14
 
15
+	"github.com/golang/glog"
16
+
15 17
 	"github.com/openshift/origin/pkg/generate/git"
16 18
 )
17 19
 
... ...
@@ -35,6 +37,9 @@ func lazyInitRepositoryHandler(config *Config, handler http.Handler) http.Handle
35 35
 			handler.ServeHTTP(w, r)
36 36
 			return
37 37
 		}
38
+		if !strings.HasSuffix(name, ".git") {
39
+			name += ".git"
40
+		}
38 41
 		path := filepath.Join(config.Home, name)
39 42
 		_, err := os.Stat(path)
40 43
 		if !os.IsNotExist(err) {
... ...
@@ -70,6 +75,8 @@ func lazyInitRepositoryHandler(config *Config, handler http.Handler) http.Handle
70 70
 func RepositoryURL(config *Config, name string, r *http.Request) *url.URL {
71 71
 	var url url.URL
72 72
 	switch {
73
+	case config.InternalURL != nil:
74
+		url = *config.InternalURL
73 75
 	case config.URL != nil:
74 76
 		url = *config.URL
75 77
 	case r != nil:
... ...
@@ -89,49 +96,62 @@ func newRepository(config *Config, path string, hooks map[string]string, self *u
89 89
 	var out []byte
90 90
 	repo := git.NewRepositoryForBinary(config.GitBinary)
91 91
 
92
+	barePath := path
93
+	if !strings.HasSuffix(barePath, ".git") {
94
+		barePath += ".git"
95
+	}
96
+	aliasPath := strings.TrimSuffix(barePath, ".git")
97
+
92 98
 	if origin != nil {
93
-		if err := repo.CloneMirror(path, origin.String()); err != nil {
99
+		if err := repo.CloneMirror(barePath, origin.String()); err != nil {
94 100
 			return out, err
95 101
 		}
96 102
 	} else {
97
-		if err := repo.Init(path, true); err != nil {
103
+		if err := repo.Init(barePath, true); err != nil {
98 104
 			return out, err
99 105
 		}
100 106
 	}
101 107
 
102 108
 	if self != nil {
103
-		if err := repo.AddLocalConfig(path, "gitserver.self.url", self.String()); err != nil {
109
+		if err := repo.AddLocalConfig(barePath, "gitserver.self.url", self.String()); err != nil {
104 110
 			return out, err
105 111
 		}
106 112
 	}
107 113
 
108 114
 	// remove all sample hooks, ignore errors here
109
-	if files, err := ioutil.ReadDir(filepath.Join(path, "hooks")); err == nil {
115
+	if files, err := ioutil.ReadDir(filepath.Join(barePath, "hooks")); err == nil {
110 116
 		for _, file := range files {
111
-			os.Remove(filepath.Join(path, "hooks", file.Name()))
117
+			os.Remove(filepath.Join(barePath, "hooks", file.Name()))
112 118
 		}
113 119
 	}
114 120
 
115 121
 	for name, hook := range hooks {
116
-		dest := filepath.Join(path, "hooks", name)
122
+		dest := filepath.Join(barePath, "hooks", name)
117 123
 		if err := os.Remove(dest); err != nil && !os.IsNotExist(err) {
118 124
 			return out, err
119 125
 		}
126
+		glog.V(5).Infof("Creating hook symlink %s -> %s", dest, hook)
120 127
 		if err := os.Symlink(hook, dest); err != nil {
121 128
 			return out, err
122 129
 		}
123 130
 	}
124 131
 
125 132
 	if initHook, ok := hooks["init"]; ok {
133
+		glog.V(5).Infof("Init hook exists, invoking it")
126 134
 		cmd := exec.Command(initHook)
127
-		cmd.Dir = path
135
+		cmd.Dir = barePath
128 136
 		result, err := cmd.CombinedOutput()
137
+		glog.V(5).Infof("Init output:\n%s", result)
129 138
 		if err != nil {
130 139
 			return out, fmt.Errorf("init hook failed: %v\n%s", err, string(result))
131 140
 		}
132 141
 		out = result
133 142
 	}
134 143
 
144
+	if err := os.Symlink(barePath, aliasPath); err != nil {
145
+		return out, fmt.Errorf("cannot create alias path %s: %v", aliasPath, err)
146
+	}
147
+
135 148
 	return out, nil
136 149
 }
137 150
 
... ...
@@ -183,6 +203,7 @@ func clone(config *Config) error {
183 183
 }
184 184
 
185 185
 func loadHooks(path string) (map[string]string, error) {
186
+	glog.V(5).Infof("Loading hooks from directory %s", path)
186 187
 	hooks := make(map[string]string)
187 188
 	if len(path) == 0 {
188 189
 		return hooks, nil
... ...
@@ -197,6 +218,7 @@ func loadHooks(path string) (map[string]string, error) {
197 197
 		}
198 198
 		hook := filepath.Join(path, file.Name())
199 199
 		name := filepath.Base(hook)
200
+		glog.V(5).Infof("Adding hook %s at %s", name, hook)
200 201
 		hooks[name] = hook
201 202
 	}
202 203
 	return hooks, nil