├── .gitignore ├── LICENSE ├── README.md ├── bin ├── cover ├── generate-coverage-html └── test ├── cache.go ├── cache_test.go ├── cloudbuild.yaml ├── cloudrun.go ├── discovery.go ├── discovery_test.go ├── doc.go ├── env.go ├── env_test.go ├── example_test.go ├── go.mod ├── go.sum ├── health_checks.go ├── health_checks_test.go ├── http.go ├── http_test.go ├── integration ├── Dockerfile ├── backend │ ├── Dockerfile │ ├── go.mod │ ├── main.go │ └── vendor │ │ ├── github.com │ │ └── kelseyhightower │ │ │ └── run │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── cache.go │ │ │ ├── cloudbuild.yaml │ │ │ ├── cloudrun.go │ │ │ ├── doc.go │ │ │ ├── env.go │ │ │ ├── go.mod │ │ │ ├── http.go │ │ │ ├── log.go │ │ │ ├── metadata.go │ │ │ ├── secrets.go │ │ │ └── useragent.go │ │ └── modules.txt ├── bin │ ├── build │ ├── create-secrets │ ├── create-service-accounts │ ├── deploy │ └── test ├── env.go ├── go.mod ├── main.go ├── metadata.go ├── secrets.go ├── service-authentication.go └── vendor │ ├── github.com │ └── kelseyhightower │ │ └── run │ │ ├── LICENSE │ │ ├── README.md │ │ ├── cache.go │ │ ├── cloudbuild.yaml │ │ ├── cloudrun.go │ │ ├── doc.go │ │ ├── env.go │ │ ├── go.mod │ │ ├── http.go │ │ ├── log.go │ │ ├── metadata.go │ │ ├── secrets.go │ │ └── useragent.go │ └── modules.txt ├── internal └── gcptest │ ├── cloudrun.go │ ├── metadata.go │ ├── secrets.go │ └── servicedirectory.go ├── log.go ├── log_test.go ├── metadata.go ├── metadata_test.go ├── network.go ├── run.go ├── secrets.go ├── secrets_test.go └── useragent.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # run 2 | 3 | [![GoDoc](https://godoc.org/github.com/kelseyhightower/run?status.svg)](https://pkg.go.dev/github.com/kelseyhightower/run) ![CloudBuild](https://badger-6bn2iswfgq-ue.a.run.app/build/status?project=hightowerlabs&id=bb0129f8-02c4-490b-b37e-777215fdb7ca) 4 | 5 | The run package provides a set of Cloud Run helper functions and does not leverage any third party dependencies. 6 | 7 | ## Usage 8 | 9 | ```Go 10 | package main 11 | 12 | import ( 13 | "net/http" 14 | 15 | "github.com/kelseyhightower/run" 16 | ) 17 | 18 | func main() { 19 | // Generates structured logs optimized for Cloud Run. 20 | run.Notice("Starting helloworld service...") 21 | 22 | // Easy access to secrets stored in Secret Manager. 23 | secret, err := run.AccessSecret("foo") 24 | if err != nil { 25 | run.Fatal(err) 26 | } 27 | 28 | _ = secret 29 | 30 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 31 | // Optionally pass in the *http.Request as the first argument 32 | // to correlate container logs with request logs. 33 | run.Info(r, "handling http request") 34 | 35 | w.Write([]byte("Hello world!\n")) 36 | }) 37 | 38 | // Start an HTTP server listening on the address defined by the 39 | // Cloud Run container runtime contract and gracefully shutdown 40 | // when terminated. 41 | if err := run.ListenAndServe(nil); err != http.ErrServerClosed { 42 | run.Fatal(err) 43 | } 44 | } 45 | ``` 46 | 47 | ### Service Authentication 48 | 49 | run takes the pain out of [service-to-service authentication](https://cloud.google.com/run/docs/authenticating/service-to-service) 50 | 51 | ```Go 52 | package main 53 | 54 | import ( 55 | "net/http" 56 | 57 | "github.com/kelseyhightower/run" 58 | ) 59 | 60 | func main() { 61 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 62 | request, err := http.NewRequest("GET", "https://example-6bn2iswfgq-uw.a.run.app", nil) 63 | if err != nil { 64 | http.Error(w, err.Error(), 500) 65 | return 66 | } 67 | 68 | // Use the run.Client to automatically attach ID tokens to outbound requests 69 | // and optionally lookup service names using Service Directory. 70 | response, err := run.Client.Do(request) 71 | if err != nil { 72 | http.Error(w, err.Error(), 500) 73 | return 74 | } 75 | defer response.Body.Close() 76 | }) 77 | 78 | if err := run.ListenAndServe(nil); err != http.ErrServerClosed { 79 | run.Fatal(err) 80 | } 81 | } 82 | ``` 83 | 84 | ## Status 85 | 86 | This package is experimental and should not be used or assumed to be stable. Breaking changes are guaranteed to happen. 87 | -------------------------------------------------------------------------------- /bin/cover: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | go test -v -coverprofile=c.out 4 | go tool cover -func=c.out 5 | rm c.out 6 | -------------------------------------------------------------------------------- /bin/generate-coverage-html: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | go test -v -coverprofile=c.out 4 | go tool cover -html=c.out -o coverage.html 5 | rm c.out 6 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | go test -v 4 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | type cache struct { 4 | data map[string]string 5 | } 6 | 7 | func (c *cache) Set(key, value string) { 8 | c.data[key] = value 9 | return 10 | } 11 | 12 | func (c *cache) Get(key string) string { 13 | if value, ok := c.data[key]; ok { 14 | return value 15 | } 16 | 17 | return "" 18 | } 19 | -------------------------------------------------------------------------------- /cache_test.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var cacheTests = []struct { 8 | k string 9 | v string 10 | }{ 11 | {"test", "http://test.example.com"}, 12 | {"", ""}, 13 | } 14 | 15 | func TestCache(t *testing.T) { 16 | data := make(map[string]string) 17 | c := &cache{data} 18 | 19 | for _, tt := range cacheTests { 20 | c.Set(tt.k, tt.v) 21 | v := c.Get(tt.k) 22 | if v != tt.v { 23 | t.Errorf("want %s, got %s", tt.v, v) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - name: golang 3 | id: 'go-test' 4 | entrypoint: 'bash' 5 | args: 6 | - '-c' 7 | - | 8 | go test -v 9 | - name: golang 10 | id: 'go-tool-cover' 11 | entrypoint: 'bash' 12 | args: 13 | - '-c' 14 | - | 15 | go test -v -coverprofile=c.out 16 | go tool cover -html=c.out -o coverage.html 17 | artifacts: 18 | objects: 19 | location: 'gs://hightowerlabs/run' 20 | paths: ['coverage.html'] 21 | -------------------------------------------------------------------------------- /cloudrun.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | var cloudrunEndpoint = "https://%s-run.googleapis.com" 13 | 14 | // ErrNameResolutionPermissionDenied is returned when access to the 15 | // Cloud Run API is denied. 16 | var ErrNameResolutionPermissionDenied = errors.New("run: permission denied to named service") 17 | 18 | // ErrNameResolutionUnauthorized is returned when calls to the Cloud 19 | // Run API are unauthorized. 20 | var ErrNameResolutionUnauthorized = errors.New("run: cloud run api unauthorized") 21 | 22 | // ErrServiceNotFound is returned when a service is not found. 23 | var ErrServiceNotFound = errors.New("run: named service not found") 24 | 25 | // ErrNameResolutionUnknownError is return when calls to the Cloud Run 26 | // API return an unknown error. 27 | var ErrNameResolutionUnknownError = errors.New("run: unexpected error retrieving named service") 28 | 29 | // ErrNameResolutionUnexpectedResponse is returned when calls to the Cloud Run 30 | // API return an unexpected response. 31 | type ErrNameResolutionUnexpectedResponse struct { 32 | StatusCode int 33 | Err error 34 | } 35 | 36 | func (e *ErrNameResolutionUnexpectedResponse) Error() string { 37 | return "run: unexpected error retrieving named service" 38 | } 39 | 40 | func (e *ErrNameResolutionUnexpectedResponse) Unwrap() error { return e.Err } 41 | 42 | // Service represents a Cloud Run service. 43 | type Service struct { 44 | Status ServiceStatus `json:"status"` 45 | } 46 | 47 | // ServiceStatus holds the current state of the Cloud Run service. 48 | type ServiceStatus struct { 49 | // URL holds the url that will distribute traffic over the 50 | // provided traffic targets. It generally has the form 51 | // https://{route-hash}-{project-hash}-{cluster-level-suffix}.a.run.app 52 | URL string `json:"url"` 53 | 54 | // Similar to url, information on where the service is available on HTTP. 55 | Address ServiceAddress `json:"address"` 56 | } 57 | 58 | type ServiceAddress struct { 59 | URL string `json:"url"` 60 | } 61 | 62 | func regionalEndpoint(region string) string { 63 | if region == "test" { 64 | return cloudrunEndpoint 65 | } 66 | return fmt.Sprintf(cloudrunEndpoint, region) 67 | } 68 | 69 | func getService(name, region, project string) (*Service, error) { 70 | var err error 71 | 72 | if region == "" { 73 | region, err = Region() 74 | if err != nil { 75 | return nil, err 76 | } 77 | } 78 | 79 | if project == "" { 80 | project, err = ProjectID() 81 | if err != nil { 82 | return nil, err 83 | } 84 | } 85 | 86 | endpoint := fmt.Sprintf("%s/apis/serving.knative.dev/v1/namespaces/%s/services/%s", 87 | regionalEndpoint(region), project, name) 88 | 89 | token, err := Token([]string{"https://www.googleapis.com/auth/cloud-platform"}) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | request, err := http.NewRequest("GET", endpoint, nil) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | request.Header.Set("User-Agent", userAgent) 100 | request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) 101 | 102 | timeout := time.Duration(5) * time.Second 103 | httpClient := &http.Client{Timeout: timeout} 104 | 105 | response, err := httpClient.Do(request) 106 | if err != nil { 107 | return nil, err 108 | } 109 | defer response.Body.Close() 110 | 111 | switch s := response.StatusCode; s { 112 | case 200: 113 | break 114 | case 401: 115 | return nil, ErrNameResolutionUnauthorized 116 | case 403: 117 | return nil, ErrNameResolutionPermissionDenied 118 | case 404: 119 | return nil, ErrServiceNotFound 120 | default: 121 | return nil, &ErrNameResolutionUnexpectedResponse{s, ErrNameResolutionUnknownError} 122 | } 123 | 124 | var service Service 125 | 126 | data, err := io.ReadAll(response.Body) 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | err = json.Unmarshal(data, &service) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | return &service, nil 137 | } 138 | -------------------------------------------------------------------------------- /discovery.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "strconv" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | var serviceDirectoryEndpoint = "https://servicedirectory.googleapis.com" 16 | 17 | type Endpoint struct { 18 | Name string `json:"name"` 19 | Address string `json:"address"` 20 | Port int `json:"port"` 21 | Annotations map[string]string `json:"annotations,omitempty"` 22 | Network string `json:"network,omitempty"` 23 | UID string `json:"uid,omitempty"` 24 | } 25 | 26 | type ListEndpoints struct { 27 | Endpoints []Endpoint `json:"endpoints"` 28 | } 29 | 30 | type LoadBalancer interface { 31 | Next() Endpoint 32 | RefreshEndpoints() 33 | } 34 | 35 | type RoundRobinLoadBalancer struct { 36 | name string 37 | namespace string 38 | endpoints []Endpoint 39 | current int 40 | } 41 | 42 | func NewRoundRobinLoadBalancer(name, namespace string) (*RoundRobinLoadBalancer, error) { 43 | endpoints, err := Endpoints(name, namespace) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | loadBalancer := &RoundRobinLoadBalancer{name, namespace, endpoints, 0} 49 | 50 | go loadBalancer.RefreshEndpoints() 51 | 52 | return loadBalancer, nil 53 | } 54 | 55 | func (lb *RoundRobinLoadBalancer) Next() Endpoint { 56 | endpoint := lb.endpoints[lb.current] 57 | lb.current++ 58 | 59 | if lb.current >= len(lb.endpoints) { 60 | lb.current = 0 61 | } 62 | return endpoint 63 | } 64 | 65 | func (lb *RoundRobinLoadBalancer) RefreshEndpoints() { 66 | for { 67 | time.Sleep(time.Second * 10) 68 | endpoints, err := Endpoints(lb.name, lb.namespace) 69 | if err != nil { 70 | Log("Error", err.Error()) 71 | continue 72 | } 73 | lb.endpoints = endpoints 74 | lb.current = 0 75 | } 76 | } 77 | 78 | func Endpoints(name, namespace string) ([]Endpoint, error) { 79 | var listEndpoints ListEndpoints 80 | 81 | scopes := []string{"https://www.googleapis.com/auth/cloud-platform"} 82 | token, err := Token(scopes) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | basePath, err := formatEndpointBasePath(name, namespace) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | url := fmt.Sprintf("%s/v1/%s", serviceDirectoryEndpoint, basePath) 93 | 94 | c := http.Client{ 95 | Timeout: time.Second * 10, 96 | } 97 | 98 | request, err := http.NewRequest(http.MethodGet, url, nil) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) 104 | 105 | response, err := c.Do(request) 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | data, err := io.ReadAll(response.Body) 111 | if err != nil { 112 | return nil, err 113 | } 114 | 115 | defer response.Body.Close() 116 | 117 | if response.StatusCode != 200 { 118 | Log("Error", string(data)) 119 | return nil, errors.New(fmt.Sprintf("run: non 200 response when retrieving endpoints: %s", response.Status)) 120 | } 121 | 122 | if err := json.Unmarshal(data, &listEndpoints); err != nil { 123 | return nil, err 124 | } 125 | 126 | return listEndpoints.Endpoints, nil 127 | } 128 | 129 | func RegisterEndpoint(namespace string) error { 130 | scopes := []string{"https://www.googleapis.com/auth/cloud-platform"} 131 | token, err := Token(scopes) 132 | if err != nil { 133 | Log("Error", fmt.Sprintf("Unable to register endpoint: %s", err)) 134 | return err 135 | } 136 | 137 | c := http.Client{ 138 | Timeout: time.Second * 10, 139 | } 140 | 141 | basePath, err := formatEndpointBasePath("", namespace) 142 | if err != nil { 143 | Log("Error", fmt.Sprintf("Unable to register endpoint. Error formating endpoint base path: %s", err)) 144 | return err 145 | } 146 | 147 | endpointID, err := generateEndpointID() 148 | if err != nil { 149 | Log("Error", fmt.Sprintf("Unable to register endpoint. Error generating endpoint ID: %s", err)) 150 | return err 151 | } 152 | 153 | url := fmt.Sprintf("%s/v1/%s?endpointId=%s", serviceDirectoryEndpoint, basePath, endpointID) 154 | 155 | ip, err := IPAddress() 156 | if err != nil { 157 | Log("Error", fmt.Sprintf("Unable to register endpoint. Error getting IP address: %s", err)) 158 | return err 159 | } 160 | 161 | port, err := strconv.Atoi(Port()) 162 | if err != nil { 163 | Log("Error", fmt.Sprintf("Unable to register endpoint. Error converting instance port: %s", err)) 164 | return err 165 | } 166 | 167 | instanceID, err := ID() 168 | if err != nil { 169 | Log("Error", fmt.Sprintf("Unable to register endpoint. Error retrieving instance ID: %s", err)) 170 | return err 171 | } 172 | 173 | annotations := make(map[string]string) 174 | annotations["instance_id"] = instanceID 175 | 176 | ep := Endpoint{ 177 | Address: ip, 178 | Port: port, 179 | Annotations: annotations, 180 | } 181 | 182 | data, err := json.Marshal(ep) 183 | if err != nil { 184 | Log("Error", fmt.Sprintf("Unable to register endpoint. Error serializing endpoint request object: %s", err)) 185 | return err 186 | } 187 | 188 | body := bytes.NewBuffer(data) 189 | 190 | request, err := http.NewRequest(http.MethodPost, url, body) 191 | if err != nil { 192 | Log("Error", fmt.Sprintf("Unable to register endpoint. Error create endpoint HTTP request: %s", err)) 193 | return err 194 | } 195 | 196 | request.Header.Set("Content-Type", "application/json") 197 | request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) 198 | 199 | response, err := c.Do(request) 200 | if err != nil { 201 | Log("Error", fmt.Sprintf("Unable to register endpoint. HTTP request failed: %s", err)) 202 | return err 203 | } 204 | 205 | if response.StatusCode != 200 { 206 | data, err := io.ReadAll(response.Body) 207 | if err != nil { 208 | Log("Error", fmt.Sprintf("Unable to register endpoint. Error reading HTTP response: %s", err)) 209 | return err 210 | } 211 | 212 | Log("Error", fmt.Sprintf("Unable to register endpoint. HTTP request failed: %s", string(data))) 213 | return errors.New(fmt.Sprintf("run: non 200 response when registering endpoint: %s", response.Status)) 214 | } 215 | 216 | Log("Info", fmt.Sprintf("Successfully registered endpoint: %s", endpointID)) 217 | return nil 218 | } 219 | 220 | func DeregisterEndpoint(namespace string) error { 221 | scopes := []string{"https://www.googleapis.com/auth/cloud-platform"} 222 | token, err := Token(scopes) 223 | if err != nil { 224 | return err 225 | } 226 | 227 | basePath, err := formatEndpointBasePath("", namespace) 228 | if err != nil { 229 | return err 230 | } 231 | 232 | endpointID, err := generateEndpointID() 233 | if err != nil { 234 | return err 235 | } 236 | 237 | url := fmt.Sprintf("%s/v1/%s/%s", serviceDirectoryEndpoint, basePath, endpointID) 238 | 239 | c := http.Client{ 240 | Timeout: time.Second * 10, 241 | } 242 | 243 | request, err := http.NewRequest(http.MethodDelete, url, nil) 244 | if err != nil { 245 | return err 246 | } 247 | 248 | request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) 249 | 250 | response, err := c.Do(request) 251 | if err != nil { 252 | return err 253 | } 254 | 255 | if response.StatusCode != 200 { 256 | data, err := io.ReadAll(response.Body) 257 | if err != nil { 258 | return err 259 | } 260 | 261 | Log("Error", string(data)) 262 | return errors.New(fmt.Sprintf("run: non 200 response when deregistering endpoint: %s", response.Status)) 263 | } 264 | 265 | return nil 266 | } 267 | 268 | func formatEndpointBasePath(name, namespace string) (string, error) { 269 | if name == "" { 270 | name = ServiceName() 271 | } 272 | region, err := Region() 273 | if err != nil { 274 | return "", err 275 | } 276 | 277 | projectID, err := ProjectID() 278 | if err != nil { 279 | return "", err 280 | } 281 | 282 | s := fmt.Sprintf("projects/%s/locations/%s/namespaces/%s/services/%s/endpoints", 283 | projectID, region, namespace, name) 284 | 285 | return s, nil 286 | } 287 | 288 | func generateEndpointID() (string, error) { 289 | serviceName := ServiceName() 290 | 291 | ip, err := IPAddress() 292 | if err != nil { 293 | return "", err 294 | } 295 | 296 | dashedIPAddress := strings.ReplaceAll(ip, ".", "-") 297 | 298 | id := fmt.Sprintf("%s-%s", serviceName, dashedIPAddress) 299 | 300 | return id, nil 301 | } 302 | -------------------------------------------------------------------------------- /discovery_test.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/kelseyhightower/run/internal/gcptest" 10 | ) 11 | 12 | var testEndpoints = []Endpoint{ 13 | { 14 | Name: "test-10-0-0-1", 15 | Address: "10.0.0.1", 16 | Port: 8080, 17 | }, 18 | { 19 | Name: "test-10-0-0-2", 20 | Address: "10.0.0.2", 21 | Port: 8080, 22 | }, 23 | } 24 | 25 | func TestEndpoints(t *testing.T) { 26 | ms := httptest.NewServer(http.HandlerFunc(gcptest.MetadataHandler)) 27 | defer ms.Close() 28 | 29 | metadataEndpoint = ms.URL 30 | 31 | ss := httptest.NewServer(http.HandlerFunc(gcptest.ServiceDirectoryHandler)) 32 | defer ss.Close() 33 | 34 | serviceDirectoryEndpoint = ss.URL 35 | 36 | endpoints, err := Endpoints("test", "test") 37 | if err != nil { 38 | t.Errorf("unexpected error: %v", err) 39 | } 40 | 41 | expected := testEndpoints 42 | 43 | if !reflect.DeepEqual(endpoints, expected) { 44 | t.Errorf("want %v, got %v", expected, endpoints) 45 | } 46 | } 47 | 48 | var newRoundRobinLoadBalancerTests = []struct { 49 | namespace string 50 | name string 51 | want []Endpoint 52 | }{ 53 | {"test", "test", testEndpoints}, 54 | } 55 | 56 | func TestNewRoundRobinLoadBalancer(t *testing.T) { 57 | ms := httptest.NewServer(http.HandlerFunc(gcptest.MetadataHandler)) 58 | defer ms.Close() 59 | 60 | metadataEndpoint = ms.URL 61 | 62 | ss := httptest.NewServer(http.HandlerFunc(gcptest.ServiceDirectoryHandler)) 63 | defer ss.Close() 64 | 65 | serviceDirectoryEndpoint = ss.URL 66 | 67 | for _, tt := range newRoundRobinLoadBalancerTests { 68 | lb, err := NewRoundRobinLoadBalancer(tt.namespace, tt.name) 69 | if err != nil { 70 | t.Errorf("unexpected error: %v", err) 71 | } 72 | 73 | for i := 0; i <= len(tt.want)-1; i++ { 74 | endpoint := lb.Next() 75 | if !reflect.DeepEqual(endpoint, tt.want[i]) { 76 | t.Errorf("want %v, got %v", tt.want[i], endpoint) 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package run provides helper functions for building Cloud Run 7 | applications. 8 | */ 9 | package run 10 | 11 | -------------------------------------------------------------------------------- /env.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import "os" 4 | 5 | // Port returns the port your HTTP server should listen on. 6 | func Port() string { 7 | return os.Getenv("PORT") 8 | } 9 | 10 | // Revision returns the name of the Cloud Run revision being run. 11 | func Revision() string { 12 | return os.Getenv("K_REVISION") 13 | } 14 | 15 | // Configuration returns the name of the Cloud Run configuration being run. 16 | func Configuration() string { 17 | return os.Getenv("K_CONFIGURATION") 18 | } 19 | 20 | // ServiceName returns the name of the Cloud Run service being run. 21 | func ServiceName() string { 22 | return os.Getenv("K_SERVICE") 23 | } 24 | -------------------------------------------------------------------------------- /env_test.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | var envTests = []struct { 9 | n string 10 | e string 11 | want string 12 | f func() string 13 | }{ 14 | {"Configuration", "K_CONFIGURATION", "test", Configuration}, 15 | {"Revision", "K_REVISION", "test-0001-vob", Revision}, 16 | {"Service", "K_SERVICE", "test", ServiceName}, 17 | {"Port", "PORT", "8080", Port}, 18 | } 19 | 20 | func TestEnv(t *testing.T) { 21 | for _, tt := range envTests { 22 | if err := os.Setenv(tt.e, tt.want); err != nil { 23 | t.Error(err) 24 | } 25 | 26 | v := tt.f() 27 | if v != tt.want { 28 | t.Errorf("want %v got %v", tt.want, v) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package run_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/kelseyhightower/run" 9 | ) 10 | 11 | func ExampleAccessSecret() { 12 | secret, err := run.AccessSecret("apikey") 13 | if err != nil { 14 | log.Println(err) 15 | return 16 | } 17 | 18 | _ = secret 19 | } 20 | 21 | func ExampleAccessSecretVersion() { 22 | secret, err := run.AccessSecretVersion("apikey", "1") 23 | if err != nil { 24 | log.Println(err) 25 | return 26 | } 27 | 28 | _ = secret 29 | } 30 | 31 | func ExampleIDToken() { 32 | serviceURL := "https://example-6bn2iswfgq-uw.a.run.app" 33 | 34 | request, err := http.NewRequest(http.MethodGet, serviceURL, nil) 35 | if err != nil { 36 | log.Println(err) 37 | return 38 | } 39 | 40 | idToken, err := run.IDToken(serviceURL) 41 | if err != nil { 42 | log.Println(err) 43 | return 44 | } 45 | 46 | request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", idToken)) 47 | } 48 | 49 | func ExampleToken() { 50 | scopes := []string{"https://www.googleapis.com/auth/cloud-platform"} 51 | 52 | project, err := run.ProjectID() 53 | if err != nil { 54 | log.Println(err) 55 | return 56 | } 57 | 58 | endpoint := fmt.Sprintf("https://cloudbuild.googleapis.com/v1/projects/%s/builds", project) 59 | 60 | request, err := http.NewRequest(http.MethodGet, endpoint, nil) 61 | if err != nil { 62 | log.Println(err) 63 | return 64 | } 65 | 66 | token, err := run.Token(scopes) 67 | if err != nil { 68 | log.Println(err) 69 | return 70 | } 71 | 72 | request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) 73 | } 74 | 75 | func ExampleLogger() { 76 | logger := run.NewLogger() 77 | 78 | logger.Notice("Starting example service...") 79 | } 80 | 81 | func ExampleLogger_defaultLogger() { 82 | run.Notice("Starting example service...") 83 | } 84 | 85 | func ExampleLogger_logCorrelation() { 86 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 87 | // Pass in the *http.Request as the first argument to correlate 88 | // container logs with request logs. 89 | run.Info(r, "Handling request...") 90 | }) 91 | } 92 | 93 | func ExampleTransport() { 94 | client := &http.Client{Transport: &run.Transport{}} 95 | 96 | response, err := client.Get("https://example-6bn2iswfgq-uw.a.run.app") 97 | if err != nil { 98 | log.Println(err) 99 | return 100 | } 101 | 102 | defer response.Body.Close() 103 | } 104 | 105 | func ExampleTransport_serviceNameResolution() { 106 | client := &http.Client{ 107 | Transport: &run.Transport{}, 108 | } 109 | 110 | response, err := client.Get("https://service-name") 111 | if err != nil { 112 | log.Println(err) 113 | return 114 | } 115 | 116 | defer response.Body.Close() 117 | } 118 | 119 | func ExampleListenAndServe() { 120 | if err := run.ListenAndServe(nil); err != http.ErrServerClosed { 121 | run.Fatal(err) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kelseyhightower/run 2 | 3 | go 1.19 4 | 5 | require golang.org/x/net v0.2.0 6 | 7 | require golang.org/x/text v0.4.0 // indirect 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= 2 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 3 | golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= 4 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 5 | -------------------------------------------------------------------------------- /health_checks.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // A Probe responds to health checks. 8 | type Probe interface { 9 | Ready() bool 10 | } 11 | 12 | type httpProbeHandler struct { 13 | probe Probe 14 | } 15 | 16 | // Ready replies to the request with an HTTP 200 response. 17 | func Ready(w http.ResponseWriter, r *http.Request) { 18 | Info(r, "HTTP startup probe succeeded") 19 | w.WriteHeader(200) 20 | } 21 | 22 | // Healthy replies to the request with an HTTP 200 response. 23 | func Healthy(w http.ResponseWriter, r *http.Request) { 24 | Info(r, "HTTP liveness probe succeeded") 25 | w.WriteHeader(200) 26 | } 27 | 28 | // HTTPProbeHandler returns a request handler that calls the given probe and 29 | // returns an HTTP 200 response if the probe Ready method returns true, or an 30 | // HTTP 500 if false. 31 | func HTTPProbeHandler(probe Probe) http.Handler { 32 | return &httpProbeHandler{probe: probe} 33 | } 34 | 35 | func (h *httpProbeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 36 | defer r.Body.Close() 37 | 38 | if h.probe.Ready() { 39 | w.WriteHeader(200) 40 | return 41 | } 42 | 43 | w.WriteHeader(500) 44 | } 45 | -------------------------------------------------------------------------------- /health_checks_test.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | func TestReady(t *testing.T) { 10 | responseRecorder := httptest.NewRecorder() 11 | request := httptest.NewRequest(http.MethodGet, "/ready", nil) 12 | Ready(responseRecorder, request) 13 | response := responseRecorder.Result() 14 | 15 | if response.StatusCode != 200 { 16 | t.Errorf("status code mismatch; want %v, got %v", 200, response.StatusCode) 17 | } 18 | } 19 | 20 | func TestHealthy(t *testing.T) { 21 | responseRecorder := httptest.NewRecorder() 22 | request := httptest.NewRequest(http.MethodGet, "/health", nil) 23 | Healthy(responseRecorder, request) 24 | response := responseRecorder.Result() 25 | 26 | if response.StatusCode != 200 { 27 | t.Errorf("status code mismatch; want %v, got %v", 200, response.StatusCode) 28 | } 29 | } 30 | 31 | type failProbe struct{} 32 | 33 | func (p failProbe) Ready() bool { 34 | return false 35 | } 36 | 37 | type successProbe struct{} 38 | 39 | func (p successProbe) Ready() bool { 40 | return true 41 | } 42 | 43 | var httpProbeHandlerTests = []struct { 44 | probe Probe 45 | want int 46 | }{ 47 | {failProbe{}, 500}, 48 | {successProbe{}, 200}, 49 | } 50 | 51 | func TestHTTPProbeHandler(t *testing.T) { 52 | for _, tt := range httpProbeHandlerTests { 53 | responseRecorder := httptest.NewRecorder() 54 | request := httptest.NewRequest(http.MethodGet, "/health", nil) 55 | 56 | HTTPProbeHandler(tt.probe).ServeHTTP(responseRecorder, request) 57 | response := responseRecorder.Result() 58 | if response.StatusCode != tt.want { 59 | t.Errorf("status code mismatch; want %v, got %v", tt.want, response.StatusCode) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | "os/signal" 12 | "strings" 13 | "syscall" 14 | 15 | "golang.org/x/net/http2" 16 | "golang.org/x/net/http2/h2c" 17 | ) 18 | 19 | var ( 20 | DefaultNamespace string 21 | DefaultRunDomain = "run.local" 22 | ) 23 | 24 | var Client = &http.Client{ 25 | Transport: &Transport{ 26 | Base: http.DefaultTransport, 27 | InjectAuthHeader: true, 28 | balancers: make(map[string]*RoundRobinLoadBalancer), 29 | }, 30 | } 31 | 32 | // Transport is a http.RoundTripper that attaches ID tokens to all 33 | // outgoing request. 34 | type Transport struct { 35 | // Base optionally provides a http.RoundTripper that handles the 36 | // request. If nil, http.DefaultTransport is used. 37 | Base http.RoundTripper 38 | 39 | // InjectAuthHeader optionally adds or replaces the HTTP Authorization 40 | // header using the ID token from the metadata service. 41 | InjectAuthHeader bool 42 | 43 | balancers map[string]*RoundRobinLoadBalancer 44 | } 45 | 46 | func (t *Transport) RoundTrip(r *http.Request) (*http.Response, error) { 47 | if t.Base == nil { 48 | t.Base = http.DefaultTransport 49 | } 50 | 51 | hostname, err := parseHostname(r.Host) 52 | if err != nil { 53 | if t.InjectAuthHeader { 54 | idToken, err := IDToken(audFromRequest(r)) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | r.Header.Add("Authorization", fmt.Sprintf("Bearer %s", idToken)) 60 | } 61 | return t.Base.RoundTrip(r) 62 | } 63 | 64 | var loadBalancer *RoundRobinLoadBalancer 65 | 66 | serviceNamespace := fmt.Sprintf("%s.%s", hostname.Service, hostname.Namespace) 67 | if lb, ok := t.balancers[serviceNamespace]; ok { 68 | loadBalancer = lb 69 | } else { 70 | l, err := NewRoundRobinLoadBalancer(hostname.Service, hostname.Namespace) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | t.balancers[serviceNamespace] = l 76 | loadBalancer = l 77 | } 78 | 79 | endpoint := loadBalancer.Next() 80 | 81 | address := endpoint.Address 82 | port := endpoint.Port 83 | 84 | u, err := url.Parse(fmt.Sprintf("http://%s:%d", address, port)) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | r.Host = u.Host 90 | r.URL.Host = u.Host 91 | r.URL.Scheme = u.Scheme 92 | r.Header.Set("Host", u.Hostname()) 93 | 94 | if t.InjectAuthHeader { 95 | idToken, err := IDToken(audFromRequest(r)) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | r.Header.Add("Authorization", fmt.Sprintf("Bearer %s", idToken)) 101 | } 102 | 103 | return t.Base.RoundTrip(r) 104 | } 105 | 106 | type Hostname struct { 107 | Domain string 108 | Namespace string 109 | Service string 110 | } 111 | 112 | var ErrInvalidHostname = errors.New("invalid hostname") 113 | 114 | func parseHostname(host string) (*Hostname, error) { 115 | var hostname Hostname 116 | 117 | if strings.ContainsAny(host, ":") { 118 | return nil, ErrInvalidHostname 119 | } 120 | 121 | if ip := net.ParseIP(host); ip != nil { 122 | return nil, ErrInvalidHostname 123 | } 124 | 125 | ss := strings.Split(host, ".") 126 | 127 | switch len(ss) { 128 | case 0: 129 | return nil, ErrInvalidHostname 130 | case 1: 131 | hostname.Namespace = DefaultNamespace 132 | hostname.Service = ss[0] 133 | case 4: 134 | domain := fmt.Sprintf("%s.%s", ss[2], ss[3]) 135 | if domain == DefaultRunDomain { 136 | hostname.Domain = domain 137 | hostname.Namespace = ss[1] 138 | hostname.Service = ss[0] 139 | } 140 | default: 141 | return nil, ErrInvalidHostname 142 | } 143 | 144 | return &hostname, nil 145 | } 146 | 147 | // audFromRequest extracts the Cloud Run service URL from an HTTP request. 148 | func audFromRequest(r *http.Request) string { 149 | return fmt.Sprintf("%s://%s", r.URL.Scheme, r.URL.Hostname()) 150 | } 151 | 152 | // ListenAndServe starts an http.Server with the given handler listening 153 | // on the port defined by the PORT environment variable or "8080" if not 154 | // set. 155 | // 156 | // ListenAndServe supports requests in HTTP/2 cleartext (h2c) format, 157 | // because TLS is terminated by Cloud Run for all client requests including 158 | // HTTP2. 159 | // 160 | // ListenAndServe traps the SIGINT and SIGTERM signals then gracefully 161 | // shuts down the server without interrupting any active connections by 162 | // calling the server's Shutdown method. 163 | // 164 | // ListenAndServe always returns a non-nil error; under normal conditions 165 | // http.ErrServerClosed will be returned indicating a successful graceful 166 | // shutdown. 167 | func ListenAndServe(handler http.Handler) error { 168 | port := os.Getenv("PORT") 169 | if port == "" { 170 | port = "8080" 171 | } 172 | 173 | if handler == nil { 174 | handler = http.DefaultServeMux 175 | } 176 | 177 | addr := net.JoinHostPort("0.0.0.0", port) 178 | 179 | h2s := &http2.Server{} 180 | server := &http.Server{Addr: addr, Handler: h2c.NewHandler(handler, h2s)} 181 | 182 | idleConnsClosed := make(chan struct{}) 183 | go func() { 184 | signalChan := make(chan os.Signal, 1) 185 | signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) 186 | <-signalChan 187 | 188 | Notice("Received shutdown signal; waiting for active connections to close") 189 | 190 | if err := server.Shutdown(context.Background()); err != nil { 191 | Error("Error during server shutdown: %v", err) 192 | } 193 | 194 | close(idleConnsClosed) 195 | }() 196 | 197 | if err := server.ListenAndServe(); err != http.ErrServerClosed { 198 | return err 199 | } 200 | 201 | <-idleConnsClosed 202 | 203 | Notice("Shutdown complete") 204 | 205 | return http.ErrServerClosed 206 | } 207 | -------------------------------------------------------------------------------- /http_test.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/kelseyhightower/run/internal/gcptest" 10 | ) 11 | 12 | func TestParseHostname(t *testing.T) { 13 | host := "ping.default.run.local" 14 | hostname, err := parseHostname(host) 15 | if err != nil { 16 | t.Error(err) 17 | } 18 | 19 | if hostname.Service != "ping" { 20 | t.Errorf("service name mismatch; want %v, got %v", "ping", hostname.Service) 21 | } 22 | } 23 | 24 | func TestTransport(t *testing.T) { 25 | ms := httptest.NewServer(http.HandlerFunc(gcptest.MetadataHandler)) 26 | defer ms.Close() 27 | 28 | metadataEndpoint = ms.URL 29 | 30 | ss := httptest.NewServer(http.HandlerFunc(gcptest.ServiceDirectoryHandler)) 31 | defer ss.Close() 32 | 33 | serviceDirectoryEndpoint = ss.URL 34 | 35 | var headers http.Header 36 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 37 | headers = r.Header.Clone() 38 | return 39 | })) 40 | defer ts.Close() 41 | 42 | httpClient := &http.Client{ 43 | Transport: &Transport{ 44 | Base: http.DefaultTransport, 45 | InjectAuthHeader: true, 46 | balancers: make(map[string]*RoundRobinLoadBalancer), 47 | }, 48 | } 49 | 50 | response, err := httpClient.Get(ts.URL) 51 | if err != nil { 52 | t.Error(err) 53 | } 54 | defer response.Body.Close() 55 | 56 | authHeader := headers.Get("Authorization") 57 | expectedAuthHeader := fmt.Sprintf("Bearer %s", gcptest.IDToken) 58 | 59 | if authHeader != expectedAuthHeader { 60 | t.Errorf("headers mismatch; want %s, got %s", expectedAuthHeader, authHeader) 61 | } 62 | } 63 | 64 | func TestTransportNameResolution(t *testing.T) { 65 | ms := httptest.NewServer(http.HandlerFunc(gcptest.MetadataHandler)) 66 | defer ms.Close() 67 | 68 | metadataEndpoint = ms.URL 69 | 70 | ss := httptest.NewServer(http.HandlerFunc(gcptest.ServiceDirectoryHandler)) 71 | defer ss.Close() 72 | 73 | serviceDirectoryEndpoint = ss.URL 74 | 75 | headers := make(http.Header) 76 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 77 | headers.Set("x-test-result", "success") 78 | })) 79 | defer ts.Close() 80 | 81 | httpClient := &http.Client{ 82 | Transport: &Transport{ 83 | Base: http.DefaultTransport, 84 | InjectAuthHeader: true, 85 | balancers: make(map[string]*RoundRobinLoadBalancer), 86 | }, 87 | } 88 | 89 | _, err := httpClient.Get(ts.URL) 90 | if err != nil { 91 | t.Error(err) 92 | } 93 | 94 | testHeader := headers.Get("x-test-result") 95 | if testHeader != "success" { 96 | t.Errorf("headers mismatch; want %s, got %s", "success", testHeader) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /integration/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.15.0-buster as go-builder 2 | WORKDIR /module 3 | COPY . /module/ 4 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ 5 | go build -a -tags netgo \ 6 | -ldflags '-w -extldflags "-static"' \ 7 | -mod vendor \ 8 | -o run-integration-tests 9 | 10 | FROM alpine:latest as certs 11 | RUN apk --update add ca-certificates 12 | 13 | FROM scratch 14 | COPY --from=go-builder /module/run-integration-tests . 15 | COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 16 | ENTRYPOINT ["/run-integration-tests"] 17 | -------------------------------------------------------------------------------- /integration/backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.15.0-buster as go-builder 2 | WORKDIR /module 3 | COPY . /module/ 4 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ 5 | go build -a -tags netgo \ 6 | -ldflags '-w -extldflags "-static"' \ 7 | -mod vendor \ 8 | -o run-integration-backend 9 | 10 | FROM alpine:latest as certs 11 | RUN apk --update add ca-certificates 12 | 13 | FROM scratch 14 | COPY --from=go-builder /module/run-integration-backend . 15 | COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 16 | ENTRYPOINT ["/run-integration-backend"] 17 | -------------------------------------------------------------------------------- /integration/backend/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kelseyhightower/run-integration-backend 2 | 3 | go 1.14 4 | 5 | replace github.com/kelseyhightower/run => ../../ 6 | 7 | require github.com/kelseyhightower/run v0.0.0-00010101000000-000000000000 8 | -------------------------------------------------------------------------------- /integration/backend/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/kelseyhightower/run" 7 | ) 8 | 9 | func main() { 10 | run.Notice("Starting integration backend service...") 11 | 12 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 13 | run.Info(r, "Handling HTTP request...") 14 | w.Write([]byte("SUCCESS")) 15 | }) 16 | 17 | run.Fatal(run.ListenAndServe(nil)) 18 | } 19 | -------------------------------------------------------------------------------- /integration/backend/vendor/github.com/kelseyhightower/run/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /integration/backend/vendor/github.com/kelseyhightower/run/README.md: -------------------------------------------------------------------------------- 1 | # run 2 | 3 | [![GoDoc](https://godoc.org/github.com/kelseyhightower/run?status.svg)](https://pkg.go.dev/github.com/kelseyhightower/run) ![CloudBuild](https://badger-6bn2iswfgq-ue.a.run.app/build/status?project=hightowerlabs&id=bb0129f8-02c4-490b-b37e-777215fdb7ca) 4 | 5 | The run package provides a set of Cloud Run helper functions and does not leverage any third party dependencies. 6 | 7 | ## Usage 8 | 9 | ```Go 10 | package main 11 | 12 | import ( 13 | "net/http" 14 | "os" 15 | 16 | "github.com/kelseyhightower/run" 17 | ) 18 | 19 | func main() { 20 | // Generates structured logs optimized for Cloud Run. 21 | run.Notice("Starting helloworld service...") 22 | 23 | // Easy access to secrets stored in Secret Manager. 24 | secret, err := run.AccessSecret("foo") 25 | if err != nil { 26 | run.Fatal(err) 27 | } 28 | 29 | _ = secret 30 | 31 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 32 | // Optionally pass in the *http.Request as the first argument 33 | // to correlate container logs with request logs. 34 | run.Info(r, "handling http request") 35 | 36 | w.Write([]byte("Hello world!\n")) 37 | }) 38 | 39 | // Start an HTTP server listening on the address defined by the 40 | // Cloud Run container runtime contract and gracefully shutdown 41 | // when terminated. 42 | if err := run.ListenAndServe(nil); err != http.ErrServerClosed { 43 | run.Fatal(err) 44 | } 45 | } 46 | ``` 47 | 48 | ### Service Authentication 49 | 50 | run takes the pain out of [service-to-service authentication](https://cloud.google.com/run/docs/authenticating/service-to-service) 51 | 52 | ```Go 53 | package main 54 | 55 | import ( 56 | "net/http" 57 | 58 | "github.com/kelseyhightower/run" 59 | ) 60 | 61 | func main() { 62 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 63 | request, err := http.NewRequest("GET", "https://example-6bn2iswfgq-uw.a.run.app", nil) 64 | if err != nil { 65 | http.Error(w, err.Error(), 500) 66 | return 67 | } 68 | 69 | // Use the run.Transport to automatically attach ID tokens to outbound requests 70 | // and optionally expand service names using the Cloud Run API. 71 | // See https://pkg.go.dev/github.com/kelseyhightower/run?tab=doc#Transport 72 | client := http.Client{Transport: &run.Transport{EnableServiceNameResolution: false}} 73 | 74 | response, err := client.Do(request) 75 | if err != nil { 76 | http.Error(w, err.Error(), 500) 77 | return 78 | } 79 | defer response.Body.Close() 80 | }) 81 | 82 | if err := run.ListenAndServe(nil); err != http.ErrServerClosed { 83 | run.Fatal(err) 84 | } 85 | } 86 | ``` 87 | 88 | ## Status 89 | 90 | This package is experimental and should not be used or assumed to be stable. Breaking changes are guaranteed to happen. 91 | -------------------------------------------------------------------------------- /integration/backend/vendor/github.com/kelseyhightower/run/cache.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | type cache struct { 4 | data map[string]string 5 | } 6 | 7 | func (c *cache) Set(key, value string) { 8 | c.data[key] = value 9 | return 10 | } 11 | 12 | func (c *cache) Get(key string) string { 13 | if value, ok := c.data[key]; ok { 14 | return value 15 | } 16 | 17 | return "" 18 | } 19 | -------------------------------------------------------------------------------- /integration/backend/vendor/github.com/kelseyhightower/run/cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - name: golang 3 | id: 'go-test' 4 | entrypoint: 'bash' 5 | args: 6 | - '-c' 7 | - | 8 | go test -v 9 | - name: golang 10 | id: 'go-tool-cover' 11 | entrypoint: 'bash' 12 | args: 13 | - '-c' 14 | - | 15 | go test -v -coverprofile=c.out 16 | go tool cover -html=c.out -o coverage.html 17 | artifacts: 18 | objects: 19 | location: 'gs://hightowerlabs/run' 20 | paths: ['coverage.html'] 21 | -------------------------------------------------------------------------------- /integration/backend/vendor/github.com/kelseyhightower/run/cloudrun.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | var cloudrunEndpoint = "https://%s-run.googleapis.com" 13 | 14 | // ErrNameResolutionPermissionDenied is returned when access to the 15 | // Cloud Run API is denied. 16 | var ErrNameResolutionPermissionDenied = errors.New("run: permission denied to named service") 17 | 18 | // ErrNameResolutionUnauthorized is returned when calls to the Cloud 19 | // Run API are unauthorized. 20 | var ErrNameResolutionUnauthorized = errors.New("run: cloud run api unauthorized") 21 | 22 | // ErrServiceNotFound is returned when a service is not found.. 23 | var ErrServiceNotFound = errors.New("run: named service not found") 24 | 25 | // ErrNameResolutionUnknownError is return when calls to the Cloud Run 26 | // API return an unknown error. 27 | var ErrNameResolutionUnknownError = errors.New("run: unexpected error retrieving named service") 28 | 29 | // ErrNameResolutionUnexpectedResponse is returned when calls to the Cloud Run 30 | // API return an unexpected response. 31 | type ErrNameResolutionUnexpectedResponse struct { 32 | StatusCode int 33 | Err error 34 | } 35 | 36 | func (e *ErrNameResolutionUnexpectedResponse) Error() string { 37 | return "run: unexpected error retrieving named service" 38 | } 39 | 40 | func (e *ErrNameResolutionUnexpectedResponse) Unwrap() error { return e.Err } 41 | 42 | // Service represents a Cloud Run service. 43 | type Service struct { 44 | Status ServiceStatus `json:"status"` 45 | } 46 | 47 | // ServiceStatus holds the current state of the Cloud Run service. 48 | type ServiceStatus struct { 49 | // URL holds the url that will distribute traffic over the 50 | // provided traffic targets. It generally has the form 51 | // https://{route-hash}-{project-hash}-{cluster-level-suffix}.a.run.app 52 | URL string `json:"url"` 53 | 54 | // Similar to url, information on where the service is available on HTTP. 55 | Address ServiceAddresss `json:"address"` 56 | } 57 | 58 | type ServiceAddresss struct { 59 | URL string `json:"url"` 60 | } 61 | 62 | func regionalEndpoint(region string) string { 63 | if region == "test" { 64 | return cloudrunEndpoint 65 | } 66 | return fmt.Sprintf(cloudrunEndpoint, region) 67 | } 68 | 69 | func getService(name, region, project string) (*Service, error) { 70 | var err error 71 | 72 | if region == "" { 73 | region, err = Region() 74 | if err != nil { 75 | return nil, err 76 | } 77 | } 78 | 79 | if project == "" { 80 | project, err = ProjectID() 81 | if err != nil { 82 | return nil, err 83 | } 84 | } 85 | 86 | endpoint := fmt.Sprintf("%s/apis/serving.knative.dev/v1/namespaces/%s/services/%s", 87 | regionalEndpoint(region), project, name) 88 | 89 | token, err := Token([]string{"https://www.googleapis.com/auth/cloud-platform"}) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | request, err := http.NewRequest("GET", endpoint, nil) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | request.Header.Set("User-Agent", userAgent) 100 | request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) 101 | 102 | timeout := time.Duration(5) * time.Second 103 | httpClient := &http.Client{Timeout: timeout} 104 | 105 | response, err := httpClient.Do(request) 106 | if err != nil { 107 | return nil, err 108 | } 109 | defer response.Body.Close() 110 | 111 | switch s := response.StatusCode; s { 112 | case 200: 113 | break 114 | case 401: 115 | return nil, ErrNameResolutionUnauthorized 116 | case 403: 117 | return nil, ErrNameResolutionPermissionDenied 118 | case 404: 119 | return nil, ErrServiceNotFound 120 | default: 121 | return nil, &ErrNameResolutionUnexpectedResponse{s, ErrNameResolutionUnknownError} 122 | } 123 | 124 | var service Service 125 | 126 | data, err := ioutil.ReadAll(response.Body) 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | err = json.Unmarshal(data, &service) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | return &service, nil 137 | } 138 | -------------------------------------------------------------------------------- /integration/backend/vendor/github.com/kelseyhightower/run/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package run provides helper functions for building Cloud Run 7 | applications. 8 | */ 9 | package run 10 | 11 | -------------------------------------------------------------------------------- /integration/backend/vendor/github.com/kelseyhightower/run/env.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import "os" 4 | 5 | // Port returns the port your HTTP server should listen on. 6 | func Port() string { 7 | return os.Getenv("PORT") 8 | } 9 | 10 | // Revision returns the name of the Cloud Run revision being run. 11 | func Revision() string { 12 | return os.Getenv("K_REVISION") 13 | } 14 | 15 | // Configuration returns the name of the Cloud Run configuration being run. 16 | func Configuration() string { 17 | return os.Getenv("K_CONFIGURATION") 18 | } 19 | 20 | // ServiceName returns the name of the Cloud Run service being run. 21 | func ServiceName() string { 22 | return os.Getenv("K_SERVICE") 23 | } 24 | -------------------------------------------------------------------------------- /integration/backend/vendor/github.com/kelseyhightower/run/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kelseyhightower/run 2 | 3 | go 1.14 4 | -------------------------------------------------------------------------------- /integration/backend/vendor/github.com/kelseyhightower/run/http.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | "os/signal" 11 | "strings" 12 | "syscall" 13 | ) 14 | 15 | var serviceCache *cache 16 | 17 | func init() { 18 | data := make(map[string]string) 19 | serviceCache = &cache{data} 20 | } 21 | 22 | // Transport is an http.RoundTripper that attaches ID tokens to all 23 | // all outgoing request. 24 | type Transport struct { 25 | // Base optionally provides an http.RoundTripper that handles the 26 | // request. If nil, http.DefaultTransport is used. 27 | Base http.RoundTripper 28 | 29 | // EnableServiceNameResolution, if true, enables the resolution 30 | // of service names using the Cloud Run API. 31 | // 32 | // When true, HTTP requests are modified by replacing the original 33 | // HTTP target URL with the URL from the named Cloud Run service 34 | // in the same region as the caller. 35 | // 36 | // Examples: 37 | // 38 | // http://service => https://service-6bn2iswfgq-ue.a.run.app 39 | // https://service => https://service-6bn2iswfgq-ue.a.run.app 40 | // 41 | // Service accounts must have the roles/run.viewer IAM permission 42 | // to resolve service names using the Cloud Run API. 43 | EnableServiceNameResolution bool 44 | } 45 | 46 | func resolveServiceName(r *http.Request) error { 47 | var ( 48 | serviceName string 49 | region string 50 | project string 51 | ) 52 | 53 | parts := strings.Split(r.URL.Host, ".") 54 | 55 | switch n := len(parts); { 56 | case n > 1: 57 | return nil 58 | case n == 0: 59 | return nil 60 | case n == 1: 61 | serviceName = parts[0] 62 | } 63 | 64 | var u *url.URL 65 | endpoint := serviceCache.Get(serviceName) 66 | if endpoint == "" { 67 | service, err := getService(serviceName, region, project) 68 | if err != nil { 69 | return fmt.Errorf("run: error resolving service name: %w", err) 70 | } 71 | 72 | endpoint = service.Status.Address.URL 73 | serviceCache.Set(serviceName, endpoint) 74 | } 75 | 76 | u, err := url.Parse(endpoint) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | r.Host = u.Host 82 | r.URL.Host = u.Host 83 | r.URL.Scheme = u.Scheme 84 | r.Header.Set("Host", u.Hostname()) 85 | 86 | return nil 87 | } 88 | 89 | // RoundTrip implements http.RoundTripper. 90 | func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { 91 | if t.EnableServiceNameResolution { 92 | if err := resolveServiceName(req); err != nil { 93 | return nil, err 94 | } 95 | } 96 | 97 | idToken, err := IDToken(audFromRequest(req)) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", idToken)) 103 | if t.Base == nil { 104 | t.Base = http.DefaultTransport 105 | } 106 | 107 | return t.Base.RoundTrip(req) 108 | } 109 | 110 | // audFromRequest extracts the Cloud Run service URL from an HTTP request. 111 | func audFromRequest(r *http.Request) string { 112 | return fmt.Sprintf("%s://%s", r.URL.Scheme, r.URL.Hostname()) 113 | } 114 | 115 | // ListenAndServe starts an http.Server with the given handler listening 116 | // on the port defined by the PORT environment variable or "8080" if not 117 | // set. 118 | // 119 | // ListenAndServe traps the SIGINT and SIGTERM signals then gracefully 120 | // shuts down the server without interrupting any active connections by 121 | // calling the server's Shutdown method. 122 | // 123 | // ListenAndServe always returns a non-nil error; under normal conditions 124 | // http.ErrServerClosed will be returned indicating a successful graceful 125 | // shutdown. 126 | func ListenAndServe(handler http.Handler) error { 127 | port := os.Getenv("PORT") 128 | if port == "" { 129 | port = "8080" 130 | } 131 | 132 | addr := net.JoinHostPort("0.0.0.0", port) 133 | 134 | server := &http.Server{Addr: addr, Handler: handler} 135 | 136 | idleConnsClosed := make(chan struct{}) 137 | go func() { 138 | signalChan := make(chan os.Signal, 1) 139 | signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) 140 | <-signalChan 141 | 142 | Notice("Received shutdown signal; waiting for active connections to close") 143 | 144 | if err := server.Shutdown(context.Background()); err != nil { 145 | Error("Error during server shutdown: %v", err) 146 | } 147 | 148 | close(idleConnsClosed) 149 | }() 150 | 151 | if err := server.ListenAndServe(); err != http.ErrServerClosed { 152 | return err 153 | } 154 | 155 | <-idleConnsClosed 156 | 157 | Notice("Shutdown complete") 158 | 159 | return http.ErrServerClosed 160 | } 161 | -------------------------------------------------------------------------------- /integration/backend/vendor/github.com/kelseyhightower/run/log.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "os" 9 | "runtime" 10 | "strconv" 11 | "strings" 12 | "sync" 13 | ) 14 | 15 | var dl = &Logger{out: os.Stdout} 16 | 17 | // An LogEntry represents a Stackdriver log entry. 18 | type LogEntry struct { 19 | Message string `json:"message"` 20 | Severity string `json:"severity,omitempty"` 21 | Component string `json:"component,omitempty"` 22 | SourceLocation *LogEntrySourceLocation `json:"logging.googleapis.com/sourceLocation,omitempty"` 23 | Trace string `json:"logging.googleapis.com/trace,omitempty"` 24 | } 25 | 26 | // A LogEntrySourceLocation holds source code location data. 27 | // 28 | // Location data is used to provide additional context when logging 29 | // to Stackdriver. 30 | type LogEntrySourceLocation struct { 31 | File string `json:"file,omitempty"` 32 | Function string `json:"function,omitempty"` 33 | Line string `json:"line,omitempty"` 34 | } 35 | 36 | // String returns a JSON formatted string expected by Stackdriver. 37 | func (le LogEntry) String() string { 38 | if le.Severity == "" { 39 | le.Severity = "INFO" 40 | } 41 | data, err := json.Marshal(le) 42 | if err != nil { 43 | fmt.Printf("json.Marshal: %v", err) 44 | } 45 | return string(data) 46 | } 47 | 48 | // A Logger represents an active logging object that generates JSON formatted 49 | // log entries to standard out. Logs are formatted as expected by Cloud Run's 50 | // Stackdriver integration. 51 | type Logger struct { 52 | mu sync.Mutex 53 | buf []byte 54 | out io.Writer 55 | } 56 | 57 | // Error calls Log on the default logger with severity set to ERROR. 58 | // 59 | // Arguments are handled in the manner of fmt.Print. 60 | func Error(v ...interface{}) { 61 | dl.Log("ERROR", v...) 62 | } 63 | 64 | // Fatal calls Log on the default logger with severity set to ERROR 65 | // followed by a call to os.Exit(1). 66 | // 67 | // Arguments are handled in the manner of fmt.Print. 68 | func Fatal(v ...interface{}) { 69 | dl.Log("ERROR", v...) 70 | os.Exit(1) 71 | } 72 | 73 | // Log writes logging events with the given severity. 74 | // 75 | // The string s contains the text to log. 76 | // 77 | // Source file location data will be included in log entires. 78 | // 79 | // Logs are written to stdout in the Stackdriver structured log 80 | // format. See https://cloud.google.com/logging/docs/structured-logging 81 | // for more details. 82 | func Log(severity, s string) { 83 | dl.Log(severity, s) 84 | } 85 | 86 | // SetOutput sets the output destination for the default logger. 87 | func SetOutput(w io.Writer) { 88 | dl.mu.Lock() 89 | defer dl.mu.Unlock() 90 | dl.out = w 91 | } 92 | 93 | // Info calls Log on the default logger with severity set to INFO. 94 | // 95 | // Arguments are handled in the manner of fmt.Print. 96 | func Info(v ...interface{}) { 97 | dl.Log("INFO", v...) 98 | } 99 | 100 | // Notice calls Log on the default logger with severity set to NOTICE. 101 | // 102 | // Arguments are handled in the manner of fmt.Print. 103 | func Notice(v ...interface{}) { 104 | dl.Log("NOTICE", v...) 105 | } 106 | 107 | // NewLogger creates a new Logger. 108 | func NewLogger() *Logger { 109 | return &Logger{out: os.Stdout} 110 | } 111 | 112 | // SetOutput sets the output destination for the logger. 113 | func (l *Logger) SetOutput(w io.Writer) { 114 | l.mu.Lock() 115 | defer l.mu.Unlock() 116 | l.out = w 117 | } 118 | 119 | // Info calls l.Log with severity set to INFO. 120 | // 121 | // Arguments are handled in the manner of fmt.Print. 122 | func (l *Logger) Info(v ...interface{}) { 123 | l.Log("INFO", v...) 124 | } 125 | 126 | // Error calls l.Log with severity set to ERROR. 127 | // 128 | // Arguments are handled in the manner of fmt.Print. 129 | func (l *Logger) Error(v ...interface{}) { 130 | l.Log("ERROR", v...) 131 | } 132 | 133 | // Fatal calls l.Log with severity set to ERROR followed by 134 | // a call to os.Exit(1). 135 | // 136 | // Arguments are handled in the manner of fmt.Print. 137 | func (l *Logger) Fatal(v ...interface{}) { 138 | l.Log("ERROR", v...) 139 | os.Exit(1) 140 | } 141 | 142 | // Notice calls l.Log with severity set to NOTICE. 143 | // 144 | // Arguments are handled in the manner of fmt.Print. 145 | func (l *Logger) Notice(v ...interface{}) { 146 | l.Log("NOTICE", v...) 147 | } 148 | 149 | // Log writes logging events with the given severity. 150 | // 151 | // If the first value is an *http.Request, the X-Cloud-Trace-Context 152 | // HTTP header will be extracted and included in the Stackdriver log 153 | // entry. 154 | // 155 | // Source file location data will be included in log entires. 156 | // 157 | // Logs are written to stdout in the Stackdriver structured log 158 | // format. See https://cloud.google.com/logging/docs/structured-logging 159 | // for more details. 160 | func (l *Logger) Log(severity string, v ...interface{}) { 161 | var traceID string 162 | 163 | tid := extractTraceID(v[0]) 164 | if tid != "" { 165 | // The first argument was an *http.Request or context object 166 | // and is not part of the message 167 | v = v[1:] 168 | 169 | pid, err := ProjectID() 170 | if err != nil { 171 | e := &LogEntry{ 172 | Message: fmt.Sprintf("unable to append trace to log, missing project id: %v", err.Error()), 173 | Severity: "ERROR", 174 | } 175 | l.write(e) 176 | } 177 | 178 | if pid == "" { 179 | e := &LogEntry{ 180 | Message: fmt.Sprint("unable to append trace to log, project id is empty"), 181 | Severity: "ERROR", 182 | } 183 | l.write(e) 184 | } else { 185 | traceID = fmt.Sprintf("projects/%s/traces/%s", pid, tid) 186 | } 187 | } 188 | 189 | var sourceLocation *LogEntrySourceLocation 190 | pc, file, line, ok := runtime.Caller(2) 191 | if ok { 192 | sourceLocation = &LogEntrySourceLocation{ 193 | File: file, 194 | Line: strconv.Itoa(line), 195 | Function: runtime.FuncForPC(pc).Name(), 196 | } 197 | } 198 | 199 | e := &LogEntry{ 200 | Message: fmt.Sprint(v...), 201 | Severity: severity, 202 | SourceLocation: sourceLocation, 203 | Trace: traceID, 204 | } 205 | 206 | l.write(e) 207 | } 208 | 209 | func (l *Logger) write(e *LogEntry) { 210 | l.mu.Lock() 211 | defer l.mu.Unlock() 212 | 213 | s := e.String() 214 | l.buf = l.buf[:0] 215 | l.buf = append(l.buf, s...) 216 | if len(s) == 0 || s[len(s)-1] != '\n' { 217 | l.buf = append(l.buf, '\n') 218 | } 219 | 220 | l.out.Write(l.buf) 221 | } 222 | 223 | func extractTraceID(v interface{}) string { 224 | var trace string 225 | 226 | switch t := v.(type) { 227 | case *http.Request: 228 | traceHeader := t.Header.Get("X-Cloud-Trace-Context") 229 | ts := strings.Split(traceHeader, "/") 230 | if len(ts) > 0 && len(ts[0]) > 0 { 231 | trace = ts[0] 232 | } 233 | default: 234 | trace = "" 235 | } 236 | 237 | return trace 238 | } 239 | -------------------------------------------------------------------------------- /integration/backend/vendor/github.com/kelseyhightower/run/metadata.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "path" 10 | "strings" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | var rmu sync.Mutex 16 | 17 | var ( 18 | runtimeID string 19 | runtimeProjectID string 20 | runtimeRegion string 21 | runtimeNumericProjectID string 22 | ) 23 | 24 | var metadataEndpoint = "http://metadata.google.internal" 25 | 26 | // ErrMetadataNotFound is returned when a metadata key is not found. 27 | var ErrMetadataNotFound = errors.New("run: metadata key not found") 28 | 29 | // ErrMetadataInvalidRequest is returned when a metadata request is invalid. 30 | var ErrMetadataInvalidRequest = errors.New("run: invalid metadata request") 31 | 32 | // ErrMetadataUnknownError is return when calls to the metadata server 33 | // return an unknown error. 34 | var ErrMetadataUnknownError = errors.New("run: unexpected error retrieving metadata key") 35 | 36 | // ErrMetadataUnexpectedResponse is returned when calls to the metadata server 37 | // return an unexpected response. 38 | type ErrMetadataUnexpectedResponse struct { 39 | StatusCode int 40 | Err error 41 | } 42 | 43 | func (e *ErrMetadataUnexpectedResponse) Error() string { 44 | return "run: unexpected error retrieving metadata key" 45 | } 46 | 47 | func (e *ErrMetadataUnexpectedResponse) Unwrap() error { return e.Err } 48 | 49 | // AccessToken holds a GCP access token. 50 | type AccessToken struct { 51 | AccessToken string `json:"access_token"` 52 | ExpiresIn int64 `json:"expires_in"` 53 | TokenType string `json:"token_type"` 54 | } 55 | 56 | // ProjectID returns the active project ID from the metadata service. 57 | func ProjectID() (string, error) { 58 | rmu.Lock() 59 | defer rmu.Unlock() 60 | 61 | if runtimeProjectID != "" { 62 | return runtimeProjectID, nil 63 | } 64 | 65 | endpoint := fmt.Sprintf("%s/computeMetadata/v1/project/project-id", metadataEndpoint) 66 | 67 | data, err := metadataRequest(endpoint) 68 | if err != nil { 69 | return "", err 70 | } 71 | 72 | runtimeProjectID = string(data) 73 | return runtimeProjectID, nil 74 | } 75 | 76 | // NumericProjectID returns the active project ID from the metadata service. 77 | func NumericProjectID() (string, error) { 78 | rmu.Lock() 79 | defer rmu.Unlock() 80 | 81 | if runtimeNumericProjectID != "" { 82 | return runtimeNumericProjectID, nil 83 | } 84 | 85 | endpoint := fmt.Sprintf("%s/computeMetadata/v1/project/numeric-project-id", metadataEndpoint) 86 | 87 | data, err := metadataRequest(endpoint) 88 | if err != nil { 89 | return "", err 90 | } 91 | 92 | runtimeNumericProjectID = string(data) 93 | return runtimeNumericProjectID, nil 94 | } 95 | 96 | // Token returns the default service account token. 97 | func Token(scopes []string) (*AccessToken, error) { 98 | s := strings.Join(scopes, ",") 99 | 100 | endpoint := fmt.Sprintf("%s/computeMetadata/v1/instance/service-accounts/default/token?scopes=%s", metadataEndpoint, s) 101 | data, err := metadataRequest(endpoint) 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | var accessToken AccessToken 107 | err = json.Unmarshal(data, &accessToken) 108 | if err != nil { 109 | return nil, fmt.Errorf("run/metadata: error retrieving access token: %v", err) 110 | } 111 | 112 | return &accessToken, nil 113 | } 114 | 115 | // IDToken returns an id token based on the service url. 116 | func IDToken(serviceURL string) (string, error) { 117 | endpoint := fmt.Sprintf("%s/computeMetadata/v1/instance/service-accounts/default/identity?audience=%s", metadataEndpoint, serviceURL) 118 | 119 | idToken, err := metadataRequest(endpoint) 120 | if err != nil { 121 | return "", fmt.Errorf("metadata.Get: failed to query id_token: %w", err) 122 | } 123 | return string(idToken), nil 124 | } 125 | 126 | // Region returns the name of the Cloud Run region. 127 | func Region() (string, error) { 128 | rmu.Lock() 129 | defer rmu.Unlock() 130 | 131 | if runtimeRegion != "" { 132 | return runtimeRegion, nil 133 | } 134 | 135 | endpoint := fmt.Sprintf("%s/computeMetadata/v1/instance/region", metadataEndpoint) 136 | 137 | data, err := metadataRequest(endpoint) 138 | if err != nil { 139 | return "", err 140 | } 141 | 142 | runtimeRegion = path.Base(string(data)) 143 | return runtimeRegion, nil 144 | } 145 | 146 | // ID returns the unique identifier of the container instance. 147 | func ID() (string, error) { 148 | rmu.Lock() 149 | defer rmu.Unlock() 150 | 151 | if runtimeID != "" { 152 | return runtimeID, nil 153 | } 154 | 155 | endpoint := fmt.Sprintf("%s/computeMetadata/v1/instance/id", metadataEndpoint) 156 | 157 | data, err := metadataRequest(endpoint) 158 | if err != nil { 159 | return "", err 160 | } 161 | 162 | runtimeID = string(data) 163 | return runtimeID, nil 164 | } 165 | 166 | func metadataRequest(endpoint string) ([]byte, error) { 167 | request, err := http.NewRequest("GET", endpoint, nil) 168 | if err != nil { 169 | return nil, err 170 | } 171 | 172 | request.Header.Set("User-Agent", userAgent) 173 | request.Header.Add("Metadata-Flavor", "Google") 174 | 175 | timeout := time.Duration(5) * time.Second 176 | httpClient := http.Client{Timeout: timeout} 177 | 178 | response, err := httpClient.Do(request) 179 | if err != nil { 180 | return nil, err 181 | } 182 | defer response.Body.Close() 183 | 184 | switch s := response.StatusCode; s { 185 | case 200: 186 | break 187 | case 400: 188 | return nil, ErrMetadataInvalidRequest 189 | case 404: 190 | return nil, ErrMetadataNotFound 191 | default: 192 | return nil, &ErrMetadataUnexpectedResponse{s, ErrMetadataUnknownError} 193 | } 194 | 195 | data, err := ioutil.ReadAll(response.Body) 196 | if err != nil { 197 | return nil, err 198 | } 199 | 200 | return data, nil 201 | } 202 | -------------------------------------------------------------------------------- /integration/backend/vendor/github.com/kelseyhightower/run/secrets.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "time" 11 | ) 12 | 13 | var ( 14 | secretmanagerEndpoint = "https://secretmanager.googleapis.com/v1" 15 | ) 16 | 17 | // ErrSecretPermissionDenied is returned when access to a secret is denied. 18 | var ErrSecretPermissionDenied = errors.New("run: permission denied to named secret") 19 | 20 | // ErrSecretUnauthorized is returned when calls to the Secret 21 | // Manager API are unauthorized. 22 | var ErrSecretUnauthorized = errors.New("run: secret manager unauthorized") 23 | 24 | // ErrSecretNotFound is returned when a secret is not found. 25 | var ErrSecretNotFound = errors.New("run: named secret not found") 26 | 27 | // ErrSecretUnknownError is return when calls to the Secret Manager 28 | // API return an unknown error. 29 | var ErrSecretUnknownError = errors.New("run: unexpected error retrieving named secret") 30 | 31 | // ErrSecretUnexpectedResponse is returned when calls to the Secret Manager 32 | // API return an unexpected response. 33 | type ErrSecretUnexpectedResponse struct { 34 | StatusCode int 35 | Err error 36 | } 37 | 38 | func (e *ErrSecretUnexpectedResponse) Error() string { 39 | return "run: unexpected error retrieving named secret" 40 | } 41 | 42 | func (e *ErrSecretUnexpectedResponse) Unwrap() error { return e.Err } 43 | 44 | // SecretVersion represents a Google Cloud Secret. 45 | type SecretVersion struct { 46 | Name string 47 | Payload SecretPayload `json:"payload"` 48 | } 49 | 50 | // SecretPayload holds the secret payload for a Google Cloud Secret. 51 | type SecretPayload struct { 52 | // A base64-encoded string. 53 | Data string `json:"data"` 54 | } 55 | 56 | func formatSecretVersion(project, name, version string) string { 57 | return fmt.Sprintf("projects/%s/secrets/%s/versions/%s", project, name, version) 58 | } 59 | 60 | // AccessSecretVersion returns a Google Cloud Secret for the given 61 | // secret name and version. 62 | func AccessSecretVersion(name, version string) ([]byte, error) { 63 | return accessSecretVersion(name, version) 64 | } 65 | 66 | // AccessSecret returns the latest version of a Google Cloud Secret 67 | // for the given name. 68 | func AccessSecret(name string) ([]byte, error) { 69 | return accessSecretVersion(name, "latest") 70 | } 71 | 72 | func accessSecretVersion(name, version string) ([]byte, error) { 73 | if version == "" { 74 | version = "latest" 75 | } 76 | 77 | token, err := Token([]string{"https://www.googleapis.com/auth/cloud-platform"}) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | numericProjectID, err := NumericProjectID() 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | secretVersion := formatSecretVersion(numericProjectID, name, version) 88 | endpoint := fmt.Sprintf("%s/%s:access", secretmanagerEndpoint, secretVersion) 89 | 90 | request, err := http.NewRequest("GET", endpoint, nil) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | request.Header.Set("User-Agent", userAgent) 96 | request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) 97 | 98 | timeout := time.Duration(5) * time.Second 99 | httpClient := http.Client{Timeout: timeout} 100 | 101 | response, err := httpClient.Do(request) 102 | if err != nil { 103 | return nil, err 104 | } 105 | defer response.Body.Close() 106 | 107 | switch s := response.StatusCode; s { 108 | case 200: 109 | break 110 | case 401: 111 | return nil, ErrSecretUnauthorized 112 | case 403: 113 | return nil, ErrSecretPermissionDenied 114 | case 404: 115 | return nil, ErrSecretNotFound 116 | default: 117 | return nil, &ErrSecretUnexpectedResponse{s, ErrSecretUnknownError} 118 | } 119 | 120 | data, err := ioutil.ReadAll(response.Body) 121 | if err != nil { 122 | return nil, err 123 | } 124 | 125 | var s SecretVersion 126 | err = json.Unmarshal(data, &s) 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | decodedData, err := base64.StdEncoding.DecodeString(s.Payload.Data) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | return decodedData, nil 137 | } 138 | -------------------------------------------------------------------------------- /integration/backend/vendor/github.com/kelseyhightower/run/useragent.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | const ( 4 | userAgent = "golang-run/0.0.8" 5 | ) 6 | -------------------------------------------------------------------------------- /integration/backend/vendor/modules.txt: -------------------------------------------------------------------------------- 1 | # github.com/kelseyhightower/run v0.0.0-00010101000000-000000000000 => ../../ 2 | ## explicit 3 | github.com/kelseyhightower/run 4 | # github.com/kelseyhightower/run => ../../ 5 | -------------------------------------------------------------------------------- /integration/bin/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PROJECT_ID=$(gcloud config get-value project) 4 | 5 | go mod vendor 6 | 7 | gcloud builds submit \ 8 | -t gcr.io/${PROJECT_ID}/run-integration-tests:0.0.17 . 9 | 10 | cd backend 11 | 12 | go mod vendor 13 | 14 | gcloud builds submit \ 15 | -t gcr.io/${PROJECT_ID}/run-integration-backend:0.0.17 . 16 | -------------------------------------------------------------------------------- /integration/bin/create-secrets: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | gcloud secrets create run-secrets-integration-tests \ 4 | --data-file secret \ 5 | --locations=us-central1,us-east1 \ 6 | --replication-policy=user-managed 7 | -------------------------------------------------------------------------------- /integration/bin/create-service-accounts: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PROJECT_ID=$(gcloud config get-value project) 4 | 5 | gcloud iam service-accounts create run-integration-backend 6 | 7 | gcloud iam service-accounts create run-integration-tests 8 | 9 | gcloud run services add-iam-policy-binding run-integration-backend \ 10 | --member serviceAccount:run-integration-tests@${PROJECT_ID}.iam.gserviceaccount.com \ 11 | --region asia-northeast1 \ 12 | --platform managed \ 13 | --role roles/run.invoker 14 | 15 | gcloud projects add-iam-policy-binding ${PROJECT_ID} \ 16 | --member serviceAccount:run-integration-tests@${PROJECT_ID}.iam.gserviceaccount.com \ 17 | --role roles/run.viewer 18 | 19 | gcloud secrets add-iam-policy-binding run-secrets-integration-tests \ 20 | --member serviceAccount:run-integration-tests@${PROJECT_ID}.iam.gserviceaccount.com \ 21 | --role="roles/secretmanager.secretAccessor" 22 | -------------------------------------------------------------------------------- /integration/bin/deploy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PROJECT_ID=$(gcloud config get-value project) 4 | 5 | gcloud run deploy run-integration-backend \ 6 | --no-allow-unauthenticated \ 7 | --concurrency 10 \ 8 | --cpu 1 \ 9 | --image gcr.io/${PROJECT_ID}/run-integration-backend:0.0.17 \ 10 | --memory '128Mi' \ 11 | --platform managed \ 12 | --region asia-northeast1 \ 13 | --service-account run-integration-backend 14 | 15 | gcloud run deploy run-integration-tests \ 16 | --allow-unauthenticated \ 17 | --concurrency 10 \ 18 | --cpu 1 \ 19 | --image gcr.io/${PROJECT_ID}/run-integration-tests:0.0.17 \ 20 | --memory '128Mi' \ 21 | --platform managed \ 22 | --region asia-northeast1 \ 23 | --service-account run-integration-tests 24 | -------------------------------------------------------------------------------- /integration/bin/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | URL=$(gcloud run services describe run-integration-tests \ 4 | --platform managed \ 5 | --region asia-northeast1 \ 6 | --format 'value(status.url)') 7 | 8 | curl ${URL}/tests/env 9 | curl ${URL}/tests/metadata 10 | curl ${URL}/tests/secrets 11 | curl ${URL}/tests/service-authentication 12 | -------------------------------------------------------------------------------- /integration/env.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/kelseyhightower/run" 8 | ) 9 | 10 | type EnvTestResults struct { 11 | Port string `json:"port"` 12 | Revision string `json:"revision"` 13 | Configuration string `json:"configuration"` 14 | ServiceName string `json:"service_name"` 15 | } 16 | 17 | func envTestHandler(w http.ResponseWriter, r *http.Request) { 18 | run.Info(r, "Starting env tests...") 19 | 20 | result := EnvTestResults{ 21 | Port: run.Port(), 22 | Revision: run.Revision(), 23 | Configuration: run.Configuration(), 24 | ServiceName: run.ServiceName(), 25 | } 26 | 27 | data, err := json.MarshalIndent(result, "", " ") 28 | if err != nil { 29 | run.Error(r, err) 30 | http.Error(w, err.Error(), 500) 31 | return 32 | } 33 | 34 | w.Write(data) 35 | } 36 | -------------------------------------------------------------------------------- /integration/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kelseyhightower/run-integration-tests 2 | 3 | go 1.14 4 | 5 | replace github.com/kelseyhightower/run => ../ 6 | 7 | require github.com/kelseyhightower/run v0.0.0-00010101000000-000000000000 8 | -------------------------------------------------------------------------------- /integration/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/kelseyhightower/run" 7 | ) 8 | 9 | func main() { 10 | run.Notice("Starting run integration tests...") 11 | 12 | http.HandleFunc("/tests/env", envTestHandler) 13 | http.HandleFunc("/tests/metadata", metadataTestHandler) 14 | http.HandleFunc("/tests/secrets", secretsTestHandler) 15 | http.HandleFunc("/tests/service-authentication", serviceAuthenticationTestHandler) 16 | 17 | if err := run.ListenAndServe(nil); err != http.ErrServerClosed { 18 | run.Fatal(err) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /integration/metadata.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/kelseyhightower/run" 8 | ) 9 | 10 | type MetadataTestResults struct { 11 | ID string `json:"id"` 12 | NumericProjectID string `json:"numeric_project_id"` 13 | ProjectID string `json:"project_id"` 14 | Region string `json:"region"` 15 | } 16 | 17 | func metadataTestHandler(w http.ResponseWriter, r *http.Request) { 18 | run.Info(r, "Starting metadata tests...") 19 | 20 | id, err := run.ID() 21 | if err != nil { 22 | run.Error(r, err) 23 | http.Error(w, err.Error(), 500) 24 | return 25 | } 26 | 27 | numericProjectID, err := run.NumericProjectID() 28 | if err != nil { 29 | run.Error(r, err) 30 | http.Error(w, err.Error(), 500) 31 | return 32 | } 33 | 34 | projectID, err := run.ProjectID() 35 | if err != nil { 36 | run.Error(r, err) 37 | http.Error(w, err.Error(), 500) 38 | return 39 | } 40 | 41 | region, err := run.Region() 42 | if err != nil { 43 | run.Error(r, err) 44 | http.Error(w, err.Error(), 500) 45 | return 46 | } 47 | 48 | result := MetadataTestResults{ 49 | ID: id, 50 | NumericProjectID: numericProjectID, 51 | ProjectID: projectID, 52 | Region: region, 53 | } 54 | 55 | data, err := json.MarshalIndent(result, "", " ") 56 | if err != nil { 57 | run.Error(r, err) 58 | http.Error(w, err.Error(), 500) 59 | return 60 | } 61 | 62 | w.Write(data) 63 | } 64 | -------------------------------------------------------------------------------- /integration/secrets.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/kelseyhightower/run" 8 | ) 9 | 10 | type SecretsTestResults struct { 11 | Secret string `json:"secret"` 12 | } 13 | 14 | func secretsTestHandler(w http.ResponseWriter, r *http.Request) { 15 | run.Info(r, "Starting secrets tests...") 16 | 17 | secret, err := run.AccessSecret("run-secrets-integration-tests") 18 | if err != nil { 19 | run.Error(r, err) 20 | http.Error(w, err.Error(), 500) 21 | return 22 | } 23 | 24 | result := &SecretsTestResults{ 25 | Secret: string(secret), 26 | } 27 | 28 | data, err := json.MarshalIndent(result, "", " ") 29 | if err != nil { 30 | run.Error(r, err) 31 | http.Error(w, err.Error(), 500) 32 | return 33 | } 34 | 35 | w.Write(data) 36 | } 37 | -------------------------------------------------------------------------------- /integration/service-authentication.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | 8 | "github.com/kelseyhightower/run" 9 | ) 10 | 11 | type ServiceAuthenticationTestResults struct { 12 | Status string `json:"status"` 13 | } 14 | 15 | func serviceAuthenticationTestHandler(w http.ResponseWriter, r *http.Request) { 16 | run.Info(r, "Starting service authentication tests...") 17 | 18 | request, err := http.NewRequest("GET", "https://run-integration-backend", nil) 19 | if err != nil { 20 | run.Error(r, err) 21 | http.Error(w, err.Error(), 500) 22 | return 23 | } 24 | 25 | tr := &run.Transport{ 26 | EnableServiceNameResolution: true, 27 | } 28 | 29 | httpClient := http.Client{Transport: tr} 30 | 31 | response, err := httpClient.Do(request) 32 | if err != nil { 33 | run.Error(r, err) 34 | http.Error(w, err.Error(), 500) 35 | return 36 | } 37 | 38 | defer response.Body.Close() 39 | 40 | responseData, err := ioutil.ReadAll(response.Body) 41 | if err != nil { 42 | run.Error(r, err) 43 | http.Error(w, err.Error(), 500) 44 | return 45 | } 46 | 47 | result := ServiceAuthenticationTestResults{ 48 | Status: string(responseData), 49 | } 50 | 51 | data, err := json.MarshalIndent(result, "", " ") 52 | if err != nil { 53 | run.Error(r, err) 54 | http.Error(w, err.Error(), 500) 55 | return 56 | } 57 | 58 | w.Write(data) 59 | } 60 | -------------------------------------------------------------------------------- /integration/vendor/github.com/kelseyhightower/run/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /integration/vendor/github.com/kelseyhightower/run/README.md: -------------------------------------------------------------------------------- 1 | # run 2 | 3 | [![GoDoc](https://godoc.org/github.com/kelseyhightower/run?status.svg)](https://pkg.go.dev/github.com/kelseyhightower/run) ![CloudBuild](https://badger-6bn2iswfgq-ue.a.run.app/build/status?project=hightowerlabs&id=bb0129f8-02c4-490b-b37e-777215fdb7ca) 4 | 5 | The run package provides a set of Cloud Run helper functions and does not leverage any third party dependencies. 6 | 7 | ## Usage 8 | 9 | ```Go 10 | package main 11 | 12 | import ( 13 | "net/http" 14 | "os" 15 | 16 | "github.com/kelseyhightower/run" 17 | ) 18 | 19 | func main() { 20 | // Generates structured logs optimized for Cloud Run. 21 | run.Notice("Starting helloworld service...") 22 | 23 | // Easy access to secrets stored in Secret Manager. 24 | secret, err := run.AccessSecret("foo") 25 | if err != nil { 26 | run.Fatal(err) 27 | } 28 | 29 | _ = secret 30 | 31 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 32 | // Optionally pass in the *http.Request as the first argument 33 | // to correlate container logs with request logs. 34 | run.Info(r, "handling http request") 35 | 36 | w.Write([]byte("Hello world!\n")) 37 | }) 38 | 39 | // Start an HTTP server listening on the address defined by the 40 | // Cloud Run container runtime contract and gracefully shutdown 41 | // when terminated. 42 | if err := run.ListenAndServe(nil); err != http.ErrServerClosed { 43 | run.Fatal(err) 44 | } 45 | } 46 | ``` 47 | 48 | ### Service Authentication 49 | 50 | run takes the pain out of [service-to-service authentication](https://cloud.google.com/run/docs/authenticating/service-to-service) 51 | 52 | ```Go 53 | package main 54 | 55 | import ( 56 | "net/http" 57 | 58 | "github.com/kelseyhightower/run" 59 | ) 60 | 61 | func main() { 62 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 63 | request, err := http.NewRequest("GET", "https://example-6bn2iswfgq-uw.a.run.app", nil) 64 | if err != nil { 65 | http.Error(w, err.Error(), 500) 66 | return 67 | } 68 | 69 | // Use the run.Transport to automatically attach ID tokens to outbound requests 70 | // and optionally expand service names using the Cloud Run API. 71 | // See https://pkg.go.dev/github.com/kelseyhightower/run?tab=doc#Transport 72 | client := http.Client{Transport: &run.Transport{EnableServiceNameResolution: false}} 73 | 74 | response, err := client.Do(request) 75 | if err != nil { 76 | http.Error(w, err.Error(), 500) 77 | return 78 | } 79 | defer response.Body.Close() 80 | }) 81 | 82 | if err := run.ListenAndServe(nil); err != http.ErrServerClosed { 83 | run.Fatal(err) 84 | } 85 | } 86 | ``` 87 | 88 | ## Status 89 | 90 | This package is experimental and should not be used or assumed to be stable. Breaking changes are guaranteed to happen. 91 | -------------------------------------------------------------------------------- /integration/vendor/github.com/kelseyhightower/run/cache.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | type cache struct { 4 | data map[string]string 5 | } 6 | 7 | func (c *cache) Set(key, value string) { 8 | c.data[key] = value 9 | return 10 | } 11 | 12 | func (c *cache) Get(key string) string { 13 | if value, ok := c.data[key]; ok { 14 | return value 15 | } 16 | 17 | return "" 18 | } 19 | -------------------------------------------------------------------------------- /integration/vendor/github.com/kelseyhightower/run/cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - name: golang 3 | id: 'go-test' 4 | entrypoint: 'bash' 5 | args: 6 | - '-c' 7 | - | 8 | go test -v 9 | - name: golang 10 | id: 'go-tool-cover' 11 | entrypoint: 'bash' 12 | args: 13 | - '-c' 14 | - | 15 | go test -v -coverprofile=c.out 16 | go tool cover -html=c.out -o coverage.html 17 | artifacts: 18 | objects: 19 | location: 'gs://hightowerlabs/run' 20 | paths: ['coverage.html'] 21 | -------------------------------------------------------------------------------- /integration/vendor/github.com/kelseyhightower/run/cloudrun.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | var cloudrunEndpoint = "https://%s-run.googleapis.com" 13 | 14 | // ErrNameResolutionPermissionDenied is returned when access to the 15 | // Cloud Run API is denied. 16 | var ErrNameResolutionPermissionDenied = errors.New("run: permission denied to named service") 17 | 18 | // ErrNameResolutionUnauthorized is returned when calls to the Cloud 19 | // Run API are unauthorized. 20 | var ErrNameResolutionUnauthorized = errors.New("run: cloud run api unauthorized") 21 | 22 | // ErrServiceNotFound is returned when a service is not found.. 23 | var ErrServiceNotFound = errors.New("run: named service not found") 24 | 25 | // ErrNameResolutionUnknownError is return when calls to the Cloud Run 26 | // API return an unknown error. 27 | var ErrNameResolutionUnknownError = errors.New("run: unexpected error retrieving named service") 28 | 29 | // ErrNameResolutionUnexpectedResponse is returned when calls to the Cloud Run 30 | // API return an unexpected response. 31 | type ErrNameResolutionUnexpectedResponse struct { 32 | StatusCode int 33 | Err error 34 | } 35 | 36 | func (e *ErrNameResolutionUnexpectedResponse) Error() string { 37 | return "run: unexpected error retrieving named service" 38 | } 39 | 40 | func (e *ErrNameResolutionUnexpectedResponse) Unwrap() error { return e.Err } 41 | 42 | // Service represents a Cloud Run service. 43 | type Service struct { 44 | Status ServiceStatus `json:"status"` 45 | } 46 | 47 | // ServiceStatus holds the current state of the Cloud Run service. 48 | type ServiceStatus struct { 49 | // URL holds the url that will distribute traffic over the 50 | // provided traffic targets. It generally has the form 51 | // https://{route-hash}-{project-hash}-{cluster-level-suffix}.a.run.app 52 | URL string `json:"url"` 53 | 54 | // Similar to url, information on where the service is available on HTTP. 55 | Address ServiceAddresss `json:"address"` 56 | } 57 | 58 | type ServiceAddresss struct { 59 | URL string `json:"url"` 60 | } 61 | 62 | func regionalEndpoint(region string) string { 63 | if region == "test" { 64 | return cloudrunEndpoint 65 | } 66 | return fmt.Sprintf(cloudrunEndpoint, region) 67 | } 68 | 69 | func getService(name, region, project string) (*Service, error) { 70 | var err error 71 | 72 | if region == "" { 73 | region, err = Region() 74 | if err != nil { 75 | return nil, err 76 | } 77 | } 78 | 79 | if project == "" { 80 | project, err = ProjectID() 81 | if err != nil { 82 | return nil, err 83 | } 84 | } 85 | 86 | endpoint := fmt.Sprintf("%s/apis/serving.knative.dev/v1/namespaces/%s/services/%s", 87 | regionalEndpoint(region), project, name) 88 | 89 | token, err := Token([]string{"https://www.googleapis.com/auth/cloud-platform"}) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | request, err := http.NewRequest("GET", endpoint, nil) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | request.Header.Set("User-Agent", userAgent) 100 | request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) 101 | 102 | timeout := time.Duration(5) * time.Second 103 | httpClient := &http.Client{Timeout: timeout} 104 | 105 | response, err := httpClient.Do(request) 106 | if err != nil { 107 | return nil, err 108 | } 109 | defer response.Body.Close() 110 | 111 | switch s := response.StatusCode; s { 112 | case 200: 113 | break 114 | case 401: 115 | return nil, ErrNameResolutionUnauthorized 116 | case 403: 117 | return nil, ErrNameResolutionPermissionDenied 118 | case 404: 119 | return nil, ErrServiceNotFound 120 | default: 121 | return nil, &ErrNameResolutionUnexpectedResponse{s, ErrNameResolutionUnknownError} 122 | } 123 | 124 | var service Service 125 | 126 | data, err := ioutil.ReadAll(response.Body) 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | err = json.Unmarshal(data, &service) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | return &service, nil 137 | } 138 | -------------------------------------------------------------------------------- /integration/vendor/github.com/kelseyhightower/run/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package run provides helper functions for building Cloud Run 7 | applications. 8 | */ 9 | package run 10 | 11 | -------------------------------------------------------------------------------- /integration/vendor/github.com/kelseyhightower/run/env.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import "os" 4 | 5 | // Port returns the port your HTTP server should listen on. 6 | func Port() string { 7 | return os.Getenv("PORT") 8 | } 9 | 10 | // Revision returns the name of the Cloud Run revision being run. 11 | func Revision() string { 12 | return os.Getenv("K_REVISION") 13 | } 14 | 15 | // Configuration returns the name of the Cloud Run configuration being run. 16 | func Configuration() string { 17 | return os.Getenv("K_CONFIGURATION") 18 | } 19 | 20 | // ServiceName returns the name of the Cloud Run service being run. 21 | func ServiceName() string { 22 | return os.Getenv("K_SERVICE") 23 | } 24 | -------------------------------------------------------------------------------- /integration/vendor/github.com/kelseyhightower/run/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kelseyhightower/run 2 | 3 | go 1.14 4 | -------------------------------------------------------------------------------- /integration/vendor/github.com/kelseyhightower/run/http.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | "os/signal" 11 | "strings" 12 | "syscall" 13 | ) 14 | 15 | var serviceCache *cache 16 | 17 | func init() { 18 | data := make(map[string]string) 19 | serviceCache = &cache{data} 20 | } 21 | 22 | // Transport is an http.RoundTripper that attaches ID tokens to all 23 | // all outgoing request. 24 | type Transport struct { 25 | // Base optionally provides an http.RoundTripper that handles the 26 | // request. If nil, http.DefaultTransport is used. 27 | Base http.RoundTripper 28 | 29 | // EnableServiceNameResolution, if true, enables the resolution 30 | // of service names using the Cloud Run API. 31 | // 32 | // When true, HTTP requests are modified by replacing the original 33 | // HTTP target URL with the URL from the named Cloud Run service 34 | // in the same region as the caller. 35 | // 36 | // Examples: 37 | // 38 | // http://service => https://service-6bn2iswfgq-ue.a.run.app 39 | // https://service => https://service-6bn2iswfgq-ue.a.run.app 40 | // 41 | // Service accounts must have the roles/run.viewer IAM permission 42 | // to resolve service names using the Cloud Run API. 43 | EnableServiceNameResolution bool 44 | } 45 | 46 | func resolveServiceName(r *http.Request) error { 47 | var ( 48 | serviceName string 49 | region string 50 | project string 51 | ) 52 | 53 | parts := strings.Split(r.URL.Host, ".") 54 | 55 | switch n := len(parts); { 56 | case n > 1: 57 | return nil 58 | case n == 0: 59 | return nil 60 | case n == 1: 61 | serviceName = parts[0] 62 | } 63 | 64 | var u *url.URL 65 | endpoint := serviceCache.Get(serviceName) 66 | if endpoint == "" { 67 | service, err := getService(serviceName, region, project) 68 | if err != nil { 69 | return fmt.Errorf("run: error resolving service name: %w", err) 70 | } 71 | 72 | endpoint = service.Status.Address.URL 73 | serviceCache.Set(serviceName, endpoint) 74 | } 75 | 76 | u, err := url.Parse(endpoint) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | r.Host = u.Host 82 | r.URL.Host = u.Host 83 | r.URL.Scheme = u.Scheme 84 | r.Header.Set("Host", u.Hostname()) 85 | 86 | return nil 87 | } 88 | 89 | // RoundTrip implements http.RoundTripper. 90 | func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { 91 | if t.EnableServiceNameResolution { 92 | if err := resolveServiceName(req); err != nil { 93 | return nil, err 94 | } 95 | } 96 | 97 | idToken, err := IDToken(audFromRequest(req)) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", idToken)) 103 | if t.Base == nil { 104 | t.Base = http.DefaultTransport 105 | } 106 | 107 | return t.Base.RoundTrip(req) 108 | } 109 | 110 | // audFromRequest extracts the Cloud Run service URL from an HTTP request. 111 | func audFromRequest(r *http.Request) string { 112 | return fmt.Sprintf("%s://%s", r.URL.Scheme, r.URL.Hostname()) 113 | } 114 | 115 | // ListenAndServe starts an http.Server with the given handler listening 116 | // on the port defined by the PORT environment variable or "8080" if not 117 | // set. 118 | // 119 | // ListenAndServe traps the SIGINT and SIGTERM signals then gracefully 120 | // shuts down the server without interrupting any active connections by 121 | // calling the server's Shutdown method. 122 | // 123 | // ListenAndServe always returns a non-nil error; under normal conditions 124 | // http.ErrServerClosed will be returned indicating a successful graceful 125 | // shutdown. 126 | func ListenAndServe(handler http.Handler) error { 127 | port := os.Getenv("PORT") 128 | if port == "" { 129 | port = "8080" 130 | } 131 | 132 | addr := net.JoinHostPort("0.0.0.0", port) 133 | 134 | server := &http.Server{Addr: addr, Handler: handler} 135 | 136 | idleConnsClosed := make(chan struct{}) 137 | go func() { 138 | signalChan := make(chan os.Signal, 1) 139 | signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) 140 | <-signalChan 141 | 142 | Notice("Received shutdown signal; waiting for active connections to close") 143 | 144 | if err := server.Shutdown(context.Background()); err != nil { 145 | Error("Error during server shutdown: %v", err) 146 | } 147 | 148 | close(idleConnsClosed) 149 | }() 150 | 151 | if err := server.ListenAndServe(); err != http.ErrServerClosed { 152 | return err 153 | } 154 | 155 | <-idleConnsClosed 156 | 157 | Notice("Shutdown complete") 158 | 159 | return http.ErrServerClosed 160 | } 161 | -------------------------------------------------------------------------------- /integration/vendor/github.com/kelseyhightower/run/log.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "os" 9 | "runtime" 10 | "strconv" 11 | "strings" 12 | "sync" 13 | ) 14 | 15 | var dl = &Logger{out: os.Stdout} 16 | 17 | // An LogEntry represents a Stackdriver log entry. 18 | type LogEntry struct { 19 | Message string `json:"message"` 20 | Severity string `json:"severity,omitempty"` 21 | Component string `json:"component,omitempty"` 22 | SourceLocation *LogEntrySourceLocation `json:"logging.googleapis.com/sourceLocation,omitempty"` 23 | Trace string `json:"logging.googleapis.com/trace,omitempty"` 24 | } 25 | 26 | // A LogEntrySourceLocation holds source code location data. 27 | // 28 | // Location data is used to provide additional context when logging 29 | // to Stackdriver. 30 | type LogEntrySourceLocation struct { 31 | File string `json:"file,omitempty"` 32 | Function string `json:"function,omitempty"` 33 | Line string `json:"line,omitempty"` 34 | } 35 | 36 | // String returns a JSON formatted string expected by Stackdriver. 37 | func (le LogEntry) String() string { 38 | if le.Severity == "" { 39 | le.Severity = "INFO" 40 | } 41 | data, err := json.Marshal(le) 42 | if err != nil { 43 | fmt.Printf("json.Marshal: %v", err) 44 | } 45 | return string(data) 46 | } 47 | 48 | // A Logger represents an active logging object that generates JSON formatted 49 | // log entries to standard out. Logs are formatted as expected by Cloud Run's 50 | // Stackdriver integration. 51 | type Logger struct { 52 | mu sync.Mutex 53 | buf []byte 54 | out io.Writer 55 | } 56 | 57 | // Error calls Log on the default logger with severity set to ERROR. 58 | // 59 | // Arguments are handled in the manner of fmt.Print. 60 | func Error(v ...interface{}) { 61 | dl.Log("ERROR", v...) 62 | } 63 | 64 | // Fatal calls Log on the default logger with severity set to ERROR 65 | // followed by a call to os.Exit(1). 66 | // 67 | // Arguments are handled in the manner of fmt.Print. 68 | func Fatal(v ...interface{}) { 69 | dl.Log("ERROR", v...) 70 | os.Exit(1) 71 | } 72 | 73 | // Log writes logging events with the given severity. 74 | // 75 | // The string s contains the text to log. 76 | // 77 | // Source file location data will be included in log entires. 78 | // 79 | // Logs are written to stdout in the Stackdriver structured log 80 | // format. See https://cloud.google.com/logging/docs/structured-logging 81 | // for more details. 82 | func Log(severity, s string) { 83 | dl.Log(severity, s) 84 | } 85 | 86 | // SetOutput sets the output destination for the default logger. 87 | func SetOutput(w io.Writer) { 88 | dl.mu.Lock() 89 | defer dl.mu.Unlock() 90 | dl.out = w 91 | } 92 | 93 | // Info calls Log on the default logger with severity set to INFO. 94 | // 95 | // Arguments are handled in the manner of fmt.Print. 96 | func Info(v ...interface{}) { 97 | dl.Log("INFO", v...) 98 | } 99 | 100 | // Notice calls Log on the default logger with severity set to NOTICE. 101 | // 102 | // Arguments are handled in the manner of fmt.Print. 103 | func Notice(v ...interface{}) { 104 | dl.Log("NOTICE", v...) 105 | } 106 | 107 | // NewLogger creates a new Logger. 108 | func NewLogger() *Logger { 109 | return &Logger{out: os.Stdout} 110 | } 111 | 112 | // SetOutput sets the output destination for the logger. 113 | func (l *Logger) SetOutput(w io.Writer) { 114 | l.mu.Lock() 115 | defer l.mu.Unlock() 116 | l.out = w 117 | } 118 | 119 | // Info calls l.Log with severity set to INFO. 120 | // 121 | // Arguments are handled in the manner of fmt.Print. 122 | func (l *Logger) Info(v ...interface{}) { 123 | l.Log("INFO", v...) 124 | } 125 | 126 | // Error calls l.Log with severity set to ERROR. 127 | // 128 | // Arguments are handled in the manner of fmt.Print. 129 | func (l *Logger) Error(v ...interface{}) { 130 | l.Log("ERROR", v...) 131 | } 132 | 133 | // Fatal calls l.Log with severity set to ERROR followed by 134 | // a call to os.Exit(1). 135 | // 136 | // Arguments are handled in the manner of fmt.Print. 137 | func (l *Logger) Fatal(v ...interface{}) { 138 | l.Log("ERROR", v...) 139 | os.Exit(1) 140 | } 141 | 142 | // Notice calls l.Log with severity set to NOTICE. 143 | // 144 | // Arguments are handled in the manner of fmt.Print. 145 | func (l *Logger) Notice(v ...interface{}) { 146 | l.Log("NOTICE", v...) 147 | } 148 | 149 | // Log writes logging events with the given severity. 150 | // 151 | // If the first value is an *http.Request, the X-Cloud-Trace-Context 152 | // HTTP header will be extracted and included in the Stackdriver log 153 | // entry. 154 | // 155 | // Source file location data will be included in log entires. 156 | // 157 | // Logs are written to stdout in the Stackdriver structured log 158 | // format. See https://cloud.google.com/logging/docs/structured-logging 159 | // for more details. 160 | func (l *Logger) Log(severity string, v ...interface{}) { 161 | var traceID string 162 | 163 | tid := extractTraceID(v[0]) 164 | if tid != "" { 165 | // The first argument was an *http.Request or context object 166 | // and is not part of the message 167 | v = v[1:] 168 | 169 | pid, err := ProjectID() 170 | if err != nil { 171 | e := &LogEntry{ 172 | Message: fmt.Sprintf("unable to append trace to log, missing project id: %v", err.Error()), 173 | Severity: "ERROR", 174 | } 175 | l.write(e) 176 | } 177 | 178 | if pid == "" { 179 | e := &LogEntry{ 180 | Message: fmt.Sprint("unable to append trace to log, project id is empty"), 181 | Severity: "ERROR", 182 | } 183 | l.write(e) 184 | } else { 185 | traceID = fmt.Sprintf("projects/%s/traces/%s", pid, tid) 186 | } 187 | } 188 | 189 | var sourceLocation *LogEntrySourceLocation 190 | pc, file, line, ok := runtime.Caller(2) 191 | if ok { 192 | sourceLocation = &LogEntrySourceLocation{ 193 | File: file, 194 | Line: strconv.Itoa(line), 195 | Function: runtime.FuncForPC(pc).Name(), 196 | } 197 | } 198 | 199 | e := &LogEntry{ 200 | Message: fmt.Sprint(v...), 201 | Severity: severity, 202 | SourceLocation: sourceLocation, 203 | Trace: traceID, 204 | } 205 | 206 | l.write(e) 207 | } 208 | 209 | func (l *Logger) write(e *LogEntry) { 210 | l.mu.Lock() 211 | defer l.mu.Unlock() 212 | 213 | s := e.String() 214 | l.buf = l.buf[:0] 215 | l.buf = append(l.buf, s...) 216 | if len(s) == 0 || s[len(s)-1] != '\n' { 217 | l.buf = append(l.buf, '\n') 218 | } 219 | 220 | l.out.Write(l.buf) 221 | } 222 | 223 | func extractTraceID(v interface{}) string { 224 | var trace string 225 | 226 | switch t := v.(type) { 227 | case *http.Request: 228 | traceHeader := t.Header.Get("X-Cloud-Trace-Context") 229 | ts := strings.Split(traceHeader, "/") 230 | if len(ts) > 0 && len(ts[0]) > 0 { 231 | trace = ts[0] 232 | } 233 | default: 234 | trace = "" 235 | } 236 | 237 | return trace 238 | } 239 | -------------------------------------------------------------------------------- /integration/vendor/github.com/kelseyhightower/run/metadata.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "path" 10 | "strings" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | var rmu sync.Mutex 16 | 17 | var ( 18 | runtimeID string 19 | runtimeProjectID string 20 | runtimeRegion string 21 | runtimeNumericProjectID string 22 | ) 23 | 24 | var metadataEndpoint = "http://metadata.google.internal" 25 | 26 | // ErrMetadataNotFound is returned when a metadata key is not found. 27 | var ErrMetadataNotFound = errors.New("run: metadata key not found") 28 | 29 | // ErrMetadataInvalidRequest is returned when a metadata request is invalid. 30 | var ErrMetadataInvalidRequest = errors.New("run: invalid metadata request") 31 | 32 | // ErrMetadataUnknownError is return when calls to the metadata server 33 | // return an unknown error. 34 | var ErrMetadataUnknownError = errors.New("run: unexpected error retrieving metadata key") 35 | 36 | // ErrMetadataUnexpectedResponse is returned when calls to the metadata server 37 | // return an unexpected response. 38 | type ErrMetadataUnexpectedResponse struct { 39 | StatusCode int 40 | Err error 41 | } 42 | 43 | func (e *ErrMetadataUnexpectedResponse) Error() string { 44 | return "run: unexpected error retrieving metadata key" 45 | } 46 | 47 | func (e *ErrMetadataUnexpectedResponse) Unwrap() error { return e.Err } 48 | 49 | // AccessToken holds a GCP access token. 50 | type AccessToken struct { 51 | AccessToken string `json:"access_token"` 52 | ExpiresIn int64 `json:"expires_in"` 53 | TokenType string `json:"token_type"` 54 | } 55 | 56 | // ProjectID returns the active project ID from the metadata service. 57 | func ProjectID() (string, error) { 58 | rmu.Lock() 59 | defer rmu.Unlock() 60 | 61 | if runtimeProjectID != "" { 62 | return runtimeProjectID, nil 63 | } 64 | 65 | endpoint := fmt.Sprintf("%s/computeMetadata/v1/project/project-id", metadataEndpoint) 66 | 67 | data, err := metadataRequest(endpoint) 68 | if err != nil { 69 | return "", err 70 | } 71 | 72 | runtimeProjectID = string(data) 73 | return runtimeProjectID, nil 74 | } 75 | 76 | // NumericProjectID returns the active project ID from the metadata service. 77 | func NumericProjectID() (string, error) { 78 | rmu.Lock() 79 | defer rmu.Unlock() 80 | 81 | if runtimeNumericProjectID != "" { 82 | return runtimeNumericProjectID, nil 83 | } 84 | 85 | endpoint := fmt.Sprintf("%s/computeMetadata/v1/project/numeric-project-id", metadataEndpoint) 86 | 87 | data, err := metadataRequest(endpoint) 88 | if err != nil { 89 | return "", err 90 | } 91 | 92 | runtimeNumericProjectID = string(data) 93 | return runtimeNumericProjectID, nil 94 | } 95 | 96 | // Token returns the default service account token. 97 | func Token(scopes []string) (*AccessToken, error) { 98 | s := strings.Join(scopes, ",") 99 | 100 | endpoint := fmt.Sprintf("%s/computeMetadata/v1/instance/service-accounts/default/token?scopes=%s", metadataEndpoint, s) 101 | data, err := metadataRequest(endpoint) 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | var accessToken AccessToken 107 | err = json.Unmarshal(data, &accessToken) 108 | if err != nil { 109 | return nil, fmt.Errorf("run/metadata: error retrieving access token: %v", err) 110 | } 111 | 112 | return &accessToken, nil 113 | } 114 | 115 | // IDToken returns an id token based on the service url. 116 | func IDToken(serviceURL string) (string, error) { 117 | endpoint := fmt.Sprintf("%s/computeMetadata/v1/instance/service-accounts/default/identity?audience=%s", metadataEndpoint, serviceURL) 118 | 119 | idToken, err := metadataRequest(endpoint) 120 | if err != nil { 121 | return "", fmt.Errorf("metadata.Get: failed to query id_token: %w", err) 122 | } 123 | return string(idToken), nil 124 | } 125 | 126 | // Region returns the name of the Cloud Run region. 127 | func Region() (string, error) { 128 | rmu.Lock() 129 | defer rmu.Unlock() 130 | 131 | if runtimeRegion != "" { 132 | return runtimeRegion, nil 133 | } 134 | 135 | endpoint := fmt.Sprintf("%s/computeMetadata/v1/instance/region", metadataEndpoint) 136 | 137 | data, err := metadataRequest(endpoint) 138 | if err != nil { 139 | return "", err 140 | } 141 | 142 | runtimeRegion = path.Base(string(data)) 143 | return runtimeRegion, nil 144 | } 145 | 146 | // ID returns the unique identifier of the container instance. 147 | func ID() (string, error) { 148 | rmu.Lock() 149 | defer rmu.Unlock() 150 | 151 | if runtimeID != "" { 152 | return runtimeID, nil 153 | } 154 | 155 | endpoint := fmt.Sprintf("%s/computeMetadata/v1/instance/id", metadataEndpoint) 156 | 157 | data, err := metadataRequest(endpoint) 158 | if err != nil { 159 | return "", err 160 | } 161 | 162 | runtimeID = string(data) 163 | return runtimeID, nil 164 | } 165 | 166 | func metadataRequest(endpoint string) ([]byte, error) { 167 | request, err := http.NewRequest("GET", endpoint, nil) 168 | if err != nil { 169 | return nil, err 170 | } 171 | 172 | request.Header.Set("User-Agent", userAgent) 173 | request.Header.Add("Metadata-Flavor", "Google") 174 | 175 | timeout := time.Duration(5) * time.Second 176 | httpClient := http.Client{Timeout: timeout} 177 | 178 | response, err := httpClient.Do(request) 179 | if err != nil { 180 | return nil, err 181 | } 182 | defer response.Body.Close() 183 | 184 | switch s := response.StatusCode; s { 185 | case 200: 186 | break 187 | case 400: 188 | return nil, ErrMetadataInvalidRequest 189 | case 404: 190 | return nil, ErrMetadataNotFound 191 | default: 192 | return nil, &ErrMetadataUnexpectedResponse{s, ErrMetadataUnknownError} 193 | } 194 | 195 | data, err := ioutil.ReadAll(response.Body) 196 | if err != nil { 197 | return nil, err 198 | } 199 | 200 | return data, nil 201 | } 202 | -------------------------------------------------------------------------------- /integration/vendor/github.com/kelseyhightower/run/secrets.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "time" 11 | ) 12 | 13 | var ( 14 | secretmanagerEndpoint = "https://secretmanager.googleapis.com/v1" 15 | ) 16 | 17 | // ErrSecretPermissionDenied is returned when access to a secret is denied. 18 | var ErrSecretPermissionDenied = errors.New("run: permission denied to named secret") 19 | 20 | // ErrSecretUnauthorized is returned when calls to the Secret 21 | // Manager API are unauthorized. 22 | var ErrSecretUnauthorized = errors.New("run: secret manager unauthorized") 23 | 24 | // ErrSecretNotFound is returned when a secret is not found. 25 | var ErrSecretNotFound = errors.New("run: named secret not found") 26 | 27 | // ErrSecretUnknownError is return when calls to the Secret Manager 28 | // API return an unknown error. 29 | var ErrSecretUnknownError = errors.New("run: unexpected error retrieving named secret") 30 | 31 | // ErrSecretUnexpectedResponse is returned when calls to the Secret Manager 32 | // API return an unexpected response. 33 | type ErrSecretUnexpectedResponse struct { 34 | StatusCode int 35 | Err error 36 | } 37 | 38 | func (e *ErrSecretUnexpectedResponse) Error() string { 39 | return "run: unexpected error retrieving named secret" 40 | } 41 | 42 | func (e *ErrSecretUnexpectedResponse) Unwrap() error { return e.Err } 43 | 44 | // SecretVersion represents a Google Cloud Secret. 45 | type SecretVersion struct { 46 | Name string 47 | Payload SecretPayload `json:"payload"` 48 | } 49 | 50 | // SecretPayload holds the secret payload for a Google Cloud Secret. 51 | type SecretPayload struct { 52 | // A base64-encoded string. 53 | Data string `json:"data"` 54 | } 55 | 56 | func formatSecretVersion(project, name, version string) string { 57 | return fmt.Sprintf("projects/%s/secrets/%s/versions/%s", project, name, version) 58 | } 59 | 60 | // AccessSecretVersion returns a Google Cloud Secret for the given 61 | // secret name and version. 62 | func AccessSecretVersion(name, version string) ([]byte, error) { 63 | return accessSecretVersion(name, version) 64 | } 65 | 66 | // AccessSecret returns the latest version of a Google Cloud Secret 67 | // for the given name. 68 | func AccessSecret(name string) ([]byte, error) { 69 | return accessSecretVersion(name, "latest") 70 | } 71 | 72 | func accessSecretVersion(name, version string) ([]byte, error) { 73 | if version == "" { 74 | version = "latest" 75 | } 76 | 77 | token, err := Token([]string{"https://www.googleapis.com/auth/cloud-platform"}) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | numericProjectID, err := NumericProjectID() 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | secretVersion := formatSecretVersion(numericProjectID, name, version) 88 | endpoint := fmt.Sprintf("%s/%s:access", secretmanagerEndpoint, secretVersion) 89 | 90 | request, err := http.NewRequest("GET", endpoint, nil) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | request.Header.Set("User-Agent", userAgent) 96 | request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) 97 | 98 | timeout := time.Duration(5) * time.Second 99 | httpClient := http.Client{Timeout: timeout} 100 | 101 | response, err := httpClient.Do(request) 102 | if err != nil { 103 | return nil, err 104 | } 105 | defer response.Body.Close() 106 | 107 | switch s := response.StatusCode; s { 108 | case 200: 109 | break 110 | case 401: 111 | return nil, ErrSecretUnauthorized 112 | case 403: 113 | return nil, ErrSecretPermissionDenied 114 | case 404: 115 | return nil, ErrSecretNotFound 116 | default: 117 | return nil, &ErrSecretUnexpectedResponse{s, ErrSecretUnknownError} 118 | } 119 | 120 | data, err := ioutil.ReadAll(response.Body) 121 | if err != nil { 122 | return nil, err 123 | } 124 | 125 | var s SecretVersion 126 | err = json.Unmarshal(data, &s) 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | decodedData, err := base64.StdEncoding.DecodeString(s.Payload.Data) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | return decodedData, nil 137 | } 138 | -------------------------------------------------------------------------------- /integration/vendor/github.com/kelseyhightower/run/useragent.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | const ( 4 | userAgent = "golang-run/0.0.8" 5 | ) 6 | -------------------------------------------------------------------------------- /integration/vendor/modules.txt: -------------------------------------------------------------------------------- 1 | # github.com/kelseyhightower/run v0.0.0-00010101000000-000000000000 => ../ 2 | ## explicit 3 | github.com/kelseyhightower/run 4 | # github.com/kelseyhightower/run => ../ 5 | -------------------------------------------------------------------------------- /internal/gcptest/cloudrun.go: -------------------------------------------------------------------------------- 1 | package gcptest 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // Service represents a Cloud Run service. 10 | type Service struct { 11 | Status ServiceStatus `json:"status"` 12 | } 13 | 14 | // ServiceStatus holds the current state of the Cloud Run service. 15 | type ServiceStatus struct { 16 | // URL holds the url that will distribute traffic over the 17 | // provided traffic targets. It generally has the form 18 | // https://{route-hash}-{project-hash}-{cluster-level-suffix}.a.run.app 19 | URL string `json:"url"` 20 | 21 | // Similar to url, information on where the service is available on HTTP. 22 | Address ServiceAddresss `json:"address"` 23 | } 24 | 25 | type ServiceAddresss struct { 26 | URL string `json:"url"` 27 | } 28 | 29 | type cloudrunHandler struct { 30 | services map[string]string 31 | } 32 | 33 | func CloudrunServer(services map[string]string) http.Handler { 34 | return &cloudrunHandler{services} 35 | } 36 | 37 | func (h *cloudrunHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 38 | u := h.services["test"] 39 | 40 | s := Service{ 41 | Status: ServiceStatus{ 42 | URL: u, 43 | Address: ServiceAddresss{ 44 | URL: u, 45 | }, 46 | }, 47 | } 48 | 49 | path := r.URL.Path 50 | 51 | if path == "/apis/serving.knative.dev/v1/namespaces/test/services/test" { 52 | data, err := json.Marshal(s) 53 | if err != nil { 54 | http.Error(w, "", 500) 55 | return 56 | } 57 | 58 | fmt.Fprint(w, string(data)) 59 | return 60 | } 61 | 62 | if path == "/apis/serving.knative.dev/v1/namespaces/test/services/not-found" { 63 | http.NotFound(w, r) 64 | return 65 | } 66 | 67 | http.Error(w, "", 500) 68 | } 69 | -------------------------------------------------------------------------------- /internal/gcptest/metadata.go: -------------------------------------------------------------------------------- 1 | package gcptest 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | const ( 10 | ID = "00bf4bf02db3546595153c211fec26b688516bf1b609f7d5e2f5d9ae17d1bcbaf6ce6c0c1b1168abf1ab255125e84e085336a36ae5715b0f95e7" 11 | NumericProjectID = "123456789" 12 | ProjectID = "test" 13 | Region = "test" 14 | ) 15 | 16 | var AccessToken = AccessTokenResponse{ 17 | AccessToken: "ya29.AHES6ZRVmB7fkLtd1XTmq6mo0S1wqZZi3-Lh_s-6Uw7p8vtgSwg", 18 | ExpiresIn: 3484, 19 | TokenType: "Bearer", 20 | } 21 | 22 | // AccessTokenResponse holds a GCP access token. 23 | type AccessTokenResponse struct { 24 | AccessToken string `json:"access_token"` 25 | ExpiresIn int64 `json:"expires_in"` 26 | TokenType string `json:"token_type"` 27 | } 28 | 29 | const ( 30 | IDToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.Et9HFtf9R3GEMA0IICOfFMVXY7kkTX1wr4qCyhIf58U" 31 | ) 32 | 33 | func BrokenMetadataHandler(w http.ResponseWriter, r *http.Request) { 34 | path := r.URL.Path 35 | 36 | if path == "/computeMetadata/v1/instance/service-accounts/default/token" { 37 | w.Write([]byte("{\"broken\"")) 38 | return 39 | } 40 | 41 | http.Error(w, "", 500) 42 | return 43 | } 44 | 45 | func MetadataHandler(w http.ResponseWriter, r *http.Request) { 46 | path := r.URL.Path 47 | 48 | if path == "/computeMetadata/v1/instance/id" { 49 | fmt.Fprint(w, ID) 50 | return 51 | } 52 | 53 | if path == "/computeMetadata/v1/notfound" { 54 | http.NotFound(w, r) 55 | return 56 | } 57 | 58 | if path == "/computeMetadata/v1/invalid" { 59 | http.Error(w, "", 400) 60 | return 61 | } 62 | 63 | if path == "/computeMetadata/v1/unknown" { 64 | http.Error(w, "", 500) 65 | return 66 | } 67 | 68 | if path == "/computeMetadata/v1/project/project-id" { 69 | fmt.Fprint(w, ProjectID) 70 | return 71 | } 72 | 73 | if path == "/computeMetadata/v1/project/numeric-project-id" { 74 | fmt.Fprint(w, NumericProjectID) 75 | return 76 | } 77 | 78 | if path == "/computeMetadata/v1/instance/zone" { 79 | fmt.Fprint(w, fmt.Sprintf("projects/%s/zones/%s-1", NumericProjectID, Region)) 80 | return 81 | } 82 | 83 | if path == "/computeMetadata/v1/instance/region" { 84 | fmt.Fprint(w, Region) 85 | return 86 | } 87 | 88 | if path == "/computeMetadata/v1/instance/service-accounts/default/token" { 89 | data, err := json.Marshal(AccessToken) 90 | if err != nil { 91 | http.Error(w, err.Error(), 500) 92 | return 93 | } 94 | 95 | w.Write(data) 96 | return 97 | } 98 | 99 | if path == "/computeMetadata/v1/instance/service-accounts/default/identity" { 100 | fmt.Fprint(w, IDToken) 101 | return 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /internal/gcptest/secrets.go: -------------------------------------------------------------------------------- 1 | package gcptest 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | const fooSecret = `{ 9 | "name": "projects/123456789/secrets/foo/versions/1", 10 | "payload": { 11 | "data": "VGVzdA==" 12 | } 13 | }` 14 | 15 | func SecretsHandler(w http.ResponseWriter, r *http.Request) { 16 | path := r.URL.Path 17 | 18 | if path == "/projects/123456789/secrets/unexpected/versions/latest:access" { 19 | http.Error(w, "", 500) 20 | } 21 | 22 | if path == "/projects/123456789/secrets/unauthorized/versions/latest:access" { 23 | http.Error(w, "", 401) 24 | } 25 | 26 | if path == "/projects/123456789/secrets/denied/versions/latest:access" { 27 | http.Error(w, "", 403) 28 | } 29 | 30 | if path == "/projects/123456789/secrets/bar/versions/latest:access" { 31 | http.NotFound(w, r) 32 | } 33 | 34 | if path == "/projects/123456789/secrets/foo/versions/1:access" { 35 | fmt.Fprint(w, fooSecret) 36 | return 37 | } 38 | 39 | if path == "/projects/123456789/secrets/foo/versions/latest:access" { 40 | fmt.Fprint(w, fooSecret) 41 | return 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /internal/gcptest/servicedirectory.go: -------------------------------------------------------------------------------- 1 | package gcptest 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | var testEndpoints = `{ 9 | "endpoints": [ 10 | { 11 | "name": "test-10-0-0-1", 12 | "address": "10.0.0.1", 13 | "port": 8080 14 | }, 15 | { 16 | "name": "test-10-0-0-2", 17 | "address": "10.0.0.2", 18 | "port": 8080 19 | } 20 | ] 21 | }` 22 | 23 | func ServiceDirectoryHandler(w http.ResponseWriter, r *http.Request) { 24 | path := r.URL.Path 25 | 26 | if path == "/v1/projects/test/locations/test/namespaces/test/services/test/endpoints" { 27 | r.Header.Set("Content-Type", "application/json") 28 | fmt.Fprintf(w, testEndpoints) 29 | return 30 | } 31 | if path == "/v1/projects/test/locations/test/namespaces/default/services/test/endpoints" { 32 | r.Header.Set("Content-Type", "application/json") 33 | fmt.Fprintf(w, testEndpoints) 34 | return 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "os" 9 | "runtime" 10 | "strconv" 11 | "strings" 12 | "sync" 13 | ) 14 | 15 | var dl = &Logger{out: os.Stdout} 16 | 17 | // An LogEntry represents a Stackdriver log entry. 18 | type LogEntry struct { 19 | Message string `json:"message"` 20 | Severity string `json:"severity,omitempty"` 21 | Component string `json:"component,omitempty"` 22 | SourceLocation *LogEntrySourceLocation `json:"logging.googleapis.com/sourceLocation,omitempty"` 23 | Trace string `json:"logging.googleapis.com/trace,omitempty"` 24 | } 25 | 26 | // A LogEntrySourceLocation holds source code location data. 27 | // 28 | // Location data is used to provide additional context when logging 29 | // to Stackdriver. 30 | type LogEntrySourceLocation struct { 31 | File string `json:"file,omitempty"` 32 | Function string `json:"function,omitempty"` 33 | Line string `json:"line,omitempty"` 34 | } 35 | 36 | // String returns a JSON formatted string expected by Stackdriver. 37 | func (le LogEntry) String() string { 38 | if le.Severity == "" { 39 | le.Severity = "INFO" 40 | } 41 | data, err := json.Marshal(le) 42 | if err != nil { 43 | fmt.Printf("json.Marshal: %v", err) 44 | } 45 | return string(data) 46 | } 47 | 48 | // A Logger represents an active logging object that generates JSON formatted 49 | // log entries to standard out. Logs are formatted as expected by Cloud Run's 50 | // Stackdriver integration. 51 | type Logger struct { 52 | mu sync.Mutex 53 | buf []byte 54 | out io.Writer 55 | } 56 | 57 | // Error calls Log on the default logger with severity set to ERROR. 58 | // 59 | // Arguments are handled in the manner of fmt.Print. 60 | func Error(v ...interface{}) { 61 | dl.Log("ERROR", v...) 62 | } 63 | 64 | // Fatal calls Log on the default logger with severity set to ERROR 65 | // followed by a call to os.Exit(1). 66 | // 67 | // Arguments are handled in the manner of fmt.Print. 68 | func Fatal(v ...interface{}) { 69 | dl.Log("ERROR", v...) 70 | os.Exit(1) 71 | } 72 | 73 | // Log writes logging events with the given severity. 74 | // 75 | // The string s contains the text to log. 76 | // 77 | // Source file location data will be included in log entires. 78 | // 79 | // Logs are written to stdout in the Stackdriver structured log 80 | // format. See https://cloud.google.com/logging/docs/structured-logging 81 | // for more details. 82 | func Log(severity, s string) { 83 | dl.Log(severity, s) 84 | } 85 | 86 | // SetOutput sets the output destination for the default logger. 87 | func SetOutput(w io.Writer) { 88 | dl.mu.Lock() 89 | defer dl.mu.Unlock() 90 | dl.out = w 91 | } 92 | 93 | // Info calls Log on the default logger with severity set to INFO. 94 | // 95 | // Arguments are handled in the manner of fmt.Print. 96 | func Info(v ...interface{}) { 97 | dl.Log("INFO", v...) 98 | } 99 | 100 | // Notice calls Log on the default logger with severity set to NOTICE. 101 | // 102 | // Arguments are handled in the manner of fmt.Print. 103 | func Notice(v ...interface{}) { 104 | dl.Log("NOTICE", v...) 105 | } 106 | 107 | // NewLogger creates a new Logger. 108 | func NewLogger() *Logger { 109 | return &Logger{out: os.Stdout} 110 | } 111 | 112 | // SetOutput sets the output destination for the logger. 113 | func (l *Logger) SetOutput(w io.Writer) { 114 | l.mu.Lock() 115 | defer l.mu.Unlock() 116 | l.out = w 117 | } 118 | 119 | // Info calls l.Log with severity set to INFO. 120 | // 121 | // Arguments are handled in the manner of fmt.Print. 122 | func (l *Logger) Info(v ...interface{}) { 123 | l.Log("INFO", v...) 124 | } 125 | 126 | // Error calls l.Log with severity set to ERROR. 127 | // 128 | // Arguments are handled in the manner of fmt.Print. 129 | func (l *Logger) Error(v ...interface{}) { 130 | l.Log("ERROR", v...) 131 | } 132 | 133 | // Fatal calls l.Log with severity set to ERROR followed by 134 | // a call to os.Exit(1). 135 | // 136 | // Arguments are handled in the manner of fmt.Print. 137 | func (l *Logger) Fatal(v ...interface{}) { 138 | l.Log("ERROR", v...) 139 | os.Exit(1) 140 | } 141 | 142 | // Notice calls l.Log with severity set to NOTICE. 143 | // 144 | // Arguments are handled in the manner of fmt.Print. 145 | func (l *Logger) Notice(v ...interface{}) { 146 | l.Log("NOTICE", v...) 147 | } 148 | 149 | // Log writes logging events with the given severity. 150 | // 151 | // If the first value is an *http.Request, the X-Cloud-Trace-Context 152 | // HTTP header will be extracted and included in the Stackdriver log 153 | // entry. 154 | // 155 | // Source file location data will be included in log entires. 156 | // 157 | // Logs are written to stdout in the Stackdriver structured log 158 | // format. See https://cloud.google.com/logging/docs/structured-logging 159 | // for more details. 160 | func (l *Logger) Log(severity string, v ...interface{}) { 161 | var traceID string 162 | 163 | tid := extractTraceID(v[0]) 164 | if tid != "" { 165 | pid, err := ProjectID() 166 | if err != nil { 167 | e := &LogEntry{ 168 | Message: fmt.Sprintf("unable to append trace to log, missing project id: %v", err.Error()), 169 | Severity: "ERROR", 170 | } 171 | l.write(e) 172 | } 173 | 174 | if pid == "" { 175 | e := &LogEntry{ 176 | Message: fmt.Sprint("unable to append trace to log, project id is empty"), 177 | Severity: "ERROR", 178 | } 179 | l.write(e) 180 | } else { 181 | traceID = fmt.Sprintf("projects/%s/traces/%s", pid, tid) 182 | } 183 | } 184 | 185 | // The first argument was an *http.Request or context object 186 | // and is not part of the message 187 | if _, ok := v[0].(*http.Request); ok { 188 | v = v[1:] 189 | } 190 | 191 | var sourceLocation *LogEntrySourceLocation 192 | pc, file, line, ok := runtime.Caller(2) 193 | if ok { 194 | sourceLocation = &LogEntrySourceLocation{ 195 | File: file, 196 | Line: strconv.Itoa(line), 197 | Function: runtime.FuncForPC(pc).Name(), 198 | } 199 | } 200 | 201 | e := &LogEntry{ 202 | Message: fmt.Sprint(v...), 203 | Severity: severity, 204 | SourceLocation: sourceLocation, 205 | Trace: traceID, 206 | } 207 | 208 | l.write(e) 209 | } 210 | 211 | func (l *Logger) write(e *LogEntry) { 212 | l.mu.Lock() 213 | defer l.mu.Unlock() 214 | 215 | s := e.String() 216 | l.buf = l.buf[:0] 217 | l.buf = append(l.buf, s...) 218 | if len(s) == 0 || s[len(s)-1] != '\n' { 219 | l.buf = append(l.buf, '\n') 220 | } 221 | 222 | l.out.Write(l.buf) 223 | } 224 | 225 | func extractTraceID(v interface{}) string { 226 | var trace string 227 | 228 | switch t := v.(type) { 229 | case *http.Request: 230 | traceHeader := t.Header.Get("X-Cloud-Trace-Context") 231 | ts := strings.Split(traceHeader, "/") 232 | if len(ts) > 0 && len(ts[0]) > 0 { 233 | trace = ts[0] 234 | } 235 | default: 236 | trace = "" 237 | } 238 | 239 | return trace 240 | } 241 | -------------------------------------------------------------------------------- /log_test.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | 11 | "github.com/kelseyhightower/run/internal/gcptest" 12 | ) 13 | 14 | var loggerTests = []struct { 15 | message string 16 | severity string 17 | }{ 18 | {"info", "INFO"}, 19 | {"error", "ERROR"}, 20 | {"notice", "NOTICE"}, 21 | } 22 | 23 | func TestLogger(t *testing.T) { 24 | logger := NewLogger() 25 | 26 | for _, tt := range loggerTests { 27 | buf := new(bytes.Buffer) 28 | logger.SetOutput(buf) 29 | 30 | switch tt.severity { 31 | case "INFO": 32 | logger.Info(tt.message) 33 | case "ERROR": 34 | logger.Error(tt.message) 35 | case "NOTICE": 36 | logger.Notice(tt.message) 37 | } 38 | 39 | var le LogEntry 40 | if err := json.Unmarshal(buf.Bytes(), &le); err != nil { 41 | t.Error(err) 42 | } 43 | 44 | if le.Message != tt.message { 45 | t.Errorf("log message mismatch, want %s, got %s", tt.message, le.Message) 46 | } 47 | 48 | if le.Severity != tt.severity { 49 | t.Errorf("log severity mismatch, want %s, got %s", tt.severity, le.Severity) 50 | } 51 | } 52 | } 53 | 54 | func TestDefaultLogger(t *testing.T) { 55 | for _, tt := range loggerTests { 56 | buf := new(bytes.Buffer) 57 | SetOutput(buf) 58 | 59 | switch tt.severity { 60 | case "INFO": 61 | Info(tt.message) 62 | case "ERROR": 63 | Error(tt.message) 64 | case "NOTICE": 65 | Notice(tt.message) 66 | } 67 | 68 | var le LogEntry 69 | if err := json.Unmarshal(buf.Bytes(), &le); err != nil { 70 | t.Error(err) 71 | } 72 | 73 | if le.Message != tt.message { 74 | t.Errorf("log message mismatch, want %s, got %s", tt.message, le.Message) 75 | } 76 | 77 | if le.Severity != tt.severity { 78 | t.Errorf("log severity mismatch, want %s, got %s", tt.severity, le.Severity) 79 | } 80 | } 81 | } 82 | 83 | func TestLoggerWithTraceID(t *testing.T) { 84 | var ( 85 | traceID = "27abb75176a19ccf353146b192ef419f" 86 | formatedTraceID = fmt.Sprintf("projects/%s/traces/%s", gcptest.ProjectID, traceID) 87 | message = "message" 88 | severity = "INFO" 89 | ) 90 | 91 | ts := httptest.NewServer(http.HandlerFunc(gcptest.MetadataHandler)) 92 | defer ts.Close() 93 | 94 | metadataEndpoint = ts.URL 95 | 96 | buf := new(bytes.Buffer) 97 | SetOutput(buf) 98 | 99 | r, err := http.NewRequest("GET", "", nil) 100 | if err != nil { 101 | t.Error(err) 102 | } 103 | 104 | r.Header.Set("X-Cloud-Trace-Context", traceID) 105 | 106 | Info(r, message) 107 | 108 | var le LogEntry 109 | if err := json.Unmarshal(buf.Bytes(), &le); err != nil { 110 | t.Error(err) 111 | } 112 | 113 | if le.Trace != formatedTraceID { 114 | t.Errorf("log traceID mismatch, want %s, got %s", formatedTraceID, le.Trace) 115 | } 116 | 117 | if le.Message != message { 118 | t.Errorf("log message mismatch, want %s, got %s", message, le.Message) 119 | } 120 | 121 | if le.Severity != severity { 122 | t.Errorf("log severity mismatch, want %s, got %s", severity, le.Severity) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /metadata.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "path" 10 | "strings" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | var rmu sync.Mutex 16 | 17 | var ( 18 | runtimeID string 19 | runtimeProjectID string 20 | runtimeRegion string 21 | runtimeNumericProjectID string 22 | ) 23 | 24 | var metadataEndpoint = "http://metadata.google.internal" 25 | 26 | // ErrMetadataNotFound is returned when a metadata key is not found. 27 | var ErrMetadataNotFound = errors.New("run: metadata key not found") 28 | 29 | // ErrMetadataInvalidRequest is returned when a metadata request is invalid. 30 | var ErrMetadataInvalidRequest = errors.New("run: invalid metadata request") 31 | 32 | // ErrMetadataUnknownError is return when calls to the metadata server 33 | // return an unknown error. 34 | var ErrMetadataUnknownError = errors.New("run: unexpected error retrieving metadata key") 35 | 36 | // ErrMetadataUnexpectedResponse is returned when calls to the metadata server 37 | // return an unexpected response. 38 | type ErrMetadataUnexpectedResponse struct { 39 | StatusCode int 40 | Err error 41 | } 42 | 43 | func (e *ErrMetadataUnexpectedResponse) Error() string { 44 | return "run: unexpected error retrieving metadata key" 45 | } 46 | 47 | func (e *ErrMetadataUnexpectedResponse) Unwrap() error { return e.Err } 48 | 49 | // AccessToken holds a GCP access token. 50 | type AccessToken struct { 51 | AccessToken string `json:"access_token"` 52 | ExpiresIn int64 `json:"expires_in"` 53 | TokenType string `json:"token_type"` 54 | } 55 | 56 | // ProjectID returns the active project ID from the metadata service. 57 | func ProjectID() (string, error) { 58 | rmu.Lock() 59 | defer rmu.Unlock() 60 | 61 | if runtimeProjectID != "" { 62 | return runtimeProjectID, nil 63 | } 64 | 65 | endpoint := fmt.Sprintf("%s/computeMetadata/v1/project/project-id", metadataEndpoint) 66 | 67 | data, err := metadataRequest(endpoint) 68 | if err != nil { 69 | return "", err 70 | } 71 | 72 | runtimeProjectID = string(data) 73 | return runtimeProjectID, nil 74 | } 75 | 76 | // NumericProjectID returns the active project ID from the metadata service. 77 | func NumericProjectID() (string, error) { 78 | rmu.Lock() 79 | defer rmu.Unlock() 80 | 81 | if runtimeNumericProjectID != "" { 82 | return runtimeNumericProjectID, nil 83 | } 84 | 85 | endpoint := fmt.Sprintf("%s/computeMetadata/v1/project/numeric-project-id", metadataEndpoint) 86 | 87 | data, err := metadataRequest(endpoint) 88 | if err != nil { 89 | return "", err 90 | } 91 | 92 | runtimeNumericProjectID = string(data) 93 | return runtimeNumericProjectID, nil 94 | } 95 | 96 | // Token returns the default service account token. 97 | func Token(scopes []string) (*AccessToken, error) { 98 | s := strings.Join(scopes, ",") 99 | 100 | endpoint := fmt.Sprintf("%s/computeMetadata/v1/instance/service-accounts/default/token?scopes=%s", metadataEndpoint, s) 101 | data, err := metadataRequest(endpoint) 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | var accessToken AccessToken 107 | err = json.Unmarshal(data, &accessToken) 108 | if err != nil { 109 | return nil, fmt.Errorf("run/metadata: error retrieving access token: %v", err) 110 | } 111 | 112 | return &accessToken, nil 113 | } 114 | 115 | // IDToken returns an id token based on the service url. 116 | func IDToken(serviceURL string) (string, error) { 117 | endpoint := fmt.Sprintf("%s/computeMetadata/v1/instance/service-accounts/default/identity?audience=%s", metadataEndpoint, serviceURL) 118 | 119 | idToken, err := metadataRequest(endpoint) 120 | if err != nil { 121 | return "", fmt.Errorf("metadata.Get: failed to query id_token: %w", err) 122 | } 123 | return string(idToken), nil 124 | } 125 | 126 | // Region returns the name of the Cloud Run region. 127 | func Region() (string, error) { 128 | rmu.Lock() 129 | defer rmu.Unlock() 130 | 131 | if runtimeRegion != "" { 132 | return runtimeRegion, nil 133 | } 134 | 135 | endpoint := fmt.Sprintf("%s/computeMetadata/v1/instance/region", metadataEndpoint) 136 | 137 | data, err := metadataRequest(endpoint) 138 | if err != nil { 139 | return "", err 140 | } 141 | 142 | runtimeRegion = path.Base(string(data)) 143 | return runtimeRegion, nil 144 | } 145 | 146 | // ID returns the unique identifier of the container instance. 147 | func ID() (string, error) { 148 | rmu.Lock() 149 | defer rmu.Unlock() 150 | 151 | if runtimeID != "" { 152 | return runtimeID, nil 153 | } 154 | 155 | endpoint := fmt.Sprintf("%s/computeMetadata/v1/instance/id", metadataEndpoint) 156 | 157 | data, err := metadataRequest(endpoint) 158 | if err != nil { 159 | return "", err 160 | } 161 | 162 | runtimeID = string(data) 163 | return runtimeID, nil 164 | } 165 | 166 | func metadataRequest(endpoint string) ([]byte, error) { 167 | request, err := http.NewRequest("GET", endpoint, nil) 168 | if err != nil { 169 | return nil, err 170 | } 171 | 172 | request.Header.Set("User-Agent", userAgent) 173 | request.Header.Add("Metadata-Flavor", "Google") 174 | 175 | timeout := time.Duration(5) * time.Second 176 | httpClient := http.Client{Timeout: timeout} 177 | 178 | response, err := httpClient.Do(request) 179 | if err != nil { 180 | return nil, err 181 | } 182 | defer response.Body.Close() 183 | 184 | switch s := response.StatusCode; s { 185 | case 200: 186 | break 187 | case 400: 188 | return nil, ErrMetadataInvalidRequest 189 | case 404: 190 | return nil, ErrMetadataNotFound 191 | default: 192 | return nil, &ErrMetadataUnexpectedResponse{s, ErrMetadataUnknownError} 193 | } 194 | 195 | data, err := ioutil.ReadAll(response.Body) 196 | if err != nil { 197 | return nil, err 198 | } 199 | 200 | return data, nil 201 | } 202 | -------------------------------------------------------------------------------- /metadata_test.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/kelseyhightower/run/internal/gcptest" 11 | ) 12 | 13 | var metadataTests = []struct { 14 | name string 15 | want string 16 | err error 17 | }{ 18 | {"id", gcptest.ID, nil}, 19 | {"idtoken", gcptest.IDToken, nil}, 20 | {"projectid", gcptest.ProjectID, nil}, 21 | {"numericprojectid", gcptest.NumericProjectID, nil}, 22 | {"region", gcptest.Region, nil}, 23 | {"notfound", "", ErrMetadataNotFound}, 24 | {"invalid", "", ErrMetadataInvalidRequest}, 25 | {"unknown", "", ErrMetadataUnknownError}, 26 | } 27 | 28 | func TestMetadata(t *testing.T) { 29 | ts := httptest.NewServer(http.HandlerFunc(gcptest.MetadataHandler)) 30 | defer ts.Close() 31 | 32 | metadataEndpoint = ts.URL 33 | 34 | v, err := Token([]string{"https://www.googleapis.com/auth/cloud-platform"}) 35 | if !errors.Is(err, nil) { 36 | t.Error(err) 37 | } 38 | 39 | if v.AccessToken != gcptest.AccessToken.AccessToken { 40 | t.Errorf("got id %v, want %v", v, gcptest.AccessToken) 41 | } 42 | 43 | for _, tt := range metadataTests { 44 | var ( 45 | err error 46 | v string 47 | ) 48 | 49 | switch tt.name { 50 | case "id": 51 | v, err = ID() 52 | case "idtoken": 53 | v, err = IDToken("https://test-0123456789-ue.a.run.app") 54 | case "projectid": 55 | v, err = ProjectID() 56 | case "numericprojectid": 57 | v, err = NumericProjectID() 58 | case "region": 59 | v, err = Region() 60 | default: 61 | v, err = errorMetadataRequest(tt.name) 62 | } 63 | 64 | if !errors.Is(err, tt.err) { 65 | t.Error(err) 66 | } 67 | 68 | if v != tt.want { 69 | t.Errorf("got id %v, want %v", v, tt.want) 70 | } 71 | } 72 | } 73 | 74 | var metadataErrorTests = []struct { 75 | name string 76 | want string 77 | err error 78 | }{ 79 | {"id", "", ErrMetadataUnknownError}, 80 | {"idtoken", "", ErrMetadataUnknownError}, 81 | {"projectid", "", ErrMetadataUnknownError}, 82 | {"numericprojectid", "", ErrMetadataUnknownError}, 83 | {"region", "", ErrMetadataUnknownError}, 84 | } 85 | 86 | func TestMetadataErrors(t *testing.T) { 87 | resetRuntimeMetadata() 88 | 89 | ts := httptest.NewServer(http.HandlerFunc(gcptest.BrokenMetadataHandler)) 90 | defer ts.Close() 91 | 92 | metadataEndpoint = ts.URL 93 | 94 | v, err := Token([]string{"https://www.googleapis.com/auth/cloud-platform"}) 95 | if errors.Is(err, nil) { 96 | t.Error(err) 97 | } 98 | 99 | if v != nil { 100 | t.Errorf("got id %v, want nil", v) 101 | } 102 | 103 | for _, tt := range metadataErrorTests { 104 | var ( 105 | err error 106 | v string 107 | ) 108 | 109 | switch tt.name { 110 | case "id": 111 | v, err = ID() 112 | case "idtoken": 113 | v, err = IDToken("https://test-0123456789-ue.a.run.app") 114 | case "projectid": 115 | v, err = ProjectID() 116 | case "numericprojectid": 117 | v, err = NumericProjectID() 118 | case "region": 119 | v, err = Region() 120 | } 121 | 122 | if !errors.Is(err, tt.err) { 123 | t.Error(err) 124 | } 125 | 126 | if v != tt.want { 127 | t.Errorf("got id %v, want %v", v, tt.want) 128 | } 129 | } 130 | } 131 | 132 | func resetRuntimeMetadata() { 133 | rmu.Lock() 134 | defer rmu.Unlock() 135 | 136 | runtimeID = "" 137 | runtimeProjectID = "" 138 | runtimeRegion = "" 139 | runtimeNumericProjectID = "" 140 | } 141 | 142 | func errorMetadataRequest(key string) (string, error) { 143 | endpoint := fmt.Sprintf("%s/computeMetadata/v1/%s", metadataEndpoint, key) 144 | v, err := metadataRequest(endpoint) 145 | return string(v), err 146 | } 147 | -------------------------------------------------------------------------------- /network.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "encoding/json" 5 | "net" 6 | "net/http" 7 | ) 8 | 9 | type IPAddressNotFoundError struct{} 10 | 11 | func (e IPAddressNotFoundError) Error() string { 12 | return "run: RFC1918 address not found" 13 | } 14 | 15 | type NetworkInterface struct { 16 | Name string `json:"name"` 17 | Index int `json:"index"` 18 | HardwareAddr string `json:"hardware_address"` 19 | IPAddresses []string `json:"ip_addresses"` 20 | } 21 | 22 | // IPAddress returns the RFC 1918 IP address assigned to the Cloud Run instance. 23 | func IPAddress(interfaces ...net.Interface) (string, error) { 24 | ips, err := netIPAddresses(interfaces...) 25 | if err != nil { 26 | return "", err 27 | } 28 | 29 | blocks := rfc1918Blocks() 30 | for _, ip := range ips { 31 | // Skip 192.168.1.1 as this is address should not be exposed to Cloud Run instances. 32 | if ip.String() == "192.168.1.1" { 33 | continue 34 | } 35 | 36 | for _, block := range blocks { 37 | if block.Contains(ip) { 38 | return ip.String(), nil 39 | } 40 | } 41 | } 42 | return "", IPAddressNotFoundError{} 43 | } 44 | 45 | func IPAddresses() ([]string, error) { 46 | ipAddresses := make([]string, 0) 47 | 48 | addrs, err := net.InterfaceAddrs() 49 | if err != nil { 50 | return nil, err 51 | } 52 | for _, address := range addrs { 53 | if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 54 | if ipnet.IP.To4() != nil { 55 | ipAddresses = append(ipAddresses, ipnet.IP.String()) 56 | } 57 | } 58 | } 59 | return ipAddresses, nil 60 | } 61 | 62 | // NetworkInterfaces returns a list of network interfaces attached to the 63 | // Cloud Run instance. 64 | func NetworkInterfaces(interfaces ...net.Interface) ([]NetworkInterface, error) { 65 | var err error 66 | interfaces, err = netInterfaces(interfaces...) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | networkInterfaces := make([]NetworkInterface, 0) 72 | for _, i := range interfaces { 73 | var err error 74 | netips, err := netIPAddresses(i) 75 | if err != nil { 76 | return networkInterfaces, err 77 | } 78 | 79 | ips := make([]string, 0) 80 | for _, netIP := range netips { 81 | ips = append(ips, netIP.String()) 82 | } 83 | 84 | ni := NetworkInterface{ 85 | Name: i.Name, 86 | Index: i.Index, 87 | HardwareAddr: i.HardwareAddr.String(), 88 | IPAddresses: ips, 89 | } 90 | 91 | networkInterfaces = append(networkInterfaces, ni) 92 | } 93 | 94 | return networkInterfaces, nil 95 | } 96 | 97 | func netInterfaces(interfaces ...net.Interface) ([]net.Interface, error) { 98 | if len(interfaces) == 0 { 99 | return net.Interfaces() 100 | } 101 | 102 | return interfaces, nil 103 | } 104 | 105 | func netIPAddresses(interfaces ...net.Interface) ([]net.IP, error) { 106 | ips := make([]net.IP, 0) 107 | 108 | ifs, err := netInterfaces(interfaces...) 109 | if err != nil { 110 | return nil, err 111 | } 112 | 113 | for _, i := range ifs { 114 | var err error 115 | addrs, err := i.Addrs() 116 | if err != nil { 117 | return ips, err 118 | } 119 | for _, address := range addrs { 120 | ip, _, err := net.ParseCIDR(address.String()) 121 | if err != nil { 122 | return ips, err 123 | } 124 | ips = append(ips, net.ParseIP(ip.String())) 125 | } 126 | } 127 | 128 | return ips, nil 129 | } 130 | 131 | func rfc1918Blocks() []*net.IPNet { 132 | blocks := make([]*net.IPNet, 0) 133 | 134 | cdirs := []string{ 135 | "10.0.0.0/8", 136 | "172.16.0.0/12", 137 | "192.168.0.0/16", 138 | } 139 | 140 | for _, cdir := range cdirs { 141 | _, block, _ := net.ParseCIDR(cdir) 142 | blocks = append(blocks, block) 143 | } 144 | 145 | return blocks 146 | } 147 | 148 | func NetworkInterfaceHandler(w http.ResponseWriter, r *http.Request) { 149 | interfaces, err := NetworkInterfaces() 150 | if err != nil { 151 | http.Error(w, err.Error(), 500) 152 | return 153 | } 154 | 155 | data, err := json.MarshalIndent(interfaces, "", " ") 156 | if err != nil { 157 | http.Error(w, err.Error(), 500) 158 | return 159 | } 160 | 161 | w.Header().Set("Content-Type", "application/json") 162 | w.Write(data) 163 | } 164 | -------------------------------------------------------------------------------- /run.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | ) 9 | 10 | // WaitForShutdown waits for the SIGKILL, SIGINT, or SIGTERM signals and shutdowns the process. 11 | func WaitForShutdown() { 12 | signalChan := make(chan os.Signal, 1) 13 | signal.Notify(signalChan, syscall.SIGKILL, syscall.SIGINT, syscall.SIGTERM) 14 | s := <-signalChan 15 | 16 | Notice(fmt.Sprintf("Received shutdown signal: %v; shutdown complete.", s.String())) 17 | } 18 | -------------------------------------------------------------------------------- /secrets.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "time" 11 | ) 12 | 13 | var ( 14 | secretmanagerEndpoint = "https://secretmanager.googleapis.com/v1" 15 | ) 16 | 17 | // ErrSecretPermissionDenied is returned when access to a secret is denied. 18 | var ErrSecretPermissionDenied = errors.New("run: permission denied to named secret") 19 | 20 | // ErrSecretUnauthorized is returned when calls to the Secret 21 | // Manager API are unauthorized. 22 | var ErrSecretUnauthorized = errors.New("run: secret manager unauthorized") 23 | 24 | // ErrSecretNotFound is returned when a secret is not found. 25 | var ErrSecretNotFound = errors.New("run: named secret not found") 26 | 27 | // ErrSecretUnknownError is return when calls to the Secret Manager 28 | // API return an unknown error. 29 | var ErrSecretUnknownError = errors.New("run: unexpected error retrieving named secret") 30 | 31 | // ErrSecretUnexpectedResponse is returned when calls to the Secret Manager 32 | // API return an unexpected response. 33 | type ErrSecretUnexpectedResponse struct { 34 | StatusCode int 35 | Err error 36 | } 37 | 38 | func (e *ErrSecretUnexpectedResponse) Error() string { 39 | return "run: unexpected error retrieving named secret" 40 | } 41 | 42 | func (e *ErrSecretUnexpectedResponse) Unwrap() error { return e.Err } 43 | 44 | // SecretVersion represents a Google Cloud Secret. 45 | type SecretVersion struct { 46 | Name string 47 | Payload SecretPayload `json:"payload"` 48 | } 49 | 50 | // SecretPayload holds the secret payload for a Google Cloud Secret. 51 | type SecretPayload struct { 52 | // A base64-encoded string. 53 | Data string `json:"data"` 54 | } 55 | 56 | func formatSecretVersion(project, name, version string) string { 57 | return fmt.Sprintf("projects/%s/secrets/%s/versions/%s", project, name, version) 58 | } 59 | 60 | // AccessSecretVersion returns a Google Cloud Secret for the given 61 | // secret name and version. 62 | func AccessSecretVersion(name, version string) ([]byte, error) { 63 | return accessSecretVersion(name, version) 64 | } 65 | 66 | // AccessSecret returns the latest version of a Google Cloud Secret 67 | // for the given name. 68 | func AccessSecret(name string) ([]byte, error) { 69 | return accessSecretVersion(name, "latest") 70 | } 71 | 72 | func accessSecretVersion(name, version string) ([]byte, error) { 73 | if version == "" { 74 | version = "latest" 75 | } 76 | 77 | token, err := Token([]string{"https://www.googleapis.com/auth/cloud-platform"}) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | numericProjectID, err := NumericProjectID() 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | secretVersion := formatSecretVersion(numericProjectID, name, version) 88 | endpoint := fmt.Sprintf("%s/%s:access", secretmanagerEndpoint, secretVersion) 89 | 90 | request, err := http.NewRequest("GET", endpoint, nil) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | request.Header.Set("User-Agent", userAgent) 96 | request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) 97 | 98 | timeout := time.Duration(5) * time.Second 99 | httpClient := http.Client{Timeout: timeout} 100 | 101 | response, err := httpClient.Do(request) 102 | if err != nil { 103 | return nil, err 104 | } 105 | defer response.Body.Close() 106 | 107 | switch s := response.StatusCode; s { 108 | case 200: 109 | break 110 | case 401: 111 | return nil, ErrSecretUnauthorized 112 | case 403: 113 | return nil, ErrSecretPermissionDenied 114 | case 404: 115 | return nil, ErrSecretNotFound 116 | default: 117 | return nil, &ErrSecretUnexpectedResponse{s, ErrSecretUnknownError} 118 | } 119 | 120 | data, err := ioutil.ReadAll(response.Body) 121 | if err != nil { 122 | return nil, err 123 | } 124 | 125 | var s SecretVersion 126 | err = json.Unmarshal(data, &s) 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | decodedData, err := base64.StdEncoding.DecodeString(s.Payload.Data) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | return decodedData, nil 137 | } 138 | -------------------------------------------------------------------------------- /secrets_test.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/kelseyhightower/run/internal/gcptest" 11 | ) 12 | 13 | var accessSecretTests = []struct { 14 | name string 15 | want []byte 16 | err error 17 | }{ 18 | {"foo", []byte("Test"), nil}, 19 | {"bar", nil, ErrSecretNotFound}, 20 | {"denied", nil, ErrSecretPermissionDenied}, 21 | {"unauthorized", nil, ErrSecretUnauthorized}, 22 | {"unexpected", nil, ErrSecretUnknownError}, 23 | } 24 | 25 | func TestAccessSecret(t *testing.T) { 26 | ms := httptest.NewServer(http.HandlerFunc(gcptest.MetadataHandler)) 27 | defer ms.Close() 28 | 29 | metadataEndpoint = ms.URL 30 | 31 | ss := httptest.NewServer(http.HandlerFunc(gcptest.SecretsHandler)) 32 | defer ss.Close() 33 | 34 | secretmanagerEndpoint = ss.URL 35 | 36 | for _, tt := range accessSecretTests { 37 | secret, err := AccessSecret(tt.name) 38 | if !errors.Is(err, tt.err) { 39 | t.Errorf("unexpected error, want %q, got %q", tt.err, err) 40 | } 41 | 42 | if !bytes.Equal(secret, tt.want) { 43 | t.Errorf("want %v, got %v", tt.want, secret) 44 | } 45 | } 46 | } 47 | 48 | func TestAccessSecretVersion(t *testing.T) { 49 | ms := httptest.NewServer(http.HandlerFunc(gcptest.MetadataHandler)) 50 | defer ms.Close() 51 | 52 | metadataEndpoint = ms.URL 53 | 54 | ss := httptest.NewServer(http.HandlerFunc(gcptest.SecretsHandler)) 55 | defer ss.Close() 56 | 57 | secretmanagerEndpoint = ss.URL 58 | 59 | secret, err := AccessSecretVersion("foo", "1") 60 | if err != nil { 61 | t.Errorf("unexpected error: %v", err) 62 | } 63 | 64 | if !bytes.Equal(secret, []byte("Test")) { 65 | t.Errorf("want %v, got %v", "Test", secret) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /useragent.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | const ( 4 | userAgent = "golang-run/0.0.8" 5 | ) 6 | --------------------------------------------------------------------------------