126 |
You’re almost there!
127 |
128 | Unfortunately with the new Chrome 85, GitHub temporarily breaks our
129 | ability to determine which GitHub repository you came from. (You can
130 | help us ask GitHub to fix it!)
132 |
133 |
134 | Please provide the URL of the previous page you came from:
135 |
136 |
142 |
143 |
144 | `, r.URL.Query().Encode())
145 | }
146 |
147 | // TODO(ahmetb): remove once https://github.community/t/chrome-85-breaks-referer/130039 is fixed
148 | func manualRedirect(w http.ResponseWriter, req *http.Request) {
149 | refURL := req.FormValue("url")
150 | origQuery, err := url.ParseQuery(req.FormValue("orig_query"))
151 | if err != nil {
152 | w.WriteHeader(http.StatusBadRequest)
153 | fmt.Fprintf(w, errors.Wrapf(err, "failed to parse orig_query=%q: %v", origQuery, err).Error())
154 | return
155 | }
156 | repo, err := parseReferer(refURL, availableExtractors)
157 | if err != nil {
158 | w.WriteHeader(http.StatusBadRequest)
159 | fmt.Fprintf(w, errors.Wrapf(err, "failed to parse url into a github repository: %s", refURL).Error())
160 | return
161 | }
162 | doRedirect(w, repo, origQuery)
163 | }
164 |
165 | type respRecorder struct {
166 | w http.ResponseWriter
167 | status int
168 | }
169 |
170 | func (rr *respRecorder) Header() http.Header { return rr.w.Header() }
171 | func (rr *respRecorder) Write(p []byte) (int, error) { return rr.w.Write(p) }
172 | func (rr *respRecorder) WriteHeader(statusCode int) {
173 | rr.status = statusCode
174 | rr.w.WriteHeader(statusCode)
175 | }
176 |
--------------------------------------------------------------------------------
/cmd/cloudshell_open/appfile.go:
--------------------------------------------------------------------------------
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 | package main
16 |
17 | import (
18 | "crypto/rand"
19 | "encoding/base64"
20 | "encoding/json"
21 | "fmt"
22 | "io"
23 | "os"
24 | "path/filepath"
25 | "sort"
26 |
27 | "github.com/fatih/color"
28 |
29 | "github.com/AlecAivazis/survey/v2"
30 | )
31 |
32 | type env struct {
33 | Description string `json:"description"`
34 | Value string `json:"value"`
35 | Required *bool `json:"required"`
36 | Generator string `json:"generator"`
37 | Order *int `json:"order"`
38 | }
39 |
40 | type options struct {
41 | AllowUnauthenticated *bool `json:"allow-unauthenticated"`
42 | Memory string `json:"memory"`
43 | CPU string `json:"cpu"`
44 | Port int `json:"port"`
45 | HTTP2 *bool `json:"http2"`
46 | Concurrency int `json:"concurrency"`
47 | MaxInstances int `json:"max-instances"`
48 | }
49 |
50 | type hook struct {
51 | Commands []string `json:"commands"`
52 | }
53 |
54 | type buildpacks struct {
55 | Builder string `json:"builder"`
56 | }
57 |
58 | type build struct {
59 | Skip *bool `json:"skip"`
60 | Buildpacks buildpacks `json:"buildpacks"`
61 | }
62 |
63 | type hooks struct {
64 | PreCreate hook `json:"precreate"`
65 | PostCreate hook `json:"postcreate"`
66 | PreBuild hook `json:"prebuild"`
67 | PostBuild hook `json:"postbuild"`
68 | }
69 |
70 | type appFile struct {
71 | Name string `json:"name"`
72 | Env map[string]env `json:"env"`
73 | Options options `json:"options"`
74 | Build build `json:"build"`
75 | Hooks hooks `json:"hooks"`
76 |
77 | // The following are unused variables that are still silently accepted
78 | // for compatibility with Heroku app.json files.
79 | IgnoredDescription string `json:"description"`
80 | IgnoredKeywords []string `json:"keywords"`
81 | IgnoredLogo string `json:"logo"`
82 | IgnoredRepository string `json:"repository"`
83 | IgnoredWebsite string `json:"website"`
84 | IgnoredStack string `json:"stack"`
85 | IgnoredFormation interface{} `json:"formation"`
86 | }
87 |
88 | const appJSON = `app.json`
89 |
90 | // hasAppFile checks if the directory has an app.json file.
91 | func hasAppFile(dir string) (bool, error) {
92 | path := filepath.Join(dir, appJSON)
93 | fi, err := os.Stat(path)
94 | if err != nil {
95 | if os.IsNotExist(err) {
96 | return false, nil
97 | }
98 | return false, err
99 | }
100 | return fi.Mode().IsRegular(), nil
101 | }
102 |
103 | func parseAppFile(r io.Reader) (*appFile, error) {
104 | var v appFile
105 | d := json.NewDecoder(r)
106 | d.DisallowUnknownFields()
107 | if err := d.Decode(&v); err != nil {
108 | return nil, fmt.Errorf("failed to parse app.json: %+v", err)
109 | }
110 |
111 | // make "required" true by default
112 | for k, env := range v.Env {
113 | if env.Required == nil {
114 | v := true
115 | env.Required = &v
116 | }
117 | v.Env[k] = env
118 | }
119 |
120 | for k, env := range v.Env {
121 | if env.Generator == "secret" && env.Value != "" {
122 | return nil, fmt.Errorf("env var %q can't have both a value and use the secret generator", k)
123 | }
124 | }
125 |
126 | return &v, nil
127 | }
128 |
129 | // getAppFile returns the parsed app.json in the directory if it exists,
130 | // otherwise returns a zero appFile.
131 | func getAppFile(dir string) (appFile, error) {
132 | var v appFile
133 | ok, err := hasAppFile(dir)
134 | if err != nil {
135 | return v, err
136 | }
137 | if !ok {
138 | return v, nil
139 | }
140 | f, err := os.Open(filepath.Join(dir, appJSON))
141 | if err != nil {
142 | return v, fmt.Errorf("error opening app.json file: %v", err)
143 | }
144 | defer f.Close()
145 | af, err := parseAppFile(f)
146 | if err != nil {
147 | return v, fmt.Errorf("failed to parse app.json file: %v", err)
148 | }
149 | return *af, nil
150 | }
151 |
152 | func rand64String() (string, error) {
153 | b := make([]byte, 64)
154 | if _, err := rand.Read(b); err != nil {
155 | return "", err
156 | }
157 | return base64.StdEncoding.EncodeToString(b), nil
158 | }
159 |
160 | // takes the envs defined in app.json, and the existing envs and returns the new envs that need to be prompted for
161 | func needEnvs(list map[string]env, existing map[string]struct{}) map[string]env {
162 | for k := range list {
163 | _, isPresent := existing[k]
164 | if isPresent {
165 | delete(list, k)
166 | }
167 | }
168 |
169 | return list
170 | }
171 |
172 | func promptOrGenerateEnvs(list map[string]env) ([]string, error) {
173 | var toGenerate []string
174 | var toPrompt = make(map[string]env)
175 |
176 | for k, e := range list {
177 | if e.Generator == "secret" {
178 | toGenerate = append(toGenerate, k)
179 | } else {
180 | toPrompt[k] = e
181 | }
182 | }
183 |
184 | generated, err := generateEnvs(toGenerate)
185 | if err != nil {
186 | return nil, err
187 | }
188 |
189 | prompted, err := promptEnv(toPrompt)
190 | if err != nil {
191 | return nil, err
192 | }
193 |
194 | return append(generated, prompted...), nil
195 | }
196 |
197 | func generateEnvs(keys []string) ([]string, error) {
198 | for i, key := range keys {
199 | resp, err := rand64String()
200 | if err != nil {
201 | return nil, fmt.Errorf("failed to generate secret for %s : %v", key, err)
202 | }
203 | keys[i] = key + "=" + resp
204 | }
205 |
206 | return keys, nil
207 | }
208 |
209 | type envKeyValuePair struct {
210 | k string
211 | v env
212 | }
213 |
214 | type envKeyValuePairs []envKeyValuePair
215 |
216 | func (e envKeyValuePairs) Len() int { return len(e) }
217 |
218 | func (e envKeyValuePairs) Swap(i, j int) {
219 | e[i], e[j] = e[j], e[i]
220 | }
221 |
222 | func (e envKeyValuePairs) Less(i, j int) bool {
223 | // if env.Order is unspecified, it should appear less.
224 | // otherwise, less values show earlier.
225 | if e[i].v.Order == nil {
226 | return false
227 | }
228 | if e[j].v.Order == nil {
229 | return true
230 | }
231 | return *e[i].v.Order < *e[j].v.Order
232 | }
233 |
234 | func sortedEnvs(envs map[string]env) []string {
235 | var v envKeyValuePairs
236 | for key, value := range envs {
237 | v = append(v, envKeyValuePair{key, value})
238 | }
239 | sort.Sort(v)
240 | var keys []string
241 | for _, vv := range v {
242 | keys = append(keys, vv.k)
243 | }
244 | return keys
245 | }
246 |
247 | func promptEnv(list map[string]env) ([]string, error) {
248 | var out []string
249 | sortedKeys := sortedEnvs(list)
250 |
251 | for _, k := range sortedKeys {
252 | e := list[k]
253 | var resp string
254 |
255 | if err := survey.AskOne(&survey.Input{
256 | Message: fmt.Sprintf("Value of %s environment variable (%s)",
257 | color.CyanString(k),
258 | color.HiBlackString(e.Description)),
259 | Default: e.Value,
260 | }, &resp,
261 | survey.WithValidator(survey.Required),
262 | surveyIconOpts,
263 | ); err != nil {
264 | return nil, fmt.Errorf("failed to get a response for environment variable %s", k)
265 | }
266 | out = append(out, k+"="+resp)
267 | }
268 |
269 | return out, nil
270 | }
271 |
--------------------------------------------------------------------------------
/cmd/cloudshell_open/appfile_test.go:
--------------------------------------------------------------------------------
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 | package main
16 |
17 | import (
18 | "io/ioutil"
19 | "os"
20 | "path/filepath"
21 | "reflect"
22 | "strings"
23 | "testing"
24 | )
25 |
26 | var (
27 | // used as convenience to take their reference in tests
28 | tru = true
29 | fals = false
30 | )
31 |
32 | func TestHasAppFile(t *testing.T) {
33 | // non existing dir
34 | ok, err := hasAppFile(filepath.Join(os.TempDir(), "non-existing-dir"))
35 | if err != nil {
36 | t.Fatalf("got err for non-existing dir: %v", err)
37 | } else if ok {
38 | t.Fatal("returned true for non-existing dir")
39 | }
40 |
41 | tmpDir, err := ioutil.TempDir(os.TempDir(), "app.json-test")
42 | if err != nil {
43 | t.Fatal(err)
44 | }
45 | defer os.RemoveAll(tmpDir)
46 |
47 | // no file
48 | ok, err = hasAppFile(tmpDir)
49 | if err != nil {
50 | t.Fatalf("failed check for dir %s", tmpDir)
51 | } else if ok {
52 | t.Fatalf("returned false when app.json doesn't exist in dir %s", tmpDir)
53 | }
54 |
55 | // if app.json is a dir, must return false
56 | appJSON := filepath.Join(tmpDir, "app.json")
57 | if err := os.Mkdir(appJSON, 0755); err != nil {
58 | t.Fatal(err)
59 | }
60 | ok, err = hasAppFile(tmpDir)
61 | if err != nil {
62 | t.Fatal(err)
63 | } else if ok {
64 | t.Fatalf("reported true when app.json is a dir")
65 | }
66 | if err := os.RemoveAll(appJSON); err != nil {
67 | t.Fatal(err)
68 | }
69 |
70 | // write file
71 | if err := ioutil.WriteFile(appJSON, []byte(`{}`), 0644); err != nil {
72 | t.Fatal(err)
73 | }
74 | ok, err = hasAppFile(tmpDir)
75 | if err != nil {
76 | t.Fatal(err)
77 | } else if !ok {
78 | t.Fatalf("reported false when app.json exists")
79 | }
80 | }
81 |
82 | func Test_parseAppFile(t *testing.T) {
83 |
84 | tests := []struct {
85 | name string
86 | args string
87 | want *appFile
88 | wantErr bool
89 | }{
90 | {"empty json is EOF", ``, nil, true},
91 | {"empty object ok", `{}`, &appFile{}, false},
92 | {"bad object at root", `1`, nil, true},
93 | {"unknown field", `{"foo":"bar"}`, nil, true},
94 | {"allow-unauthenticated true", `{"options": {"allow-unauthenticated": true}}`,
95 | &appFile{Options: options{AllowUnauthenticated: &tru}}, false},
96 | {"wrong env type", `{"env": "foo"}`, nil, true},
97 | {"wrong env value type", `{"env": {"foo":"bar"}}`, nil, true},
98 | {"env not array", `{"env": []}`, nil, true},
99 | {"empty env list is ok", `{"env": {}}`, &appFile{Env: map[string]env{}}, false},
100 | {"non-string key type in env", `{"env": {
101 | 1: {}
102 | }}`, nil, true},
103 | {"unknown field in env", `{"env": {
104 | "KEY": {"unknown":"value"}
105 | }}`, nil, true},
106 | {"required is true by default", `{
107 | "env": {"KEY":{}}}`, &appFile{Env: map[string]env{
108 | "KEY": {Required: &tru}}}, false},
109 | {"required can be set to false", `{
110 | "env": {"KEY":{"required":false}}}`, &appFile{
111 | Env: map[string]env{"KEY": {Required: &fals}}}, false},
112 | {"required has to be bool", `{
113 | "env": {"KEY":{"required": "false"}}}`, nil, true},
114 | {"value has to be string", `{
115 | "env": {"KEY":{"value": 100}}}`, nil, true},
116 | {"generator secret", `{
117 | "env": {"KEY":{"generator": "secret"}}}`, &appFile{Env: map[string]env{
118 | "KEY": {Required: &tru, Generator: "secret"}}}, false},
119 | {"generator secret and value", `{
120 | "env": {"KEY":{"generator": "secret", "value": "asdf"}}}`, nil, true},
121 | {"parses ok", `{
122 | "name": "foo",
123 | "env": {
124 | "KEY_1": {
125 | "required": false,
126 | "description": "key 1 is cool"
127 | },
128 | "KEY_2": {
129 | "value": "k2"
130 | }
131 | }}`,
132 | &appFile{
133 | Name: "foo",
134 | Options: options{},
135 | Env: map[string]env{
136 | "KEY_1": {
137 | Required: &fals,
138 | Description: "key 1 is cool",
139 | },
140 | "KEY_2": {
141 | Value: "k2",
142 | Required: &tru,
143 | },
144 | }}, false},
145 | {"precreate", `{
146 | "hooks": {
147 | "precreate": {
148 | "commands": [
149 | "echo pre",
150 | "date"
151 | ]
152 | }
153 | }}`, &appFile{Hooks: hooks{PreCreate: hook{Commands: []string{"echo pre", "date"}}}}, false},
154 | {"postcreate", `{
155 | "hooks": {
156 | "postcreate": {
157 | "commands": [
158 | "echo post"
159 | ]
160 | }
161 | }}`, &appFile{Hooks: hooks{PostCreate: hook{Commands: []string{"echo post"}}}}, false},
162 | }
163 | for _, tt := range tests {
164 | t.Run(tt.name, func(t *testing.T) {
165 | got, err := parseAppFile(strings.NewReader(tt.args))
166 | if (err != nil) != tt.wantErr {
167 | t.Errorf("parseAppFile() error = %v, wantErr %v", err, tt.wantErr)
168 | return
169 | }
170 | if !reflect.DeepEqual(got, tt.want) {
171 | t.Errorf("parseAppFile() = %v, want %v", got, tt.want)
172 | }
173 | })
174 | }
175 | }
176 |
177 | func Test_parseAppFile_parsesIgnoredKnownFields(t *testing.T) {
178 | appFile := `{
179 | "description": "String",
180 | "repository": "URL",
181 | "logo": "URL",
182 | "website": "URL",
183 | "keywords": ["String", "String"]
184 | }`
185 | _, err := parseAppFile(strings.NewReader(appFile))
186 | if err != nil {
187 | t.Fatalf("app.json with ignored but known fields failed: %v", err)
188 | }
189 | }
190 |
191 | func TestGetAppFile(t *testing.T) {
192 | dir, err := ioutil.TempDir(os.TempDir(), "app.json-test")
193 | if err != nil {
194 | t.Fatal(err)
195 | }
196 | defer os.RemoveAll(dir)
197 |
198 | // non existing file must return zero
199 | v, err := getAppFile(dir)
200 | if err != nil {
201 | t.Fatal(err)
202 | }
203 | var zero appFile
204 | if !reflect.DeepEqual(v, zero) {
205 | t.Fatalf("not zero value: got=%#v, expected=%#v", v, zero)
206 | }
207 |
208 | // captures parse error
209 | if err := ioutil.WriteFile(filepath.Join(dir, "app.json"), []byte(`
210 | {"env": {"KEY": 1 }}
211 | `), 0644); err != nil {
212 | t.Fatal(err)
213 | }
214 | if _, err := getAppFile(dir); err == nil {
215 | t.Fatal("was expected to fail with invalid json")
216 | }
217 |
218 | // parse valid file
219 | if err := ioutil.WriteFile(filepath.Join(dir, "app.json"), []byte(`
220 | {"env": {"KEY": {"value":"bar"} }}
221 | `), 0644); err != nil {
222 | t.Fatal(err)
223 | }
224 |
225 | v, err = getAppFile(dir)
226 | if err != nil {
227 | t.Fatal(err)
228 | }
229 | expected := appFile{Env: map[string]env{
230 | "KEY": {Value: "bar", Required: &tru},
231 | }}
232 | if !reflect.DeepEqual(v, expected) {
233 | t.Fatalf("wrong parsed value: got=%#v, expected=%#v", v, expected)
234 | }
235 | }
236 |
237 | func Test_sortedEnvs(t *testing.T) {
238 | envs := map[string]env{
239 | "NIL_ORDER": {},
240 | "ORDER_100": {Order: mkInt(100)},
241 | "ORDER_0": {Order: mkInt(0)},
242 | "ORDER_-10": {Order: mkInt(-10)},
243 | "ORDER_50": {Order: mkInt(50)},
244 | }
245 | got := sortedEnvs(envs)
246 | expected := []string{
247 | "ORDER_-10", "ORDER_0", "ORDER_50", "ORDER_100", "NIL_ORDER",
248 | }
249 |
250 | if !reflect.DeepEqual(got, expected) {
251 | t.Fatalf("sorted envs in wrong order: expected:%v\ngot=%v", expected, got)
252 | }
253 | }
254 |
255 | func mkInt(i int) *int {
256 | return &i
257 | }
258 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Cloud Run Button
2 |
3 | If you have a public repository, you can add this button to your `README.md` and
4 | let anyone deploy your application to [Google Cloud Run][run] with a single
5 | click.
6 |
7 | [run]: https://cloud.google.com/run
8 |
9 | Try it out with a "hello, world" Go application ([source](https://github.com/GoogleCloudPlatform/cloud-run-hello)):
10 |
11 | [](https://deploy.cloud.run/?git_repo=https://github.com/GoogleCloudPlatform/cloud-run-hello.git)
13 |
14 | ### Demo
15 |
16 | [](https://storage.googleapis.com/cloudrun/cloud-run-button.gif)
17 |
18 | ### Add the Cloud Run Button to Your Repo's README
19 |
20 | 1. Copy & paste this markdown:
21 |
22 | ```text
23 | [](https://deploy.cloud.run)
24 | ```
25 |
26 | 1. If the repo contains a `Dockerfile`, it will be built using the `docker build` command. If the repo uses Maven for
27 | the build and it contains the [Jib plugin](https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin),
28 | then the container image will be built with Jib
29 | ([Jib Spring Boot Sample](https://github.com/GoogleContainerTools/jib/tree/master/examples/spring-boot)). Otherwise,
30 | [CNCF Buildpacks](https://buildpacks.io/) (i.e. the `pack build` command) will attempt to build the repo
31 | ([buildpack samples][buildpack-samples]). Alternatively, you can skip these built-in build methods using the
32 | `build.skip` field (see below) and use a `prebuild` or `postbuild` hook to build the container image yourself.
33 |
34 | [buildpack-samples]: https://github.com/GoogleCloudPlatform/buildpack-samples
35 |
36 | ### Customizing source repository parameters
37 |
38 | - When no parameters are passed, the referer is used to detect the git repo and branch
39 | - To specify a git repo, add a `git_repo=URL` query parameter
40 | - To specify a git branch, add a `revision=BRANCH_NAME` query parameter.
41 | - To run the build in a subdirectory of the repo, add a `dir=SUBDIR` query parameter.
42 |
43 |
44 | ### Customizing deployment parameters
45 |
46 | If you include an `app.json` at the root of your repository, it allows you
47 | customize the experience such as defining an alternative service name, or
48 | prompting for additional environment variables.
49 |
50 | For example, a fully populated `app.json` file looks like this:
51 |
52 | ```json
53 | {
54 | "name": "foo-app",
55 | "env": {
56 | "BACKGROUND_COLOR": {
57 | "description": "specify a css color",
58 | "value": "#fefefe",
59 | "required": false
60 | },
61 | "TITLE": {
62 | "description": "title for your site"
63 | },
64 | "APP_SECRET": {
65 | "generator": "secret"
66 | },
67 | "ORDERED_ENV": {
68 | "description": "control the order env variables are prompted",
69 | "order": 100
70 | }
71 | },
72 | "options": {
73 | "allow-unauthenticated": false,
74 | "memory": "512Mi",
75 | "cpu": "1",
76 | "port": 80,
77 | "http2": false,
78 | "concurrency": 80,
79 | "max-instances": 10
80 | },
81 | "build": {
82 | "skip": false,
83 | "buildpacks": {
84 | "builder": "some/builderimage"
85 | }
86 | },
87 | "hooks": {
88 | "prebuild": {
89 | "commands": [
90 | "./my-custom-prebuild"
91 | ]
92 | },
93 | "postbuild": {
94 | "commands": [
95 | "./my-custom-postbuild"
96 | ]
97 | },
98 | "precreate": {
99 | "commands": [
100 | "echo 'test'"
101 | ]
102 | },
103 | "postcreate": {
104 | "commands": [
105 | "./setup.sh"
106 | ]
107 | }
108 | }
109 | }
110 | ```
111 |
112 | Reference:
113 |
114 | - `name`: _(optional, default: repo name, or sub-directory name if specified)_
115 | Name of the Cloud Run service and the built container image. Not validated for
116 | naming restrictions.
117 | - `env`: _(optional)_ Prompt user for environment variables.
118 | - `description`: _(optional)_ short explanation of what the environment
119 | variable does, keep this short to make sure it fits into a line.
120 | - `value`: _(optional)_ default value for the variable, should be a string.
121 | - `required`, _(optional, default: `true`)_ indicates if they user must provide
122 | a value for this variable.
123 | - `generator`, _(optional)_ use a generator for the value, currently only support `secret`
124 | - `order`, _(optional)_ if specified, used to indicate the order in which the
125 | variable is prompted to the user. If some variables specify this and some
126 | don't, then the unspecified ones are prompted last.
127 | - `options`: _(optional)_ Options when deploying the service
128 | - `allow-unauthenticated`: _(optional, default: `true`)_ allow unauthenticated requests
129 | - `memory`: _(optional)_ memory for each instance
130 | - `cpu`: _(optional)_ cpu for each instance
131 | - `port`: _(optional)_ if your application doesn't respect the PORT environment
132 | variable provided by Cloud Run, specify the port number it listens on
133 | - `http2`: _(optional)_ use http2 for the connection
134 | - `concurrency`: _(optional)_ concurrent requests for each instance
135 | - `max-instances`: _(optional)_ autoscaling limit (max 1000)
136 | - `build`: _(optional)_ Build configuration
137 | - `skip`: _(optional, default: `false`)_ skips the built-in build methods (`docker build`, `Maven Jib`, and
138 | `buildpacks`), but still allows for `prebuild` and `postbuild` hooks to be run in order to build the container image
139 | manually
140 | - `buildpacks`: _(optional)_ buildpacks config (Note: Additional Buildpack config can be specified using a `project.toml` file. [See the spec for details](https://buildpacks.io/docs/reference/config/project-descriptor/).)
141 | - `builder`: _(optional, default: `gcr.io/buildpacks/builder:v1`)_ overrides the buildpack builder image
142 | - `hooks`: _(optional)_ Run commands in separate bash shells with the environment variables configured for the
143 | application and environment variables `GOOGLE_CLOUD_PROJECT` (Google Cloud project), `GOOGLE_CLOUD_REGION`
144 | (selected Google Cloud Region), `K_SERVICE` (Cloud Run service name), `IMAGE_URL` (container image URL), `APP_DIR`
145 | (application directory). Command outputs are shown as they are executed.
146 | - `prebuild`: _(optional)_ Runs the specified commands before running the built-in build methods. Use the `IMAGE_URL`
147 | environment variable to determine the container image name you need to build.
148 | - `commands`: _(array of strings)_ The list of commands to run
149 | - `postbuild`: _(optional)_ Runs the specified commands after running the built-in build methods. Use the `IMAGE_URL`
150 | environment variable to determine the container image name you need to build.
151 | - `commands`: _(array of strings)_ The list of commands to run
152 | - `precreate`: _(optional)_ Runs the specified commands before the service has been created
153 | - `commands`: _(array of strings)_ The list of commands to run
154 | - `postcreate`: _(optional)_ Runs the specified commands after the service has been created; the `SERVICE_URL` environment variable provides the URL of the deployed Cloud Run service
155 | - `commands`: _(array of strings)_ The list of commands to run
156 |
157 | ### Notes
158 |
159 | - Disclaimer: This is not an officially supported Google product.
160 | - See [LICENSE](./LICENSE) for the licensing information.
161 | - See [Contribution Guidelines](./CONTRIBUTING.md) on how to contribute.
162 |
--------------------------------------------------------------------------------
/tests/run_integration_test.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import click
3 | import os
4 | import shutil
5 | import subprocess
6 | import time
7 | from urllib import request, error
8 |
9 | from googleapiclient.discovery import build as api
10 |
11 |
12 | GIT_URL = os.environ.get(
13 | "GIT_URL", "https://github.com/GoogleCloudPlatform/cloud-run-button"
14 | )
15 | GIT_BRANCH = os.environ.get("GIT_BRANCH", "master")
16 | TESTS_DIR = "tests"
17 |
18 | # Keep to Python 3.7 systems (gcloud image currently Python 3.7.3)
19 | GOOGLE_CLOUD_PROJECT = os.environ.get("GOOGLE_CLOUD_PROJECT", None)
20 | if not GOOGLE_CLOUD_PROJECT:
21 | raise Exception("'GOOGLE_CLOUD_PROJECT' env var not found")
22 |
23 | GOOGLE_CLOUD_REGION = os.environ.get("GOOGLE_CLOUD_REGION", None)
24 | if not GOOGLE_CLOUD_REGION:
25 | raise Exception("'GOOGLE_CLOUD_REGION' env var not found")
26 |
27 | WORKING_DIR = os.environ.get("WORKING_DIR", ".")
28 |
29 | DEBUG = os.environ.get("DEBUG", False)
30 | if DEBUG == "":
31 | DEBUG = False
32 |
33 | ###############################################################################
34 |
35 |
36 | def debugging(*args):
37 | c = click.get_current_context()
38 | output = " ".join([str(k) for k in args])
39 | if DEBUG:
40 | print(f"🐞 {output}")
41 |
42 |
43 | def print_help_msg(command):
44 | with click.Context(command) as ctx:
45 | click.echo(command.get_help(ctx))
46 |
47 |
48 | def gcloud(*args):
49 | """Invoke the gcloud executable"""
50 | return run_shell(
51 | ["gcloud"]
52 | + list(args)
53 | + [
54 | "--platform",
55 | "managed",
56 | "--project",
57 | GOOGLE_CLOUD_PROJECT,
58 | "--region",
59 | GOOGLE_CLOUD_REGION,
60 | ]
61 | )
62 |
63 |
64 | def cloudshell_open(directory, repo_url, git_branch):
65 | """Invoke the cloudshell_open executable."""
66 | params = [
67 | f"{WORKING_DIR}/cloudshell_open",
68 | f"--repo_url={repo_url}",
69 | f"--git_branch={git_branch}",
70 | ]
71 |
72 | if directory:
73 | params += [f"--dir={TESTS_DIR}/{directory}"]
74 | return run_shell(params)
75 |
76 |
77 | def run_shell(params):
78 | """Invoke the given subproceess, capturing output status and returning stdout"""
79 | debugging("Running:", " ".join(params))
80 |
81 | env = {}
82 | env.update(os.environ)
83 | env.update({"TRUSTED_ENVIRONMENT": "true", "SKIP_CLONE_REPORTING": "true"})
84 |
85 | resp = subprocess.run(params, capture_output=True, env=env)
86 |
87 | output = resp.stdout.decode("utf-8")
88 | error = resp.stderr.decode("utf-8")
89 |
90 | if DEBUG:
91 | debugging("stdout:", output or "