├── .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 |
--------------------------------------------------------------------------------