Skip to content

Commit c190acc

Browse files
committed
Submitting review 8934b9e
Upgrade the AppEngine app to AppEngine Flex instead of Managed VMs. Managed VMs was deprecated and shutdown, so we need to switch over to the GA equivalent, which is AppEngine Flex. Not every feature from Managed VMs wound up in AppEngine Flex, so we actually split the old app into two separate services: 1. An AppEngine Standard service that is responsible for the repo config UI and uses the "users" API to restrict access to project admins. 2. An AppEngine Flex service that receives webhooks from GitHub and then mirrors PR metadata into git-notes.
2 parents 91a7f9f + 0a76d0b commit c190acc

16 files changed

Lines changed: 934 additions & 746 deletions

README.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ The format written is the one defined by the
77
[git-appraise code review system](https://github.com/google/git-appraise), so pull
88
requests that are mirrored using this tool can be reviewed using git-appraise.
99

10+
## Disclaimer
11+
12+
This is not an officially supported Google product.
13+
1014
## Organization
1115

1216
There are 3 packages in this repo:
@@ -44,14 +48,15 @@ git appraise push
4448
### The Github Mirror App
4549

4650
This app allows users to continually update their git repositories with github
47-
metadata (pull requests and build statuses). It runs in a managed VM in a
48-
non-default module, and should expose a web interface at
49-
github-mirror-dot-$INSTANCE.appspot.com.
51+
metadata (pull requests and build statuses). It runs in an AppEngine app, and
52+
should expose a web interface at <PROJECT>.appspot.com.
5053

5154
It uses the app engine datastore to store its configuration.
5255

5356
To deploy:
5457

5558
```shell
56-
gcloud app deploy ./app.yaml
59+
gcloud app deploy ./app/admin/*.yaml
60+
gcloud app deploy ./app/hooks/*.yaml
61+
gcloud app deploy ./app/*.yaml
5762
```

app/admin/app.yaml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Copyright 2019 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
runtime: go
16+
api_version: go1
17+
18+
handlers:
19+
- url: /add
20+
script: _go_app
21+
login: admin
22+
23+
- url: /delete
24+
script: _go_app
25+
login: admin
26+
27+
- url: /restartOperations
28+
script: _go_app
29+
30+
- url: /
31+
script: _go_app
32+
login: admin

app/admin/cron.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
cron:
2+
- description: "hourly restart abandoned operations"
3+
url: /restartOperations
4+
schedule: every 60 mins

app/web.go renamed to app/admin/main.go

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const (
3636
idRepoToken = "repoToken"
3737
)
3838

39-
var configTemplate = template.Must(template.ParseFiles("/go/src/app/templates/config.tmpl"))
39+
var configTemplate = template.Must(template.ParseFiles("index.html"))
4040

4141
// renderRepo represents a single repository to be rendered on the page
4242
type renderRepo struct {
@@ -52,12 +52,12 @@ type renderConfig struct {
5252

5353
// configHandler renders a configuration page
5454
func configHandler(w http.ResponseWriter, req *http.Request) {
55-
c := appengine.NewContext(req)
55+
ctx := appengine.NewContext(req)
5656

5757
repos, err := getAllRepoData(appengine.NewContext(req))
5858

5959
if err != nil {
60-
log.Errorf(c, "Error fetching repos: %s", err.Error())
60+
log.Errorf(ctx, "Error fetching repos: %s", err.Error())
6161
http.Error(w, err.Error(), http.StatusInternalServerError)
6262
return
6363
}
@@ -78,104 +78,119 @@ func configHandler(w http.ResponseWriter, req *http.Request) {
7878
// addHandler handles POSTs to the /add endpoint
7979
func addHandler(w http.ResponseWriter, req *http.Request) {
8080
defer http.Redirect(w, req, "/", http.StatusSeeOther)
81-
c := appengine.NewContext(req)
81+
ctx := appengine.NewContext(req)
8282

8383
if req.Method != "POST" {
84-
log.Errorf(c, "Incorrect method for /add endpoint: %s", req.Method)
84+
log.Errorf(ctx, "Incorrect method for /add endpoint: %s", req.Method)
8585
return
8686
}
8787

8888
err := req.ParseForm()
8989
if err != nil {
90-
log.Errorf(c, "Couldn't parse form for /add endpoint: %s", err.Error())
90+
log.Errorf(ctx, "Couldn't parse form for /add endpoint: %s", err.Error())
9191
return
9292
}
9393

9494
repoName := req.PostForm.Get(idRepoName)
9595
if repoName == "" {
96-
log.Errorf(c, "No repoName for /add endpoint: %v", req.PostForm)
96+
log.Errorf(ctx, "No repoName for /add endpoint: %v", req.PostForm)
9797
return
9898
}
9999

100100
repoToken := req.PostForm.Get(idRepoToken)
101101
if repoToken == "" {
102-
log.Errorf(c, "No repoToken for /add endpoint: %v", req.PostForm)
102+
log.Errorf(ctx, "No repoToken for /add endpoint: %v", req.PostForm)
103103
return
104104
}
105105

106106
splitName := strings.Split(repoName, "/")
107107
if len(splitName) != 2 {
108-
log.Errorf(c, "Invalid repository name (can't split on '/'): %s", repoName)
108+
log.Errorf(ctx, "Invalid repository name (can't split on '/'): %s", repoName)
109109
return
110110
}
111111

112-
log.Infof(c, "Adding repository %s", repoName)
112+
log.Infof(ctx, "Adding repository %s", repoName)
113113

114-
err = initRepoData(c, splitName[0], splitName[1], repoToken)
114+
err = initRepoData(ctx, splitName[0], splitName[1], repoToken)
115115

116116
if err != nil {
117-
log.Errorf(c, "Couldn't store repository %s: %s", repoName, err.Error())
117+
log.Errorf(ctx, "Couldn't store repository %s: %s", repoName, err.Error())
118118
return
119119
}
120120

121-
go validate(splitName[0], splitName[1])
121+
validate(ctx, splitName[0], splitName[1])
122122
}
123123

124124
// deleteHandler handles POSTS to the /delete endpoint
125125
func deleteHandler(w http.ResponseWriter, req *http.Request) {
126126
defer http.Redirect(w, req, "/", http.StatusSeeOther)
127-
c := appengine.NewContext(req)
127+
ctx := appengine.NewContext(req)
128128

129129
if req.Method != "POST" {
130-
log.Errorf(c, "Incorrect method for /delete endpoint: %s", req.Method)
130+
log.Errorf(ctx, "Incorrect method for /delete endpoint: %s", req.Method)
131131
return
132132
}
133133

134134
err := req.ParseForm()
135135
if err != nil {
136-
log.Errorf(c, "Couldn't parse form for /delete endpoint: %s", err.Error())
136+
log.Errorf(ctx, "Couldn't parse form for /delete endpoint: %s", err.Error())
137137
return
138138
}
139139

140140
fullRepoName := req.PostForm.Get(idRepoName)
141141
if fullRepoName == "" {
142-
log.Errorf(c, "No repoName for /delete endpoint: %v", req.PostForm)
142+
log.Errorf(ctx, "No repoName for /delete endpoint: %v", req.PostForm)
143143
return
144144
}
145145

146146
splitName := strings.Split(fullRepoName, "/")
147147
if len(splitName) != 2 {
148-
log.Errorf(c, "Invalid repository name (can't split on '/'): %s", fullRepoName)
148+
log.Errorf(ctx, "Invalid repository name (can't split on '/'): %s", fullRepoName)
149149
return
150150
}
151151

152-
go deactivate(splitName[0], splitName[1])
152+
deactivate(ctx, splitName[0], splitName[1])
153+
}
154+
155+
func restartOperationsHandler(w http.ResponseWriter, req *http.Request) {
156+
ctx := appengine.NewContext(req)
157+
restartAbandonedOperations(ctx)
158+
w.Write([]byte("done"))
153159
}
154160

155161
// enforceLoginHandler wraps another handler, returning a handler that will
156162
// enforce user login and then pass off control down the chain.
157163
func enforceLoginHandler(next http.Handler) http.Handler {
158164
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
159-
c := appengine.NewContext(req)
160-
u := user.Current(c)
165+
ctx := appengine.NewContext(req)
166+
u := user.Current(ctx)
161167
if u == nil {
162168
// Not logged in
163-
url, err := user.LoginURL(c, req.URL.String())
169+
url, err := user.LoginURL(ctx, req.URL.String())
164170
if err != nil {
165171
http.Error(w, err.Error(), http.StatusInternalServerError)
166172
return
167173
}
168174
http.Redirect(w, req, url, http.StatusSeeOther)
169175
return
170176
}
177+
178+
// Ensure that the persistent storage is set up before continuing...
179+
initStorage(ctx)
180+
171181
// Pass off control
172182
next.ServeHTTP(w, req)
173183
})
174184
}
175185

176186
func setupHandlers() {
177-
setupHookHandlers()
178187
http.Handle("/add", enforceLoginHandler(http.HandlerFunc(addHandler)))
179188
http.Handle("/delete", enforceLoginHandler(http.HandlerFunc(deleteHandler)))
189+
http.Handle("/restartOperations", http.HandlerFunc(restartOperationsHandler))
180190
http.Handle("/", enforceLoginHandler(http.HandlerFunc(configHandler)))
181191
}
192+
193+
func main() {
194+
setupHandlers()
195+
appengine.Main()
196+
}

0 commit comments

Comments
 (0)