├── .github └── workflows │ └── testing.yml ├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── csmnamer ├── hash.go ├── hasher_test.go ├── namer.go └── namer_test.go ├── deployment_info.go ├── go.mod ├── go.sum ├── main.go ├── main_test.go ├── map_flag.go ├── map_flag_test.go ├── tools ├── cloudbuild-artifacts.yaml └── package.sh ├── version.go └── version_test.go /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | 3 | on: 4 | pull_request: 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | tests: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-go@v3 16 | with: 17 | go-version: 1.24 18 | 19 | - name: Run tests 20 | run: go test -v ./... -buildvcs=true 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /td-grpc-bootstrap 2 | /td-grpc-bootstrap-*.tar.gz 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows 28 | [Google's Open Source Community Guidelines](https://opensource.google/conduct/). 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | FROM gcr.io/distroless/static 16 | COPY td-grpc-bootstrap ./ 17 | ENTRYPOINT ["/td-grpc-bootstrap"] 18 | -------------------------------------------------------------------------------- /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 | # Traffic Director gRPC Bootstrap 2 | 3 | This repository contains sources to generate a bootstrap file for the XDS 4 | functionality in gRPC when using GCP and Traffic Director as your control plane. 5 | 6 | The gRPC bootstrap format is described in [gRFC A27][]. More information about 7 | Traffic Director is available on the [Google Cloud 8 | website](https://cloud.google.com/traffic-director/). 9 | 10 | [gRFC A27]: https://github.com/grpc/proposal/blob/master/A27-xds-global-load-balancing.md 11 | 12 | ## Public Docker Image 13 | 14 | Built Docker image is publicly available at Google Container Registry: 15 | gcr.io/trafficdirector-prod/td-grpc-bootstrap 16 | 17 | Please refer to the [GKE setup guide](https://cloud.google.com/traffic-director/docs/set-up-proxyless-gke) 18 | for more details. 19 | 20 | ## Running unit tests 21 | 22 | To run unit tests, run the following command: 23 | ```shell 24 | go test ./... -buildvcs=true 25 | ``` -------------------------------------------------------------------------------- /csmnamer/hash.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // DO NOT EDIT: This is a sync of services_platform/thetis/common/gke_net/naming.go 16 | // and should not be modified to maintain functional consistency. 17 | 18 | package csmnamer 19 | 20 | import ( 21 | "crypto/sha256" 22 | "strconv" 23 | ) 24 | 25 | // lookup table to maintain entropy when converting bytes to string. 26 | var table []string 27 | 28 | func init() { 29 | for i := 0; i < 10; i++ { 30 | table = append(table, strconv.Itoa(i)) 31 | } 32 | for i := 0; i < 26; i++ { 33 | table = append(table, string('a'+rune(i))) 34 | } 35 | } 36 | 37 | // Hash creates a content hash string of length n of s utilizing sha256. 38 | // Note that 256 is not evenly divisible by 36, so the first four elements 39 | // will be slightly more likely (3.125% chance) than the rest (2.734375% chance). 40 | // This results in a per-character chance of collision of 41 | // (4 * ((8/256)^2) + (36-4) * ((7/256)^2)) instead of (1 / 36). 42 | // For an 8 character hash string (used for cluster UID and suffix hash), this 43 | // comes out to 3.600e-13 instead of 3.545e-13, which is a negligibly larger 44 | // chance of collision. 45 | func Hash(s string, n int) string { 46 | var h string 47 | bytes := sha256.Sum256(([]byte)(s)) 48 | for i := 0; i < n && i < len(bytes); i++ { 49 | idx := int(bytes[i]) % len(table) 50 | h += table[idx] 51 | } 52 | return h 53 | } 54 | 55 | // TrimFieldsEvenly trims the fields evenly and keeps the total length <= max. 56 | // Truncation is spread in ratio with their original length, meaning smaller 57 | // fields will be truncated less than longer ones. 58 | func TrimFieldsEvenly(max int, fields ...string) []string { 59 | if max <= 0 { 60 | return fields 61 | } 62 | total := 0 63 | for _, s := range fields { 64 | total += len(s) 65 | } 66 | if total <= max { 67 | return fields 68 | } 69 | 70 | // Distribute truncation evenly among the fields. 71 | excess := total - max 72 | remaining := max 73 | var lengths []int 74 | for _, s := range fields { 75 | // Scale truncation to shorten longer fields more than ones that are already 76 | // short. 77 | l := len(s) - len(s)*excess/total - 1 78 | lengths = append(lengths, l) 79 | remaining -= l 80 | } 81 | // Add fractional space that was rounded down. 82 | for i := 0; i < remaining; i++ { 83 | lengths[i]++ 84 | } 85 | 86 | var ret []string 87 | for i, l := range lengths { 88 | ret = append(ret, fields[i][:l]) 89 | } 90 | 91 | return ret 92 | } 93 | -------------------------------------------------------------------------------- /csmnamer/hasher_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // DO NOT EDIT: This is a sync of services_platform/thetis/common/gke_net/naming_test.go 16 | // and should not be modified to maintain functional consistency. 17 | 18 | package csmnamer 19 | 20 | import "testing" 21 | 22 | func TestTrimFieldsEvenly(t *testing.T) { 23 | longString := "01234567890123456789012345678901234567890123456789" 24 | cases := []struct { 25 | desc string 26 | fields []string 27 | want []string 28 | max int 29 | }{ 30 | { 31 | desc: "no-change", 32 | fields: []string{longString}, 33 | want: []string{longString}, 34 | max: 100, 35 | }, 36 | { 37 | desc: "equal-to-max-and-no-change", 38 | fields: []string{longString, longString}, 39 | want: []string{longString, longString}, 40 | max: 100, 41 | }, 42 | { 43 | desc: "equally-trimmed-to-half", 44 | fields: []string{longString, longString}, 45 | want: []string{longString[:25], longString[:25]}, 46 | max: 50, 47 | }, 48 | { 49 | desc: "trimmed-to-only-10", 50 | fields: []string{longString, longString, longString}, 51 | want: []string{longString[:4], longString[:3], longString[:3]}, 52 | max: 10, 53 | }, 54 | { 55 | desc: "trimmed-to-only-3", 56 | fields: []string{longString, longString, longString}, 57 | want: []string{longString[:1], longString[:1], longString[:1]}, 58 | max: 3, 59 | }, 60 | { 61 | desc: "one-long-field-with-one-short-field", 62 | fields: []string{longString, longString[:10]}, 63 | want: []string{"01234567890123456", "012"}, 64 | max: 20, 65 | }, 66 | { 67 | desc: "one-long-field-with-one-short-field-and-trimmed-to-1", 68 | fields: []string{longString, longString[:1]}, 69 | want: []string{longString[:1], ""}, 70 | max: 1, 71 | }, 72 | { 73 | desc: "one-long-field-with-one-short-field-and-trimmed-to-5", 74 | fields: []string{longString, longString[:1]}, 75 | want: []string{longString[:5], ""}, 76 | max: 5, 77 | }, 78 | } 79 | 80 | for _, tc := range cases { 81 | t.Run(tc.desc, func(t *testing.T) { 82 | got := TrimFieldsEvenly(tc.max, tc.fields...) 83 | if len(got) != len(tc.want) { 84 | t.Fatalf("TrimFieldsEvenly(): got length %d, want %d", len(got), len(tc.want)) 85 | } 86 | 87 | totalLen := 0 88 | for i := range got { 89 | totalLen += len(got[i]) 90 | if got[i] != tc.want[i] { 91 | t.Errorf("TrimFieldsEvenly(): got the %d field to be %q, want %q", i, got[i], tc.want[i]) 92 | } 93 | } 94 | 95 | if tc.max < totalLen { 96 | t.Errorf("TrimFieldsEvenly(): got total length %d, want less than %d", totalLen, tc.max) 97 | } 98 | }) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /csmnamer/namer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // DO NOT EDIT: This code is a subset of services_platform/thetis/gateway/core/v1alpha2/common/appnettranslator/gsm/namer.go 16 | // and should not be modified to maintain functional consistency. 17 | 18 | package csmnamer 19 | 20 | import ( 21 | "fmt" 22 | "strings" 23 | "unicode" 24 | ) 25 | 26 | const ( 27 | // Length limit for hash created from fields that uniquely identify a GCE resource and 28 | // appended as a suffix to the resource name 29 | nHashLen = 12 30 | // max length of a GCE resource name. 31 | resourceNameMaxLen = 63 32 | // clusterUIDLen is the length of cluster UID, computed as a hash of ClusterName 33 | // prefix used for GCE resource names created by GAMMA mesh. 34 | clusterUIDLen = 4 35 | // csmMeshPrefix is the prefix override used in the CSMMesh use cases. 36 | csmMeshPrefix = "gsmmesh" 37 | ) 38 | 39 | type MeshNamer struct { 40 | ClusterName string 41 | Location string 42 | } 43 | 44 | func (m *MeshNamer) GenerateMeshId() string { 45 | return readableResourceName(m.ClusterName, m.Location) 46 | } 47 | 48 | // Returns a readable resource name in the following format 49 | // {prefix}-{component#0}-{component#1}...-{hash} 50 | // The length of the returned resource name is guarantee to be within 51 | // resourceNameLen which is the maximum length of a GCE resource. A component 52 | // will only be included explicitly in the resource name if it doesn't have an 53 | // invalid character (any character that is not a letter, digit or '-'). 54 | // Components in the resource name maybe trimmed to fit the maximum length 55 | // requirement. {hash} uniquely identifies the component set. 56 | func readableResourceName(components ...string) string { 57 | // clusterHash enforces uniqueness of resources of different clusters in 58 | // the same project. 59 | clusterHash := Hash(strings.Join(components, ";"), clusterUIDLen) 60 | prefix := csmMeshPrefix + "-" + clusterHash 61 | // resourceHash enforces uniqueness of resources of the same cluster. 62 | resourceHash := Hash(strings.Join(components, ";"), nHashLen) 63 | // Ideally we explicitly include all components in the GCP resource name, so 64 | // it's easier to be related to the corresponding k8s resource(s). However, 65 | // only certain characters are allowed in a GCP resource name(e.g. a common 66 | // character '.' in hostnames is not allowed in GCP resource name). 67 | var explicitComponents []string 68 | for _, c := range components { 69 | // Only explicitly include a component in GCP resource name if all 70 | // characters in it are allowed. Omitting a component here is okay since 71 | // the resourceHash already represents the full component set. 72 | if allCharAllowedInResourceName(c) { 73 | explicitComponents = append(explicitComponents, c) 74 | } 75 | } 76 | // The maximum total length of components is determined by subtracting length 77 | // of the following substring from the maximum length of resource name: 78 | // * prefix 79 | // * separators "-". There will be len(explicitComponents) + 1 of them. 80 | // * hash 81 | componentsMaxLen := resourceNameMaxLen - len(prefix) - (len(explicitComponents) + 1) - len(resourceHash) 82 | // Drop components from the resource name if the allowed maximum total length 83 | // of them is less them the total number of components. (This happens when 84 | // there are too many components) 85 | if componentsMaxLen < len(explicitComponents) { 86 | return fmt.Sprintf("%s-%s", prefix, resourceHash) 87 | } 88 | // Trim components to fit the allowed maximum total length. 89 | trimmed := TrimFieldsEvenly(componentsMaxLen, explicitComponents...) 90 | return fmt.Sprintf("%s-%s-%s", prefix, strings.Join(trimmed, "-"), resourceHash) 91 | } 92 | 93 | func allCharAllowedInResourceName(s string) bool { 94 | if len(s) == 0 { 95 | return false 96 | } 97 | for _, r := range s { 98 | if !(unicode.IsDigit(r) || unicode.IsLetter(r) || r == '-') { 99 | return false 100 | } 101 | } 102 | return true 103 | } 104 | -------------------------------------------------------------------------------- /csmnamer/namer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // DO NOT EDIT: This code is a subset of services_platform/thetis/gateway/core/v1alpha2/common/appnettranslator/gsm/namer_test.go 16 | // and should not be modified to maintain functional consistency. 17 | 18 | package csmnamer 19 | 20 | import ( 21 | "strconv" 22 | "strings" 23 | "testing" 24 | ) 25 | 26 | func longString(n int) string { 27 | var ret string 28 | for i := 0; i < n; i++ { 29 | ret += strconv.Itoa(i % 10) 30 | } 31 | return ret 32 | } 33 | 34 | func manyComponents(n int) []string { 35 | var ret []string 36 | for i := 0; i < n; i++ { 37 | ret = append(ret, strconv.Itoa(i)) 38 | } 39 | return ret 40 | } 41 | 42 | func TestReadableResourceName(t *testing.T) { 43 | cases := []struct { 44 | desc string 45 | components []string 46 | }{ 47 | { 48 | desc: "no-component", 49 | components: []string{}, 50 | }, 51 | { 52 | desc: "single-component", 53 | components: []string{"default"}, 54 | }, 55 | { 56 | desc: "multiple-components", 57 | components: []string{"default", "my-app-net-mesh"}, 58 | }, 59 | { 60 | desc: "multiple-components-with-invalid-char", 61 | components: []string{"default", "my-app-net-mesh", "1.2.3.4??"}, 62 | }, 63 | { 64 | desc: "multiple-components-with-invalid-char", 65 | components: []string{"default", "my-app-net-mesh", "example.com"}, 66 | }, 67 | { 68 | desc: "too-many-components", 69 | components: manyComponents(resourceNameMaxLen), 70 | }, 71 | { 72 | desc: "long-components", 73 | components: []string{"default", longString(resourceNameMaxLen), "80"}, 74 | }, 75 | } 76 | 77 | for _, tc := range cases { 78 | t.Run(tc.desc, func(t *testing.T) { 79 | got := readableResourceName(tc.components...) 80 | if len(got) > resourceNameMaxLen { 81 | t.Errorf("readableResourceName(): got resource name of length %d, want <= %d", len(got), resourceNameMaxLen) 82 | } 83 | subs := strings.Split(got, "-") 84 | gotHashLen := len(subs[len(subs)-1]) 85 | if gotHashLen != nHashLen { 86 | t.Errorf("readableResourceName(): got suffix hash of length %d, want %d", gotHashLen, nHashLen) 87 | } 88 | gotPrefix := subs[0] 89 | if gotPrefix != csmMeshPrefix { 90 | t.Errorf("readableResourceName(): got prefix %s, want %s", gotPrefix, csmMeshPrefix) 91 | } 92 | }) 93 | } 94 | } 95 | 96 | func TestGenerateMeshId(t *testing.T) { 97 | cases := []struct { 98 | desc string 99 | clusterName string 100 | location string 101 | want string 102 | }{ 103 | { 104 | desc: "no-error", 105 | location: "us-central1-a", 106 | clusterName: "test-cluster", 107 | want: "gsmmesh-4g63-test-cluster-us-central1-a-4g63fl4kjz0z", 108 | }, 109 | { 110 | desc: "longest-everything-and-still-no-error", 111 | location: "us-northeast1-a", 112 | clusterName: "test-cluster-test-cluster-test-clusterss", 113 | want: "gsmmesh-l5lo-test-cluster-test-cluster-t-us-northe-l5loax1rjdik", 114 | }, 115 | } 116 | 117 | for _, tc := range cases { 118 | t.Run(tc.desc, func(t *testing.T) { 119 | namer := MeshNamer{ 120 | ClusterName: tc.clusterName, 121 | Location: tc.location, 122 | } 123 | if got := namer.GenerateMeshId(); got != tc.want { 124 | t.Fatalf("Got name %q, want %q", got, tc.want) 125 | } 126 | }) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /deployment_info.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | "net/http" 20 | "net/url" 21 | "time" 22 | ) 23 | 24 | type deploymentType int 25 | 26 | const ( 27 | deploymentTypeUnknown deploymentType = iota 28 | deploymentTypeGKE 29 | deploymentTypeGCE 30 | ) 31 | 32 | // getDeploymentType tries to talk the metadata server at 33 | // http://metadata.google.internal and uses a response header with key "Server" 34 | // to determine the deployment type. 35 | func getDeploymentType() (deploymentType, error) { 36 | parsedURL, err := url.Parse("http://metadata.google.internal") 37 | if err != nil { 38 | return deploymentTypeUnknown, err 39 | } 40 | client := &http.Client{Timeout: 5 * time.Second} 41 | req := &http.Request{ 42 | Method: "GET", 43 | URL: parsedURL, 44 | Header: http.Header{"Metadata-Flavor": {"Google"}}, 45 | } 46 | resp, err := client.Do(req) 47 | if err != nil { 48 | return deploymentTypeUnknown, err 49 | } 50 | resp.Body.Close() 51 | 52 | // Read the "Server" header to determine the deployment type. 53 | vals := resp.Header.Values("Server") 54 | for _, val := range vals { 55 | switch val { 56 | case "GKE Metadata Server": 57 | return deploymentTypeGKE, nil 58 | case "Metadata Server for VM": 59 | return deploymentTypeGCE, nil 60 | default: 61 | return deploymentTypeUnknown, fmt.Errorf("unknown Server type: %s", val) 62 | } 63 | } 64 | 65 | return deploymentTypeUnknown, fmt.Errorf("no values in response header for key: %q", "Server") 66 | } 67 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module td-grpc-bootstrap 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/google/go-cmp v0.5.4 7 | github.com/google/uuid v1.6.0 8 | ) 9 | 10 | require golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= 2 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 3 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 4 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 5 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 6 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 7 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 8 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Binary main generates the xDS bootstrap configuration necessary for gRPC 16 | // applications to connect to and use Traffic Director as their xDS control 17 | // plane. 18 | package main 19 | 20 | import ( 21 | "bytes" 22 | "encoding/json" 23 | "flag" 24 | "fmt" 25 | "io" 26 | "net" 27 | "net/http" 28 | "net/url" 29 | "os" 30 | "path" 31 | "regexp" 32 | "strconv" 33 | "strings" 34 | "time" 35 | 36 | "td-grpc-bootstrap/csmnamer" 37 | 38 | "github.com/google/uuid" 39 | ) 40 | 41 | var ( 42 | xdsServerURI = flag.String("xds-server-uri", "trafficdirector.googleapis.com:443", "override of server uri, for testing") 43 | outputName = flag.String("output", "-", "output file name") 44 | gcpProjectNumber = flag.Int64("gcp-project-number", 0, "the gcp project number. If unknown, can be found via 'gcloud projects list'") 45 | vpcNetworkName = flag.String("vpc-network-name", "default", "VPC network name") 46 | localityZone = flag.String("locality-zone", "", "the locality zone to use, instead of retrieving it from the metadata server. Useful when not running on GCP and/or for testing") 47 | ignoreResourceDeletion = flag.Bool("ignore-resource-deletion-experimental", false, "assume missing resources notify operators when using Traffic Director, as in gRFC A53. This is not currently the case. This flag is EXPERIMENTAL and may be changed or removed in a later release.") 48 | secretsDir = flag.String("secrets-dir", "/var/run/secrets/workload-spiffe-credentials", "path to a directory containing TLS certificates and keys required for PSM security") 49 | gkeClusterName = flag.String("gke-cluster-name", "", "GKE cluster name to use, instead of retrieving it from the metadata server.") 50 | gkePodName = flag.String("gke-pod-name-experimental", "", "GKE pod name to use, instead of reading it from $HOSTNAME or /etc/hostname file. This flag is EXPERIMENTAL and may be changed or removed in a later release.") 51 | gkeNamespace = flag.String("gke-namespace-experimental", "", "GKE namespace to use. This flag is EXPERIMENTAL and may be changed or removed in a later release.") 52 | gkeLocation = flag.String("gke-location-experimental", "", "the location (region/zone) of the GKE cluster, instead of retrieving it from the metadata server. This flag is EXPERIMENTAL and may be changed or removed in a later release.") 53 | gceVM = flag.String("gce-vm-experimental", "", "GCE VM name to use, instead of reading it from the metadata server. This flag is EXPERIMENTAL and may be changed or removed in a later release.") 54 | configMesh = flag.String("config-mesh", "", "Dictates which Mesh resource to use.") 55 | generateMeshID = flag.Bool("generate-mesh-id", false, "When enabled, the CSM MeshID is generated. If config-mesh flag is specified, this flag would be ignored. Location and Cluster Name would be retrieved from the metadata server unless specified via gke-location and gke-cluster-name flags respectively.") 56 | includeAllowedGrpcServices = flag.Bool("include-allowed-grpc-services-experimental", false, "When enabled, generates `allowed_grpc_services` map that includes current xDS Server URI. This flag is EXPERIMENTAL and may be changed or removed in a later release.") 57 | isTrustedXDSServer = flag.Bool("is-trusted-xds-server-experimental", false, "Whether to include the server feature trusted_xds_server for TD. This flag is EXPERIMENTAL and may be changed or removed in a later release.") 58 | ) 59 | 60 | const ( 61 | tdAuthority = "traffic-director-global.xds.googleapis.com" 62 | c2pAuthority = "traffic-director-c2p.xds.googleapis.com" 63 | ) 64 | 65 | func main() { 66 | nodeMetadata := make(map[string]string) 67 | flag.Var(newStringMapVal(&nodeMetadata), "node-metadata", 68 | "additional metadata of the form key=value to be included in the node configuration") 69 | 70 | flag.Var(flag.Lookup("secrets-dir").Value, "secrets-dir-experimental", 71 | "alias of secrets-dir. This flag is EXPERIMENTAL and will be removed in a later release") 72 | flag.Var(flag.Lookup("node-metadata").Value, "node-metadata-experimental", 73 | "alias of node-metadata. This flag is EXPERIMENTAL and will be removed in a later release") 74 | flag.Var(flag.Lookup("gke-cluster-name").Value, "gke-cluster-name-experimental", 75 | "alias of gke-cluster-name. This flag is EXPERIMENTAL and will be removed in a later release") 76 | flag.Var(flag.Lookup("generate-mesh-id").Value, "generate-mesh-id-experimental", 77 | "alias of generate-mesh-id. This flag is EXPERIMENTAL and will be removed in a later release") 78 | flag.Var(flag.Lookup("config-mesh").Value, "config-mesh-experimental", 79 | "alias of config-mesh. This flag is EXPERIMENTAL and will be removed in a later release") 80 | 81 | flag.Parse() 82 | 83 | if *gcpProjectNumber == 0 { 84 | var err error 85 | *gcpProjectNumber, err = getProjectID() 86 | if err != nil { 87 | fmt.Fprintf(os.Stderr, "Error: failed to determine project id: %s\n", err) 88 | os.Exit(1) 89 | } 90 | } 91 | 92 | ip, err := getHostIP() 93 | if err != nil { 94 | fmt.Fprintf(os.Stderr, "Warning: failed to determine host's IP: %s\n", err) 95 | } 96 | 97 | // Retrieve zone from the metadata server only if not specified in args. 98 | zone := *localityZone 99 | if zone == "" { 100 | zone, err = getZone() 101 | if err != nil { 102 | fmt.Fprintf(os.Stderr, "Warning: %s\n", err) 103 | } 104 | } 105 | 106 | // Generate deployment info from metadata server or from command-line 107 | // arguments, with the latter taking preference. 108 | var deploymentInfo map[string]string 109 | dType, err := getDeploymentType() 110 | if err != nil { 111 | fmt.Fprintf(os.Stderr, "Warning: unable to determine deployment type: %s\n", err) 112 | } 113 | switch dType { 114 | case deploymentTypeGKE: 115 | cluster := *gkeClusterName 116 | if cluster == "" { 117 | cluster, err = getClusterName() 118 | if err != nil { 119 | fmt.Fprintf(os.Stderr, "Error: generating deployment info: %s\n", err) 120 | os.Exit(1) 121 | } 122 | } 123 | pod := *gkePodName 124 | if pod == "" { 125 | pod = getPodName() 126 | } 127 | clusterLocation := *gkeLocation 128 | if clusterLocation == "" { 129 | clusterLocation, err = getClusterLocality() 130 | if err != nil { 131 | fmt.Fprintf(os.Stderr, "Error: generating deployment info: %s\n", err) 132 | os.Exit(1) 133 | } 134 | } 135 | deploymentInfo = map[string]string{ 136 | "GKE-CLUSTER": cluster, 137 | "GKE-LOCATION": clusterLocation, 138 | "GCP-ZONE": zone, 139 | "INSTANCE-IP": ip, 140 | "GKE-POD": pod, 141 | } 142 | if *gkeNamespace != "" { 143 | deploymentInfo["GKE-NAMESPACE"] = *gkeNamespace 144 | } 145 | case deploymentTypeGCE: 146 | vmName := *gceVM 147 | if vmName == "" { 148 | vmName = getVMName() 149 | } 150 | deploymentInfo = map[string]string{ 151 | "GCE-VM": vmName, 152 | "GCP-ZONE": zone, 153 | "INSTANCE-IP": ip, 154 | } 155 | } 156 | 157 | meshID := *configMesh 158 | if *generateMeshID { 159 | if meshID != "" { 160 | fmt.Fprint(os.Stderr, "Error: --config-mesh flag cannot be specified while --generate-mesh-id is also set.\n") 161 | os.Exit(1) 162 | } 163 | 164 | clusterLocality := *gkeLocation 165 | if clusterLocality == "" { 166 | clusterLocality, err = getClusterLocality() 167 | if err != nil { 168 | fmt.Fprintf(os.Stderr, "Error: unable to generate mesh id: %s\n", err) 169 | os.Exit(1) 170 | } 171 | } 172 | 173 | cluster := *gkeClusterName 174 | if cluster == "" { 175 | cluster, err = getClusterName() 176 | if err != nil { 177 | fmt.Fprintf(os.Stderr, "Error: unable to generate mesh id: %s\n", err) 178 | os.Exit(1) 179 | } 180 | } 181 | 182 | meshNamer := csmnamer.MeshNamer{ 183 | ClusterName: cluster, 184 | Location: clusterLocality, 185 | } 186 | meshID = meshNamer.GenerateMeshId() 187 | } 188 | 189 | gitCommitHash, err := getCommitID() 190 | if err != nil { 191 | fmt.Fprintf(os.Stderr, "Error: unable to determine git commit ID: %s\n", err) 192 | os.Exit(1) 193 | } 194 | 195 | input := configInput{ 196 | xdsServerURI: *xdsServerURI, 197 | gcpProjectNumber: *gcpProjectNumber, 198 | vpcNetworkName: *vpcNetworkName, 199 | ip: ip, 200 | zone: zone, 201 | ignoreResourceDeletion: *ignoreResourceDeletion, 202 | secretsDir: *secretsDir, 203 | metadataLabels: nodeMetadata, 204 | deploymentInfo: deploymentInfo, 205 | configMesh: meshID, 206 | ipv6Capable: isIPv6Capable(), 207 | gitCommitHash: gitCommitHash, 208 | isTrustedXDSServer: *isTrustedXDSServer, 209 | includeAllowedGrpcServices: *includeAllowedGrpcServices, 210 | } 211 | 212 | if err := validate(input); err != nil { 213 | fmt.Fprintf(os.Stderr, "Error: %s\n", err) 214 | os.Exit(1) 215 | } 216 | 217 | config, err := generate(input) 218 | if err != nil { 219 | fmt.Fprintf(os.Stderr, "Failed to generate config: %s\n", err) 220 | os.Exit(1) 221 | } 222 | var output *os.File 223 | if *outputName == "-" { 224 | output = os.Stdout 225 | } else { 226 | output, err = os.Create(*outputName) 227 | if err != nil { 228 | fmt.Fprintf(os.Stderr, "Failed to open output file: %s\n", err) 229 | os.Exit(1) 230 | } 231 | } 232 | _, err = output.Write(config) 233 | if err != nil { 234 | fmt.Fprintf(os.Stderr, "Failed to write config: %s\n", err) 235 | os.Exit(1) 236 | } 237 | _, err = output.Write([]byte("\n")) 238 | if err != nil { 239 | fmt.Fprintf(os.Stderr, "Failed to write config: %s\n", err) 240 | os.Exit(1) 241 | } 242 | err = output.Close() 243 | if err != nil { 244 | fmt.Fprintf(os.Stderr, "Failed to close config: %s\n", err) 245 | os.Exit(1) 246 | } 247 | } 248 | 249 | type configInput struct { 250 | xdsServerURI string 251 | gcpProjectNumber int64 252 | vpcNetworkName string 253 | ip string 254 | zone string 255 | ignoreResourceDeletion bool 256 | secretsDir string 257 | metadataLabels map[string]string 258 | deploymentInfo map[string]string 259 | configMesh string 260 | ipv6Capable bool 261 | gitCommitHash string 262 | isTrustedXDSServer bool 263 | includeAllowedGrpcServices bool 264 | } 265 | 266 | func validate(in configInput) error { 267 | re := regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9-]{0,63}$`) 268 | if in.configMesh != "" && !re.MatchString(in.configMesh) { 269 | return fmt.Errorf("config-mesh may only contain letters, numbers, and '-'. It must begin with a letter and must not exceed 64 characters in length") 270 | } 271 | 272 | return nil 273 | } 274 | 275 | func generate(in configInput) ([]byte, error) { 276 | xdsServer := server{ 277 | ServerURI: in.xdsServerURI, 278 | ChannelCreds: []creds{{Type: "google_default"}}, 279 | } 280 | 281 | // Set xds_v3. 282 | xdsServer.ServerFeatures = append(xdsServer.ServerFeatures, "xds_v3") 283 | if in.isTrustedXDSServer { 284 | xdsServer.ServerFeatures = append(xdsServer.ServerFeatures, "trusted_xds_server") 285 | } 286 | 287 | if in.ignoreResourceDeletion { 288 | xdsServer.ServerFeatures = append(xdsServer.ServerFeatures, "ignore_resource_deletion") 289 | } 290 | 291 | // Setting networkIdentifier based on flags. 292 | networkIdentifier := in.vpcNetworkName 293 | if in.configMesh != "" { 294 | networkIdentifier = fmt.Sprintf("mesh:%s", in.configMesh) 295 | } 296 | 297 | c := &config{ 298 | XDSServers: []server{xdsServer}, 299 | Node: &node{ 300 | ID: fmt.Sprintf("projects/%d/networks/%s/nodes/%s", in.gcpProjectNumber, networkIdentifier, uuid.New().String()), 301 | Cluster: "cluster", // unused by TD 302 | Locality: &locality{ 303 | Zone: in.zone, 304 | }, 305 | Metadata: map[string]any{ 306 | "INSTANCE_IP": in.ip, 307 | "TRAFFICDIRECTOR_GRPC_BOOTSTRAP_GENERATOR_SHA": in.gitCommitHash, 308 | }, 309 | }, 310 | Authorities: map[string]Authority{ 311 | tdAuthority: { 312 | // Listener Resource Name format for normal TD usecases looks like: 313 | // xdstp:///envoy.config.listener.v3.Listener//<(network)|(mesh:mesh_name)>/id 314 | ClientListenerResourceNameTemplate: fmt.Sprintf("xdstp://%s/envoy.config.listener.v3.Listener/%d/%s/%%s", tdAuthority, in.gcpProjectNumber, networkIdentifier), 315 | }, 316 | c2pAuthority: { 317 | // In the case of DirectPath, it is safe to assume that the operator is notified of missing resources. 318 | // In other words, "ignore_resource_deletion" server_features is always set. 319 | XDSServers: []server{{ 320 | ServerURI: "dns:///directpath-pa.googleapis.com", 321 | ChannelCreds: []creds{{Type: "google_default"}}, 322 | ServerFeatures: []string{"xds_v3", "ignore_resource_deletion"}, 323 | }}, 324 | ClientListenerResourceNameTemplate: fmt.Sprintf("xdstp://%s/envoy.config.listener.v3.Listener/%%s", c2pAuthority), 325 | }, 326 | }, 327 | ClientDefaultListenerResourceNameTemplate: fmt.Sprintf("xdstp://%s/envoy.config.listener.v3.Listener/%d/%s/%%s", tdAuthority, in.gcpProjectNumber, networkIdentifier), 328 | } 329 | 330 | for k, v := range in.metadataLabels { 331 | c.Node.Metadata[k] = v 332 | } 333 | 334 | // For PSM Security. 335 | c.CertificateProviders = map[string]certificateProviderConfig{ 336 | "google_cloud_private_spiffe": { 337 | PluginName: "file_watcher", 338 | Config: privateSPIFFEConfig{ 339 | CertificateFile: path.Join(in.secretsDir, "certificates.pem"), 340 | PrivateKeyFile: path.Join(in.secretsDir, "private_key.pem"), 341 | CACertificateFile: path.Join(in.secretsDir, "ca_certificates.pem"), 342 | // The file_watcher plugin will parse this a Duration proto, but it is totally 343 | // fine to just emit a string here. 344 | RefreshInterval: "600s", 345 | }, 346 | }, 347 | } 348 | 349 | // For Rate Limiting 350 | if in.includeAllowedGrpcServices { 351 | c.AllowedGrpcServices = map[string]allowedGrpcServiceConfig{ 352 | getQualifiedXDSURI(in.xdsServerURI): { 353 | ChannelCreds: []creds{{Type: "google_default"}}, 354 | }, 355 | } 356 | } 357 | 358 | c.ServerListenerResourceNameTemplate = "grpc/server?xds.resource.listening_address=%s" 359 | if in.deploymentInfo != nil { 360 | c.Node.Metadata["TRAFFIC_DIRECTOR_CLIENT_ENVIRONMENT"] = in.deploymentInfo 361 | } 362 | 363 | if in.ipv6Capable { 364 | c.Node.Metadata["TRAFFICDIRECTOR_DIRECTPATH_C2P_IPV6_CAPABLE"] = true 365 | } 366 | 367 | return json.MarshalIndent(c, "", " ") 368 | } 369 | 370 | func getHostIP() (string, error) { 371 | hostname, err := os.Hostname() 372 | if err != nil { 373 | return "", err 374 | } 375 | addrs, err := net.LookupHost(hostname) 376 | if err != nil { 377 | return "", err 378 | } 379 | if len(addrs) == 0 { 380 | return "", fmt.Errorf("no addresses found for hostname: %s", hostname) 381 | } 382 | return addrs[0], nil 383 | } 384 | 385 | func getZone() (string, error) { 386 | qualifiedZone, err := getFromMetadata("http://metadata.google.internal/computeMetadata/v1/instance/zone") 387 | if err != nil { 388 | return "", fmt.Errorf("failed to determine zone: could not discover instance zone: %w", err) 389 | } 390 | i := bytes.LastIndexByte(qualifiedZone, '/') 391 | if i == -1 { 392 | return "", fmt.Errorf("failed to determine zone: could not parse zone from metadata server: %s", qualifiedZone) 393 | } 394 | return string(qualifiedZone[i+1:]), nil 395 | } 396 | 397 | func getProjectID() (int64, error) { 398 | projectIDBytes, err := getFromMetadata("http://metadata.google.internal/computeMetadata/v1/project/numeric-project-id") 399 | if err != nil { 400 | return 0, fmt.Errorf("could not discover project id: %w", err) 401 | } 402 | projectID, err := strconv.ParseInt(string(projectIDBytes), 10, 64) 403 | if err != nil { 404 | return 0, fmt.Errorf("could not parse project id from metadata server: %w", err) 405 | } 406 | return projectID, nil 407 | } 408 | 409 | func getClusterName() (string, error) { 410 | cluster, err := getFromMetadata("http://metadata.google.internal/computeMetadata/v1/instance/attributes/cluster-name") 411 | if err != nil { 412 | return "", fmt.Errorf("failed to determine GKE cluster name: %s", err) 413 | } 414 | return string(cluster), nil 415 | } 416 | 417 | func getClusterLocality() (string, error) { 418 | locality, err := getFromMetadata("http://metadata.google.internal/computeMetadata/v1/instance/attributes/cluster-location") 419 | if err != nil { 420 | return "", fmt.Errorf("failed to determine GKE cluster locality: %s", err) 421 | } 422 | return string(locality), nil 423 | } 424 | 425 | func getPodName() string { 426 | pod, err := os.Hostname() 427 | if err != nil { 428 | fmt.Fprintf(os.Stderr, "could not discover GKE pod name: %v", err) 429 | } 430 | return pod 431 | } 432 | 433 | func getVMName() string { 434 | vm, err := getFromMetadata("http://metadata.google.internal/computeMetadata/v1/instance/name") 435 | if err != nil { 436 | fmt.Fprintf(os.Stderr, "could not discover GCE VM name: %v", err) 437 | return "" 438 | } 439 | return string(vm) 440 | } 441 | 442 | // isIPv6Capable returns true if the VM is configured with an IPv6 address. 443 | // This will contact the metadata server to retrieve this information. 444 | func isIPv6Capable() bool { 445 | _, err := getFromMetadata("http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/ipv6s") 446 | return err == nil 447 | } 448 | 449 | func getFromMetadata(urlStr string) ([]byte, error) { 450 | parsedURL, err := url.Parse(urlStr) 451 | if err != nil { 452 | return nil, err 453 | } 454 | client := &http.Client{ 455 | Timeout: 5 * time.Second, 456 | } 457 | req := &http.Request{ 458 | Method: "GET", 459 | URL: parsedURL, 460 | Header: http.Header{ 461 | "Metadata-Flavor": {"Google"}, 462 | }, 463 | } 464 | resp, err := client.Do(req) 465 | if err != nil { 466 | return nil, fmt.Errorf("failed communicating with metadata server: %w", err) 467 | } 468 | body, err := io.ReadAll(resp.Body) 469 | resp.Body.Close() 470 | if err != nil { 471 | return nil, fmt.Errorf("failed reading from metadata server: %w", err) 472 | } 473 | if code := resp.StatusCode; code < 200 || code > 299 { 474 | return nil, fmt.Errorf("metadata server returned status code %d for url %q", code, parsedURL) 475 | } 476 | return body, nil 477 | } 478 | 479 | func getQualifiedXDSURI(serverURI string) string { 480 | if strings.HasPrefix(serverURI, "dns:///") { 481 | return serverURI 482 | } 483 | return "dns:///" + serverURI 484 | } 485 | 486 | type config struct { 487 | XDSServers []server `json:"xds_servers,omitempty"` 488 | Authorities map[string]Authority `json:"authorities,omitempty"` 489 | Node *node `json:"node,omitempty"` 490 | CertificateProviders map[string]certificateProviderConfig `json:"certificate_providers,omitempty"` 491 | AllowedGrpcServices map[string]allowedGrpcServiceConfig `json:"allowed_grpc_services,omitempty"` 492 | ServerListenerResourceNameTemplate string `json:"server_listener_resource_name_template,omitempty"` 493 | ClientDefaultListenerResourceNameTemplate string `json:"client_default_listener_resource_name_template,omitempty"` 494 | } 495 | 496 | type server struct { 497 | ServerURI string `json:"server_uri,omitempty"` 498 | ChannelCreds []creds `json:"channel_creds,omitempty"` 499 | ServerFeatures []string `json:"server_features,omitempty"` 500 | } 501 | 502 | // Authority is the configuration corresponding to an authority name in the map. 503 | // 504 | // For more details, see: 505 | // https://github.com/grpc/proposal/blob/master/A47-xds-federation.md#bootstrap-config-changes 506 | type Authority struct { 507 | XDSServers []server `json:"xds_servers,omitempty"` 508 | ClientListenerResourceNameTemplate string `json:"client_listener_resource_name_template,omitempty"` 509 | } 510 | 511 | type creds struct { 512 | Type string `json:"type,omitempty"` 513 | Config any `json:"config,omitempty"` 514 | } 515 | 516 | type node struct { 517 | ID string `json:"id,omitempty"` 518 | Cluster string `json:"cluster,omitempty"` 519 | Metadata map[string]any `json:"metadata,omitempty"` 520 | Locality *locality `json:"locality,omitempty"` 521 | BuildVersion string `json:"build_version,omitempty"` 522 | } 523 | 524 | type locality struct { 525 | Region string `json:"region,omitempty"` 526 | Zone string `json:"zone,omitempty"` 527 | SubZone string `json:"sub_zone,omitempty"` 528 | } 529 | 530 | type certificateProviderConfig struct { 531 | PluginName string `json:"plugin_name,omitempty"` 532 | Config any `json:"config,omitempty"` 533 | } 534 | 535 | type privateSPIFFEConfig struct { 536 | CertificateFile string `json:"certificate_file,omitempty"` 537 | PrivateKeyFile string `json:"private_key_file,omitempty"` 538 | CACertificateFile string `json:"ca_certificate_file,omitempty"` 539 | RefreshInterval string `json:"refresh_interval,omitempty"` 540 | } 541 | 542 | type allowedGrpcServiceConfig struct { 543 | ChannelCreds []creds `json:"channel_creds,omitempty"` 544 | } 545 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | "math/rand" 20 | "net" 21 | "net/http" 22 | "net/http/httptest" 23 | "strings" 24 | "testing" 25 | 26 | "github.com/google/go-cmp/cmp" 27 | "github.com/google/uuid" 28 | ) 29 | 30 | func TestValidate(t *testing.T) { 31 | tests := []struct { 32 | desc string 33 | input configInput 34 | wantError string 35 | }{ 36 | { 37 | desc: "fails when config-mesh has too many characters", 38 | input: configInput{ 39 | xdsServerURI: "example.com:443", 40 | gcpProjectNumber: 123456789012345, 41 | vpcNetworkName: "thedefault", 42 | ip: "10.9.8.7", 43 | zone: "uscentral-5", 44 | metadataLabels: map[string]string{"k1": "v1", "k2": "v2"}, 45 | configMesh: strings.Repeat("a", 65), 46 | }, 47 | wantError: "config-mesh may only contain letters, numbers, and '-'. It must begin with a letter and must not exceed 64 characters in length", 48 | }, 49 | { 50 | desc: "fails when config-mesh does not start with an alphabetic letter", 51 | input: configInput{ 52 | xdsServerURI: "example.com:443", 53 | gcpProjectNumber: 123456789012345, 54 | vpcNetworkName: "thedefault", 55 | ip: "10.9.8.7", 56 | zone: "uscentral-5", 57 | metadataLabels: map[string]string{"k1": "v1", "k2": "v2"}, 58 | configMesh: "4foo", 59 | }, 60 | wantError: "config-mesh may only contain letters, numbers, and '-'. It must begin with a letter and must not exceed 64 characters in length", 61 | }, 62 | { 63 | desc: "fails when config-mesh contains characters besides letters, numbers, and hyphens.", 64 | input: configInput{ 65 | xdsServerURI: "example.com:443", 66 | gcpProjectNumber: 123456789012345, 67 | vpcNetworkName: "thedefault", 68 | ip: "10.9.8.7", 69 | zone: "uscentral-5", 70 | metadataLabels: map[string]string{"k1": "v1", "k2": "v2"}, 71 | configMesh: "h*x8", 72 | }, 73 | wantError: "config-mesh may only contain letters, numbers, and '-'. It must begin with a letter and must not exceed 64 characters in length", 74 | }, 75 | } 76 | 77 | for _, test := range tests { 78 | t.Run(test.desc, func(t *testing.T) { 79 | err := validate(test.input) 80 | if test.wantError != err.Error() { 81 | t.Fatalf("validate(%+v) returned output does not match expected:\nGot: \"%v\"\nWant: \"%s\"", test.input, err.Error(), test.wantError) 82 | } 83 | }) 84 | } 85 | } 86 | 87 | func TestGenerate(t *testing.T) { 88 | tests := []struct { 89 | desc string 90 | input configInput 91 | wantOutput string 92 | }{ 93 | { 94 | desc: "happy case with v3 config by default", 95 | input: configInput{ 96 | xdsServerURI: "example.com:443", 97 | gcpProjectNumber: 123456789012345, 98 | vpcNetworkName: "thedefault", 99 | ip: "10.9.8.7", 100 | zone: "uscentral-5", 101 | metadataLabels: map[string]string{"k1": "v1", "k2": "v2"}, 102 | gitCommitHash: "7202b7c611ebd6d382b7b0240f50e9824200bffd", 103 | }, 104 | wantOutput: `{ 105 | "xds_servers": [ 106 | { 107 | "server_uri": "example.com:443", 108 | "channel_creds": [ 109 | { 110 | "type": "google_default" 111 | } 112 | ], 113 | "server_features": [ 114 | "xds_v3" 115 | ] 116 | } 117 | ], 118 | "authorities": { 119 | "traffic-director-c2p.xds.googleapis.com": { 120 | "xds_servers": [ 121 | { 122 | "server_uri": "dns:///directpath-pa.googleapis.com", 123 | "channel_creds": [ 124 | { 125 | "type": "google_default" 126 | } 127 | ], 128 | "server_features": [ 129 | "xds_v3", 130 | "ignore_resource_deletion" 131 | ] 132 | } 133 | ], 134 | "client_listener_resource_name_template": "xdstp://traffic-director-c2p.xds.googleapis.com/envoy.config.listener.v3.Listener/%s" 135 | }, 136 | "traffic-director-global.xds.googleapis.com": { 137 | "client_listener_resource_name_template": "xdstp://traffic-director-global.xds.googleapis.com/envoy.config.listener.v3.Listener/123456789012345/thedefault/%s" 138 | } 139 | }, 140 | "node": { 141 | "id": "projects/123456789012345/networks/thedefault/nodes/52fdfc07-2182-454f-963f-5f0f9a621d72", 142 | "cluster": "cluster", 143 | "metadata": { 144 | "INSTANCE_IP": "10.9.8.7", 145 | "TRAFFICDIRECTOR_GRPC_BOOTSTRAP_GENERATOR_SHA": "7202b7c611ebd6d382b7b0240f50e9824200bffd", 146 | "k1": "v1", 147 | "k2": "v2" 148 | }, 149 | "locality": { 150 | "zone": "uscentral-5" 151 | } 152 | }, 153 | "certificate_providers": { 154 | "google_cloud_private_spiffe": { 155 | "plugin_name": "file_watcher", 156 | "config": { 157 | "certificate_file": "certificates.pem", 158 | "private_key_file": "private_key.pem", 159 | "ca_certificate_file": "ca_certificates.pem", 160 | "refresh_interval": "600s" 161 | } 162 | } 163 | }, 164 | "server_listener_resource_name_template": "grpc/server?xds.resource.listening_address=%s", 165 | "client_default_listener_resource_name_template": "xdstp://traffic-director-global.xds.googleapis.com/envoy.config.listener.v3.Listener/123456789012345/thedefault/%s" 166 | }`, 167 | }, 168 | { 169 | desc: "Server feature for Trusted xds server", 170 | input: configInput{ 171 | xdsServerURI: "example.com:443", 172 | gcpProjectNumber: 123456789012345, 173 | vpcNetworkName: "thedefault", 174 | ip: "10.9.8.7", 175 | zone: "uscentral-5", 176 | metadataLabels: map[string]string{"k1": "v1", "k2": "v2"}, 177 | gitCommitHash: "7202b7c611ebd6d382b7b0240f50e9824200bffd", 178 | isTrustedXDSServer: true, 179 | }, 180 | wantOutput: `{ 181 | "xds_servers": [ 182 | { 183 | "server_uri": "example.com:443", 184 | "channel_creds": [ 185 | { 186 | "type": "google_default" 187 | } 188 | ], 189 | "server_features": [ 190 | "xds_v3", 191 | "trusted_xds_server" 192 | ] 193 | } 194 | ], 195 | "authorities": { 196 | "traffic-director-c2p.xds.googleapis.com": { 197 | "xds_servers": [ 198 | { 199 | "server_uri": "dns:///directpath-pa.googleapis.com", 200 | "channel_creds": [ 201 | { 202 | "type": "google_default" 203 | } 204 | ], 205 | "server_features": [ 206 | "xds_v3", 207 | "ignore_resource_deletion" 208 | ] 209 | } 210 | ], 211 | "client_listener_resource_name_template": "xdstp://traffic-director-c2p.xds.googleapis.com/envoy.config.listener.v3.Listener/%s" 212 | }, 213 | "traffic-director-global.xds.googleapis.com": { 214 | "client_listener_resource_name_template": "xdstp://traffic-director-global.xds.googleapis.com/envoy.config.listener.v3.Listener/123456789012345/thedefault/%s" 215 | } 216 | }, 217 | "node": { 218 | "id": "projects/123456789012345/networks/thedefault/nodes/52fdfc07-2182-454f-963f-5f0f9a621d72", 219 | "cluster": "cluster", 220 | "metadata": { 221 | "INSTANCE_IP": "10.9.8.7", 222 | "TRAFFICDIRECTOR_GRPC_BOOTSTRAP_GENERATOR_SHA": "7202b7c611ebd6d382b7b0240f50e9824200bffd", 223 | "k1": "v1", 224 | "k2": "v2" 225 | }, 226 | "locality": { 227 | "zone": "uscentral-5" 228 | } 229 | }, 230 | "certificate_providers": { 231 | "google_cloud_private_spiffe": { 232 | "plugin_name": "file_watcher", 233 | "config": { 234 | "certificate_file": "certificates.pem", 235 | "private_key_file": "private_key.pem", 236 | "ca_certificate_file": "ca_certificates.pem", 237 | "refresh_interval": "600s" 238 | } 239 | } 240 | }, 241 | "server_listener_resource_name_template": "grpc/server?xds.resource.listening_address=%s", 242 | "client_default_listener_resource_name_template": "xdstp://traffic-director-global.xds.googleapis.com/envoy.config.listener.v3.Listener/123456789012345/thedefault/%s" 243 | }`, 244 | }, 245 | { 246 | desc: "happy case with security config", 247 | input: configInput{ 248 | xdsServerURI: "example.com:443", 249 | gcpProjectNumber: 123456789012345, 250 | vpcNetworkName: "thedefault", 251 | ip: "10.9.8.7", 252 | zone: "uscentral-5", 253 | secretsDir: "/secrets/dir/", 254 | gitCommitHash: "7202b7c611ebd6d382b7b0240f50e9824200bffd", 255 | }, 256 | wantOutput: `{ 257 | "xds_servers": [ 258 | { 259 | "server_uri": "example.com:443", 260 | "channel_creds": [ 261 | { 262 | "type": "google_default" 263 | } 264 | ], 265 | "server_features": [ 266 | "xds_v3" 267 | ] 268 | } 269 | ], 270 | "authorities": { 271 | "traffic-director-c2p.xds.googleapis.com": { 272 | "xds_servers": [ 273 | { 274 | "server_uri": "dns:///directpath-pa.googleapis.com", 275 | "channel_creds": [ 276 | { 277 | "type": "google_default" 278 | } 279 | ], 280 | "server_features": [ 281 | "xds_v3", 282 | "ignore_resource_deletion" 283 | ] 284 | } 285 | ], 286 | "client_listener_resource_name_template": "xdstp://traffic-director-c2p.xds.googleapis.com/envoy.config.listener.v3.Listener/%s" 287 | }, 288 | "traffic-director-global.xds.googleapis.com": { 289 | "client_listener_resource_name_template": "xdstp://traffic-director-global.xds.googleapis.com/envoy.config.listener.v3.Listener/123456789012345/thedefault/%s" 290 | } 291 | }, 292 | "node": { 293 | "id": "projects/123456789012345/networks/thedefault/nodes/52fdfc07-2182-454f-963f-5f0f9a621d72", 294 | "cluster": "cluster", 295 | "metadata": { 296 | "INSTANCE_IP": "10.9.8.7", 297 | "TRAFFICDIRECTOR_GRPC_BOOTSTRAP_GENERATOR_SHA": "7202b7c611ebd6d382b7b0240f50e9824200bffd" 298 | }, 299 | "locality": { 300 | "zone": "uscentral-5" 301 | } 302 | }, 303 | "certificate_providers": { 304 | "google_cloud_private_spiffe": { 305 | "plugin_name": "file_watcher", 306 | "config": { 307 | "certificate_file": "/secrets/dir/certificates.pem", 308 | "private_key_file": "/secrets/dir/private_key.pem", 309 | "ca_certificate_file": "/secrets/dir/ca_certificates.pem", 310 | "refresh_interval": "600s" 311 | } 312 | } 313 | }, 314 | "server_listener_resource_name_template": "grpc/server?xds.resource.listening_address=%s", 315 | "client_default_listener_resource_name_template": "xdstp://traffic-director-global.xds.googleapis.com/envoy.config.listener.v3.Listener/123456789012345/thedefault/%s" 316 | }`, 317 | }, 318 | { 319 | desc: "happy case with deployment info", 320 | input: configInput{ 321 | xdsServerURI: "example.com:443", 322 | gcpProjectNumber: 123456789012345, 323 | vpcNetworkName: "thedefault", 324 | ip: "10.9.8.7", 325 | zone: "uscentral-5", 326 | deploymentInfo: map[string]string{ 327 | "GCP-ZONE": "uscentral-5", 328 | "GKE-CLUSTER": "test-gke-cluster", 329 | "GKE-NAMESPACE": "test-gke-namespace", 330 | "GKE-POD": "test-gke-pod", 331 | "INSTANCE-IP": "10.9.8.7", 332 | "GCE-VM": "test-gce-vm", 333 | }, 334 | gitCommitHash: "7202b7c611ebd6d382b7b0240f50e9824200bffd", 335 | }, 336 | wantOutput: `{ 337 | "xds_servers": [ 338 | { 339 | "server_uri": "example.com:443", 340 | "channel_creds": [ 341 | { 342 | "type": "google_default" 343 | } 344 | ], 345 | "server_features": [ 346 | "xds_v3" 347 | ] 348 | } 349 | ], 350 | "authorities": { 351 | "traffic-director-c2p.xds.googleapis.com": { 352 | "xds_servers": [ 353 | { 354 | "server_uri": "dns:///directpath-pa.googleapis.com", 355 | "channel_creds": [ 356 | { 357 | "type": "google_default" 358 | } 359 | ], 360 | "server_features": [ 361 | "xds_v3", 362 | "ignore_resource_deletion" 363 | ] 364 | } 365 | ], 366 | "client_listener_resource_name_template": "xdstp://traffic-director-c2p.xds.googleapis.com/envoy.config.listener.v3.Listener/%s" 367 | }, 368 | "traffic-director-global.xds.googleapis.com": { 369 | "client_listener_resource_name_template": "xdstp://traffic-director-global.xds.googleapis.com/envoy.config.listener.v3.Listener/123456789012345/thedefault/%s" 370 | } 371 | }, 372 | "node": { 373 | "id": "projects/123456789012345/networks/thedefault/nodes/52fdfc07-2182-454f-963f-5f0f9a621d72", 374 | "cluster": "cluster", 375 | "metadata": { 376 | "INSTANCE_IP": "10.9.8.7", 377 | "TRAFFICDIRECTOR_GRPC_BOOTSTRAP_GENERATOR_SHA": "7202b7c611ebd6d382b7b0240f50e9824200bffd", 378 | "TRAFFIC_DIRECTOR_CLIENT_ENVIRONMENT": { 379 | "GCE-VM": "test-gce-vm", 380 | "GCP-ZONE": "uscentral-5", 381 | "GKE-CLUSTER": "test-gke-cluster", 382 | "GKE-NAMESPACE": "test-gke-namespace", 383 | "GKE-POD": "test-gke-pod", 384 | "INSTANCE-IP": "10.9.8.7" 385 | } 386 | }, 387 | "locality": { 388 | "zone": "uscentral-5" 389 | } 390 | }, 391 | "certificate_providers": { 392 | "google_cloud_private_spiffe": { 393 | "plugin_name": "file_watcher", 394 | "config": { 395 | "certificate_file": "certificates.pem", 396 | "private_key_file": "private_key.pem", 397 | "ca_certificate_file": "ca_certificates.pem", 398 | "refresh_interval": "600s" 399 | } 400 | } 401 | }, 402 | "server_listener_resource_name_template": "grpc/server?xds.resource.listening_address=%s", 403 | "client_default_listener_resource_name_template": "xdstp://traffic-director-global.xds.googleapis.com/envoy.config.listener.v3.Listener/123456789012345/thedefault/%s" 404 | }`, 405 | }, 406 | { 407 | desc: "configMesh specified", 408 | input: configInput{ 409 | xdsServerURI: "example.com:443", 410 | gcpProjectNumber: 123456789012345, 411 | vpcNetworkName: "thedefault", 412 | ip: "10.9.8.7", 413 | zone: "uscentral-5", 414 | deploymentInfo: map[string]string{ 415 | "GCP-ZONE": "uscentral-5", 416 | "GKE-CLUSTER": "test-gke-cluster", 417 | "GKE-NAMESPACE": "test-gke-namespace", 418 | "GKE-POD": "test-gke-pod", 419 | "INSTANCE-IP": "10.9.8.7", 420 | "GCE-VM": "test-gce-vm", 421 | }, 422 | configMesh: "testmesh", 423 | gitCommitHash: "7202b7c611ebd6d382b7b0240f50e9824200bffd", 424 | }, 425 | wantOutput: `{ 426 | "xds_servers": [ 427 | { 428 | "server_uri": "example.com:443", 429 | "channel_creds": [ 430 | { 431 | "type": "google_default" 432 | } 433 | ], 434 | "server_features": [ 435 | "xds_v3" 436 | ] 437 | } 438 | ], 439 | "authorities": { 440 | "traffic-director-c2p.xds.googleapis.com": { 441 | "xds_servers": [ 442 | { 443 | "server_uri": "dns:///directpath-pa.googleapis.com", 444 | "channel_creds": [ 445 | { 446 | "type": "google_default" 447 | } 448 | ], 449 | "server_features": [ 450 | "xds_v3", 451 | "ignore_resource_deletion" 452 | ] 453 | } 454 | ], 455 | "client_listener_resource_name_template": "xdstp://traffic-director-c2p.xds.googleapis.com/envoy.config.listener.v3.Listener/%s" 456 | }, 457 | "traffic-director-global.xds.googleapis.com": { 458 | "client_listener_resource_name_template": "xdstp://traffic-director-global.xds.googleapis.com/envoy.config.listener.v3.Listener/123456789012345/mesh:testmesh/%s" 459 | } 460 | }, 461 | "node": { 462 | "id": "projects/123456789012345/networks/mesh:testmesh/nodes/52fdfc07-2182-454f-963f-5f0f9a621d72", 463 | "cluster": "cluster", 464 | "metadata": { 465 | "INSTANCE_IP": "10.9.8.7", 466 | "TRAFFICDIRECTOR_GRPC_BOOTSTRAP_GENERATOR_SHA": "7202b7c611ebd6d382b7b0240f50e9824200bffd", 467 | "TRAFFIC_DIRECTOR_CLIENT_ENVIRONMENT": { 468 | "GCE-VM": "test-gce-vm", 469 | "GCP-ZONE": "uscentral-5", 470 | "GKE-CLUSTER": "test-gke-cluster", 471 | "GKE-NAMESPACE": "test-gke-namespace", 472 | "GKE-POD": "test-gke-pod", 473 | "INSTANCE-IP": "10.9.8.7" 474 | } 475 | }, 476 | "locality": { 477 | "zone": "uscentral-5" 478 | } 479 | }, 480 | "certificate_providers": { 481 | "google_cloud_private_spiffe": { 482 | "plugin_name": "file_watcher", 483 | "config": { 484 | "certificate_file": "certificates.pem", 485 | "private_key_file": "private_key.pem", 486 | "ca_certificate_file": "ca_certificates.pem", 487 | "refresh_interval": "600s" 488 | } 489 | } 490 | }, 491 | "server_listener_resource_name_template": "grpc/server?xds.resource.listening_address=%s", 492 | "client_default_listener_resource_name_template": "xdstp://traffic-director-global.xds.googleapis.com/envoy.config.listener.v3.Listener/123456789012345/mesh:testmesh/%s" 493 | }`, 494 | }, 495 | { 496 | desc: "ignore_resource_deletion and v3", 497 | input: configInput{ 498 | xdsServerURI: "example.com:443", 499 | gcpProjectNumber: 123456789012345, 500 | vpcNetworkName: "thedefault", 501 | ip: "10.9.8.7", 502 | zone: "uscentral-5", 503 | ignoreResourceDeletion: true, 504 | gitCommitHash: "7202b7c611ebd6d382b7b0240f50e9824200bffd", 505 | }, 506 | wantOutput: `{ 507 | "xds_servers": [ 508 | { 509 | "server_uri": "example.com:443", 510 | "channel_creds": [ 511 | { 512 | "type": "google_default" 513 | } 514 | ], 515 | "server_features": [ 516 | "xds_v3", 517 | "ignore_resource_deletion" 518 | ] 519 | } 520 | ], 521 | "authorities": { 522 | "traffic-director-c2p.xds.googleapis.com": { 523 | "xds_servers": [ 524 | { 525 | "server_uri": "dns:///directpath-pa.googleapis.com", 526 | "channel_creds": [ 527 | { 528 | "type": "google_default" 529 | } 530 | ], 531 | "server_features": [ 532 | "xds_v3", 533 | "ignore_resource_deletion" 534 | ] 535 | } 536 | ], 537 | "client_listener_resource_name_template": "xdstp://traffic-director-c2p.xds.googleapis.com/envoy.config.listener.v3.Listener/%s" 538 | }, 539 | "traffic-director-global.xds.googleapis.com": { 540 | "client_listener_resource_name_template": "xdstp://traffic-director-global.xds.googleapis.com/envoy.config.listener.v3.Listener/123456789012345/thedefault/%s" 541 | } 542 | }, 543 | "node": { 544 | "id": "projects/123456789012345/networks/thedefault/nodes/52fdfc07-2182-454f-963f-5f0f9a621d72", 545 | "cluster": "cluster", 546 | "metadata": { 547 | "INSTANCE_IP": "10.9.8.7", 548 | "TRAFFICDIRECTOR_GRPC_BOOTSTRAP_GENERATOR_SHA": "7202b7c611ebd6d382b7b0240f50e9824200bffd" 549 | }, 550 | "locality": { 551 | "zone": "uscentral-5" 552 | } 553 | }, 554 | "certificate_providers": { 555 | "google_cloud_private_spiffe": { 556 | "plugin_name": "file_watcher", 557 | "config": { 558 | "certificate_file": "certificates.pem", 559 | "private_key_file": "private_key.pem", 560 | "ca_certificate_file": "ca_certificates.pem", 561 | "refresh_interval": "600s" 562 | } 563 | } 564 | }, 565 | "server_listener_resource_name_template": "grpc/server?xds.resource.listening_address=%s", 566 | "client_default_listener_resource_name_template": "xdstp://traffic-director-global.xds.googleapis.com/envoy.config.listener.v3.Listener/123456789012345/thedefault/%s" 567 | }`, 568 | }, 569 | { 570 | desc: "happy path for allowed_grpc_services", 571 | input: configInput{ 572 | xdsServerURI: "example.com:443", 573 | gcpProjectNumber: 123456789012345, 574 | vpcNetworkName: "thedefault", 575 | ip: "10.9.8.7", 576 | zone: "uscentral-5", 577 | gitCommitHash: "7202b7c611ebd6d382b7b0240f50e9824200bffd", 578 | includeAllowedGrpcServices: true, 579 | }, 580 | wantOutput: `{ 581 | "xds_servers": [ 582 | { 583 | "server_uri": "example.com:443", 584 | "channel_creds": [ 585 | { 586 | "type": "google_default" 587 | } 588 | ], 589 | "server_features": [ 590 | "xds_v3" 591 | ] 592 | } 593 | ], 594 | "authorities": { 595 | "traffic-director-c2p.xds.googleapis.com": { 596 | "xds_servers": [ 597 | { 598 | "server_uri": "dns:///directpath-pa.googleapis.com", 599 | "channel_creds": [ 600 | { 601 | "type": "google_default" 602 | } 603 | ], 604 | "server_features": [ 605 | "xds_v3", 606 | "ignore_resource_deletion" 607 | ] 608 | } 609 | ], 610 | "client_listener_resource_name_template": "xdstp://traffic-director-c2p.xds.googleapis.com/envoy.config.listener.v3.Listener/%s" 611 | }, 612 | "traffic-director-global.xds.googleapis.com": { 613 | "client_listener_resource_name_template": "xdstp://traffic-director-global.xds.googleapis.com/envoy.config.listener.v3.Listener/123456789012345/thedefault/%s" 614 | } 615 | }, 616 | "node": { 617 | "id": "projects/123456789012345/networks/thedefault/nodes/52fdfc07-2182-454f-963f-5f0f9a621d72", 618 | "cluster": "cluster", 619 | "metadata": { 620 | "INSTANCE_IP": "10.9.8.7", 621 | "TRAFFICDIRECTOR_GRPC_BOOTSTRAP_GENERATOR_SHA": "7202b7c611ebd6d382b7b0240f50e9824200bffd" 622 | }, 623 | "locality": { 624 | "zone": "uscentral-5" 625 | } 626 | }, 627 | "certificate_providers": { 628 | "google_cloud_private_spiffe": { 629 | "plugin_name": "file_watcher", 630 | "config": { 631 | "certificate_file": "certificates.pem", 632 | "private_key_file": "private_key.pem", 633 | "ca_certificate_file": "ca_certificates.pem", 634 | "refresh_interval": "600s" 635 | } 636 | } 637 | }, 638 | "allowed_grpc_services": { 639 | "dns:///example.com:443": { 640 | "channel_creds": [ 641 | { 642 | "type": "google_default" 643 | } 644 | ] 645 | } 646 | }, 647 | "server_listener_resource_name_template": "grpc/server?xds.resource.listening_address=%s", 648 | "client_default_listener_resource_name_template": "xdstp://traffic-director-global.xds.googleapis.com/envoy.config.listener.v3.Listener/123456789012345/thedefault/%s" 649 | }`, 650 | }, 651 | } 652 | 653 | for _, test := range tests { 654 | t.Run(test.desc, func(t *testing.T) { 655 | uuid.SetRand(rand.New(rand.NewSource(1))) 656 | 657 | gotOutput, err := generate(test.input) 658 | if err != nil { 659 | t.Fatalf("generate(%+v) failed: %v", test.input, err) 660 | } 661 | if diff := cmp.Diff(test.wantOutput, string(gotOutput)); diff != "" { 662 | t.Fatalf("generate(%+v) returned output does not match expected (-want +got):\n%s", test.input, diff) 663 | } 664 | }) 665 | } 666 | } 667 | 668 | func TestGetZone(t *testing.T) { 669 | server := httptest.NewServer(nil) 670 | defer server.Close() 671 | overrideHTTP(server) 672 | want := "us-central5-c" 673 | http.HandleFunc("metadata.google.internal/computeMetadata/v1/instance/zone", 674 | func(w http.ResponseWriter, r *http.Request) { 675 | if r.Header.Get("Metadata-Flavor") != "Google" { 676 | http.Error(w, "Missing Metadata-Flavor", http.StatusForbidden) 677 | return 678 | } 679 | w.Write([]byte("projects/123456789012345/zones/us-central5-c")) 680 | }) 681 | got, err := getZone() 682 | if err != nil { 683 | t.Fatalf("want no error, got :%v", err) 684 | } 685 | if want != got { 686 | t.Fatalf("want %v, got: %v", want, got) 687 | } 688 | } 689 | 690 | func TestGetProjectId(t *testing.T) { 691 | server := httptest.NewServer(nil) 692 | defer server.Close() 693 | overrideHTTP(server) 694 | want := int64(123456789012345) 695 | http.HandleFunc("metadata.google.internal/computeMetadata/v1/project/numeric-project-id", 696 | func(w http.ResponseWriter, r *http.Request) { 697 | if r.Header.Get("Metadata-Flavor") != "Google" { 698 | http.Error(w, "Missing Metadata-Flavor", http.StatusForbidden) 699 | return 700 | } 701 | w.Write([]byte("123456789012345")) 702 | }) 703 | got, err := getProjectID() 704 | if err != nil { 705 | t.Fatalf("want no error, got :%v", err) 706 | } 707 | if want != got { 708 | t.Fatalf("want %v, got: %v", want, got) 709 | } 710 | } 711 | 712 | func TestGetClusterName(t *testing.T) { 713 | server := httptest.NewServer(nil) 714 | defer server.Close() 715 | overrideHTTP(server) 716 | want := "test-cluster" 717 | http.HandleFunc("metadata.google.internal/computeMetadata/v1/instance/attributes/cluster-name", 718 | func(w http.ResponseWriter, r *http.Request) { 719 | if r.Header.Get("Metadata-Flavor") != "Google" { 720 | http.Error(w, "Missing Metadata-Flavor", http.StatusForbidden) 721 | return 722 | } 723 | w.Write([]byte("test-cluster")) 724 | }) 725 | if got, _ := getClusterName(); got != want { 726 | t.Fatalf("getClusterName() = %s, want: %s", got, want) 727 | } 728 | } 729 | 730 | func TestGetClusterLocality(t *testing.T) { 731 | tests := []struct { 732 | desc string 733 | handler func(http.ResponseWriter, *http.Request) 734 | want string 735 | wantErr bool 736 | }{ 737 | { 738 | desc: "zonal_succeess", 739 | handler: func(w http.ResponseWriter, r *http.Request) { 740 | if r.Header.Get("Metadata-Flavor") != "Google" { 741 | http.Error(w, "Missing Metadata-Flavor", http.StatusForbidden) 742 | return 743 | } 744 | w.Write([]byte("us-west1-a")) 745 | }, 746 | want: "us-west1-a", 747 | }, 748 | { 749 | desc: "regional_succeess", 750 | handler: func(w http.ResponseWriter, r *http.Request) { 751 | if r.Header.Get("Metadata-Flavor") != "Google" { 752 | http.Error(w, "Missing Metadata-Flavor", http.StatusForbidden) 753 | return 754 | } 755 | w.Write([]byte("us-west1")) 756 | }, 757 | want: "us-west1", 758 | }, 759 | { 760 | desc: "no_response_from_server", 761 | handler: func(w http.ResponseWriter, r *http.Request) { 762 | http.Error(w, "Error", http.StatusForbidden) 763 | }, 764 | wantErr: true, 765 | }, 766 | } 767 | 768 | for _, tt := range tests { 769 | t.Run(tt.desc, func(t *testing.T) { 770 | mux := http.NewServeMux() 771 | mux.HandleFunc("metadata.google.internal/computeMetadata/v1/instance/attributes/cluster-location", tt.handler) 772 | server := httptest.NewServer(mux) 773 | defer server.Close() 774 | overrideHTTP(server) 775 | 776 | got, err := getClusterLocality() 777 | if (err != nil) != tt.wantErr { 778 | t.Fatalf("getClusterLocality() returned error: %s wantErr: %v", err, tt.wantErr) 779 | } 780 | if got != tt.want { 781 | t.Fatalf("getClusterLocality() = %s want: %s", got, tt.want) 782 | } 783 | }) 784 | } 785 | } 786 | 787 | func TestGetVMName(t *testing.T) { 788 | server := httptest.NewServer(nil) 789 | defer server.Close() 790 | overrideHTTP(server) 791 | want := "test-vm" 792 | http.HandleFunc("metadata.google.internal/computeMetadata/v1/instance/name", 793 | func(w http.ResponseWriter, r *http.Request) { 794 | if r.Header.Get("Metadata-Flavor") != "Google" { 795 | http.Error(w, "Missing Metadata-Flavor", http.StatusForbidden) 796 | return 797 | } 798 | w.Write([]byte("test-vm")) 799 | }) 800 | if got := getVMName(); got != want { 801 | t.Fatalf("getVMName() = %s, want: %s", got, want) 802 | } 803 | } 804 | 805 | func TestCheckIPv6Capable(t *testing.T) { 806 | tests := []struct { 807 | desc string 808 | httpHandler func(http.ResponseWriter, *http.Request) 809 | wantOutput bool 810 | }{ 811 | { 812 | desc: "v6 enabled", 813 | httpHandler: func(w http.ResponseWriter, r *http.Request) { 814 | if r.Header.Get("Metadata-Flavor") != "Google" { 815 | http.Error(w, "Missing Metadata-Flavor", http.StatusForbidden) 816 | return 817 | } 818 | w.Write([]byte("6970:7636:2061:6464:7265:7373:2062:6162")) 819 | }, 820 | wantOutput: true, 821 | }, 822 | { 823 | desc: "v6 not enabled", 824 | httpHandler: func(w http.ResponseWriter, r *http.Request) { 825 | http.Error(w, "Not Found", 404) 826 | }, 827 | wantOutput: false, 828 | }, 829 | } 830 | 831 | for _, test := range tests { 832 | t.Run(test.desc, func(t *testing.T) { 833 | mux := http.NewServeMux() 834 | mux.HandleFunc("metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/ipv6s", test.httpHandler) 835 | server := httptest.NewServer(mux) 836 | defer server.Close() 837 | overrideHTTP(server) 838 | if got := isIPv6Capable(); got != test.wantOutput { 839 | t.Fatalf("isIPv6Capable() = %t, want: %t", got, test.wantOutput) 840 | } 841 | 842 | }) 843 | } 844 | 845 | } 846 | 847 | func overrideHTTP(s *httptest.Server) { 848 | http.DefaultTransport = &http.Transport{ 849 | DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { 850 | return (&net.Dialer{}).DialContext(ctx, "tcp", s.Listener.Addr().String()) 851 | }, 852 | } 853 | } 854 | 855 | func Test_getQualifiedXdsUri(t *testing.T) { 856 | tests := []struct { 857 | name string 858 | xdsServerURI string 859 | want string 860 | }{ 861 | {"append when missing dns:", "example.com:123", "dns:///example.com:123"}, 862 | {"as is when contains dns:", "dns:///example.com:123", "dns:///example.com:123"}, 863 | } 864 | for _, tt := range tests { 865 | t.Run(tt.name, func(t *testing.T) { 866 | if got := getQualifiedXDSURI(tt.xdsServerURI); got != tt.want { 867 | t.Errorf("getQualifiedXdsUri() = %v, want %v", got, tt.want) 868 | } 869 | }) 870 | } 871 | } 872 | -------------------------------------------------------------------------------- /map_flag.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "sort" 21 | "strings" 22 | ) 23 | 24 | // stringMapVal implements the flag.Value interface and supports passing key 25 | // value pairs multiple times on the command line. 26 | type stringMapVal map[string]string 27 | 28 | func newStringMapVal(m *map[string]string) *stringMapVal { 29 | return (*stringMapVal)(m) 30 | } 31 | 32 | func (s *stringMapVal) Set(val string) error { 33 | parts := strings.SplitN(val, "=", 2) 34 | if len(parts) != 2 { 35 | return fmt.Errorf("%q is not formatted as key=value", val) 36 | } 37 | (*s)[parts[0]] = parts[1] 38 | return nil 39 | } 40 | 41 | func (s *stringMapVal) String() string { 42 | keys := make([]string, 0, len(*s)) 43 | for key := range *s { 44 | keys = append(keys, key) 45 | } 46 | sort.Strings(keys) 47 | var b bytes.Buffer 48 | for i, key := range keys { 49 | if i > 0 { 50 | b.WriteRune(',') 51 | } 52 | b.WriteString(key) 53 | b.WriteRune('=') 54 | b.WriteString((*s)[key]) 55 | } 56 | return b.String() 57 | } 58 | -------------------------------------------------------------------------------- /map_flag_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "flag" 19 | "io/ioutil" 20 | "testing" 21 | 22 | "github.com/google/go-cmp/cmp" 23 | "github.com/google/go-cmp/cmp/cmpopts" 24 | ) 25 | 26 | func TestStringMapVal(t *testing.T) { 27 | tests := []struct { 28 | desc string 29 | keyValues []string 30 | wantMap map[string]string 31 | wantErr bool 32 | }{ 33 | { 34 | desc: "badly formatted", 35 | keyValues: []string{"key:val"}, 36 | wantErr: true, 37 | }, 38 | { 39 | desc: "happy single", 40 | keyValues: []string{"key=val"}, 41 | wantMap: map[string]string{"key": "val"}, 42 | }, 43 | { 44 | desc: "happy multiple", 45 | keyValues: []string{"key1=val1", "key2=val2"}, 46 | wantMap: map[string]string{"key1": "val1", "key2": "val2"}, 47 | }, 48 | { 49 | desc: "happy with = in val", 50 | keyValues: []string{"key=val=1"}, 51 | wantMap: map[string]string{"key": "val=1"}, 52 | }, 53 | { 54 | desc: "happy with empty val", 55 | keyValues: []string{"key="}, 56 | wantMap: map[string]string{"key": ""}, 57 | }, 58 | } 59 | 60 | for _, test := range tests { 61 | t.Run(test.desc, func(t *testing.T) { 62 | sm := make(map[string]string) 63 | fs := flag.NewFlagSet("testStringMapVal", flag.ContinueOnError) 64 | fs.SetOutput(ioutil.Discard) 65 | fs.Var(newStringMapVal(&sm), "metadata", "") 66 | 67 | var cmdLine []string 68 | for _, kv := range test.keyValues { 69 | cmdLine = append(cmdLine, "-metadata", kv) 70 | } 71 | if err := fs.Parse(cmdLine); (err != nil) != test.wantErr { 72 | t.Fatalf("Parse(%v) returned err: %v, wantErr: %v", cmdLine, err, test.wantErr) 73 | } 74 | if test.wantErr { 75 | return 76 | } 77 | if !cmp.Equal(sm, test.wantMap, cmpopts.EquateEmpty()) { 78 | t.Fatalf("stringMap after Parse(%v) is: %v, want: %v", cmdLine, sm, test.wantMap) 79 | } 80 | }) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /tools/cloudbuild-artifacts.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | steps: 16 | - name: golang:1.24 17 | args: ['go', 'build', '.'] 18 | - name: golang:1.24 19 | args: ['go', 'test', './...', "-buildvcs=true"] 20 | - name: alpine 21 | args: ['./tools/package.sh', '$COMMIT_SHA'] 22 | - name: gcr.io/cloud-builders/docker 23 | args: [ 'build', '-t', 'us-docker.pkg.dev/grpc-testing/trafficdirector/td-grpc-bootstrap:${COMMIT_SHA}', '.' ] 24 | options: 25 | env: 26 | - CGO_ENABLED=0 27 | artifacts: 28 | objects: 29 | location: 'gs://grpc-td-builds/td-grpc-bootstrap/' 30 | paths: ['td-grpc-bootstrap-${COMMIT_SHA}.tar.gz'] 31 | images: ['us-docker.pkg.dev/grpc-testing/trafficdirector/td-grpc-bootstrap:${COMMIT_SHA}'] 32 | -------------------------------------------------------------------------------- /tools/package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Copyright 2020 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | set -e 17 | 18 | if [[ $# -ne 1 ]]; then 19 | echo "Usage: $0 VERSION" 20 | echo "" 21 | echo "Expected to be run from the root of the repo" 22 | exit 1 23 | fi 24 | 25 | version="$1" 26 | mkdir "td-grpc-bootstrap-${version}/" 27 | cp td-grpc-bootstrap "td-grpc-bootstrap-${version}/" 28 | tar czf "td-grpc-bootstrap-${version}.tar.gz" "td-grpc-bootstrap-${version}/" 29 | rm -r "td-grpc-bootstrap-${version}/" 30 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | "runtime/debug" 20 | ) 21 | 22 | // getCommitID returns the commit ID of the current build, or an error if it 23 | // cannot be determined. It reads the "vcs.revision" setting from the 24 | // runtime.BuildInfo and returns that value. 25 | func getCommitID() (string, error) { 26 | info, ok := debug.ReadBuildInfo() 27 | if !ok { 28 | return "", fmt.Errorf("error calling debug.ReadBuildInfo") 29 | } 30 | for _, setting := range info.Settings { 31 | if setting.Key == "vcs.revision" { 32 | return setting.Value, nil 33 | } 34 | } 35 | return "", fmt.Errorf("BuildInfo.Settings is missing vcs.revision") 36 | } 37 | -------------------------------------------------------------------------------- /version_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "regexp" 19 | "testing" 20 | ) 21 | 22 | func TestGetCommitId(t *testing.T) { 23 | commitID, err := getCommitID() 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | re := regexp.MustCompile(`^[a-f0-9]{40}$`) 29 | if !re.MatchString(commitID) { 30 | t.Fatalf("getCommitId(): returned an invalid commit ID: %q. Want commit ID to be a valid SHA1 hash.", commitID) 31 | } 32 | } 33 | --------------------------------------------------------------------------------