Skip to content

Commit a8a9b9e

Browse files
authored
golang extension: implement GetAllHeaders in the cluster_specifier plugin. (envoyproxy#39094)
Fixes: envoyproxy#38644 - This is my first time working with Envoy and Cgo so there may be better ways to implement this. I used [copyHeaderMapToGo](https://github.com/envoyproxy/envoy/blob/61809bc1377543e3e6493157ab8fc96070867179/contrib/golang/upstreams/http/tcp/source/upstream_request.cc#L384-L414) as a reference for this implementation. ntegration testing - I would prefer to add some unit testing as well for the Go -> Cgo code, but I didn't see existing examples of this so I wasn't sure if it's common practice. Signed-off-by: Kyle O'Connor <[email protected]>
1 parent 5b9a855 commit a8a9b9e

File tree

8 files changed

+155
-0
lines changed

8 files changed

+155
-0
lines changed

contrib/golang/router/cluster_specifier/source/cgo.cc

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ absl::string_view referGoString(void* str) {
2525
extern "C" {
2626
#endif
2727

28+
// Assigns the number of request headers and their total byte size to the provided uint64 pointers.
29+
void envoyGoClusterSpecifierGetNumHeadersAndByteSize(unsigned long long header_ptr,
30+
void* header_num, void* byte_size) {
31+
auto header = reinterpret_cast<Http::RequestHeaderMap*>(header_ptr);
32+
auto go_header_num = reinterpret_cast<GoUint64*>(header_num);
33+
auto go_byte_size = reinterpret_cast<GoUint64*>(byte_size);
34+
*go_header_num = header->size();
35+
*go_byte_size = header->byteSize();
36+
}
37+
2838
// Get the value of the specified header key from the request header map.
2939
// Only use the first value when there are multiple values associated with the key.
3040
int envoyGoClusterSpecifierGetHeader(unsigned long long header_ptr, void* key, void* value) {
@@ -42,6 +52,41 @@ int envoyGoClusterSpecifierGetHeader(unsigned long long header_ptr, void* key, v
4252
return static_cast<int>(GetHeaderResult::Mising);
4353
}
4454

55+
// Copies the request headers from `header_ptr` into the provided buffer `buf` and populates `strs`
56+
// with the string metadata of each key and value. `strs` points into `buf` which stores
57+
// concatenated raw header bytes in the format `key1val1key2val2...`. Note the buffer should
58+
// be allocated with sufficient memory to store the headers and `strs` is expected to be of length
59+
// twice the number of headers.
60+
void envoyGoClusterSpecifierGetAllHeaders(unsigned long long header_ptr, void* strs, void* buf) {
61+
auto headers = reinterpret_cast<Http::RequestHeaderMap*>(header_ptr);
62+
auto go_strs = reinterpret_cast<GoString*>(strs);
63+
auto go_buf = reinterpret_cast<char*>(buf);
64+
auto i = 0;
65+
headers->iterate(
66+
[&i, &go_strs, &go_buf](const Http::HeaderEntry& header) -> Http::HeaderMap::Iterate {
67+
auto key = header.key().getStringView();
68+
auto value = header.value().getStringView();
69+
70+
auto len = key.length();
71+
go_strs[i].n = len;
72+
go_strs[i].p = go_buf;
73+
// go_buf is allocated in go heap memory with sufficient space for all headers.
74+
memcpy(go_buf, key.data(), len); // NOLINT(safe-memcpy)
75+
go_buf += len;
76+
i++;
77+
78+
len = value.length();
79+
go_strs[i].n = len;
80+
if (len > 0) {
81+
go_strs[i].p = go_buf;
82+
memcpy(go_buf, value.data(), len); // NOLINT(safe-memcpy)
83+
go_buf += len;
84+
}
85+
i++;
86+
return Http::HeaderMap::Iterate::Continue;
87+
});
88+
}
89+
4590
// Log the message with the error level.
4691
void envoyGoClusterSpecifierLogError(unsigned long long plugin_ptr, void* msg) {
4792
auto msgStr = referGoString(msg);

contrib/golang/router/cluster_specifier/source/go/pkg/api/capi.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@ package api
1919

2020
type HttpCAPI interface {
2121
HttpGetHeader(headerPtr uint64, key *string, value *string) bool
22+
HttpGetAllHeaders(headerPtr uint64) map[string][]string
2223
HttpLogError(pluginPtr uint64, msg *string)
2324
}

contrib/golang/router/cluster_specifier/source/go/pkg/api/cluster.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,7 @@ type RequestHeaderMap interface {
3737
// Get value of key
3838
// If multiple values associated with this key, first one will be returned.
3939
Get(key string) (string, bool)
40+
41+
// Get all the request headers mapped to their corresponding values.
42+
GetAllHeaders() map[string][]string
4043
}

contrib/golang/router/cluster_specifier/source/go/pkg/cluster_specifier/api.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66
extern "C" {
77
#endif
88

9+
void envoyGoClusterSpecifierGetNumHeadersAndByteSize(unsigned long long header_ptr,
10+
void* header_num, void* byte_size);
911
int envoyGoClusterSpecifierGetHeader(unsigned long long header_ptr, void* key, void* value);
1012
void envoyGoClusterSpecifierLogError(unsigned long long plugin_ptr, void* msg);
13+
void envoyGoClusterSpecifierGetAllHeaders(unsigned long long header_ptr, void* strs, void* buf);
1114

1215
#ifdef __cplusplus
1316
} // extern "C"

contrib/golang/router/cluster_specifier/source/go/pkg/cluster_specifier/capi_impl.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ package cluster_specifier
3030
*/
3131
import "C"
3232
import (
33+
"runtime"
3334
"unsafe"
3435

3536
"github.com/envoyproxy/envoy/contrib/golang/router/cluster_specifier/source/go/pkg/api"
@@ -44,6 +45,34 @@ func (c *httpCApiImpl) HttpGetHeader(headerPtr uint64, key *string, value *strin
4445
return int(found) == foundHeaderValue
4546
}
4647

48+
func (c *httpCApiImpl) HttpGetAllHeaders(headerPtr uint64) map[string][]string {
49+
var headerNum uint64
50+
var headerBytes uint64
51+
C.envoyGoClusterSpecifierGetNumHeadersAndByteSize(C.ulonglong(headerPtr), unsafe.Pointer(&headerNum), unsafe.Pointer(&headerBytes))
52+
53+
m := make(map[string][]string, headerNum)
54+
if headerNum == 0 {
55+
return m
56+
}
57+
58+
strs := make([]string, headerNum*2)
59+
buf := make([]byte, headerBytes)
60+
C.envoyGoClusterSpecifierGetAllHeaders(C.ulonglong(headerPtr), unsafe.Pointer(unsafe.SliceData(strs)), unsafe.Pointer(unsafe.SliceData(buf)))
61+
62+
for i := uint64(0); i < headerNum*2; i += 2 {
63+
key := strs[i]
64+
value := strs[i+1]
65+
66+
if v, found := m[key]; !found {
67+
m[key] = []string{value}
68+
} else {
69+
m[key] = append(v, value)
70+
}
71+
}
72+
runtime.KeepAlive(buf)
73+
return m
74+
}
75+
4776
func (c *httpCApiImpl) HttpLogError(pluginPtr uint64, msg *string) {
4877
C.envoyGoClusterSpecifierLogError(C.ulonglong(pluginPtr), unsafe.Pointer(msg))
4978
}

contrib/golang/router/cluster_specifier/source/go/pkg/cluster_specifier/type.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,7 @@ func (h *httpHeaderMap) Get(key string) (string, bool) {
3232
found := cAPI.HttpGetHeader(h.headerPtr, &key, &value)
3333
return value, found
3434
}
35+
36+
func (h *httpHeaderMap) GetAllHeaders() map[string][]string {
37+
return cAPI.HttpGetAllHeaders(h.headerPtr)
38+
}

contrib/golang/router/cluster_specifier/test/golang_integration_test.cc

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,59 @@ TEST_F(GolangClusterSpecifierIntegrationTest, Panic_Unknown) {
201201
cleanupUpstreamAndDownstream();
202202
}
203203

204+
// Go plugin return cluster: "cluster_unknown"
205+
TEST_F(GolangClusterSpecifierIntegrationTest, GetAllHeaders_UnknownCluster) {
206+
auto so_id = "simple";
207+
auto yaml_string = absl::StrFormat(yaml_fmt, so_id, genSoPath(so_id), "");
208+
initializeRoute(yaml_string);
209+
210+
codec_client_ = makeHttpConnection(lookupPort("http"));
211+
212+
Http::TestRequestHeaderMapImpl request_headers{
213+
{":method", "GET"}, {":path", "/test"}, {":scheme", "http"},
214+
{":authority", "test.com"}, {"custom_header_1", "true"}, {"custom_header_1", "false"}};
215+
216+
// custom_header_1 has at least one "true" value which will result in "cluster_unknown" being
217+
// returned.
218+
auto response = codec_client_->makeHeaderOnlyRequest(request_headers);
219+
220+
ASSERT_TRUE(response->waitForEndStream());
221+
EXPECT_TRUE(response->complete());
222+
EXPECT_THAT(response->headers(), Http::HttpStatusIs("503"));
223+
224+
cleanupUpstreamAndDownstream();
225+
}
226+
227+
// Go plugin choose cluster: "cluster_0"
228+
TEST_F(GolangClusterSpecifierIntegrationTest, GetAllHeaders_DefaultCluster) {
229+
auto so_id = "simple";
230+
auto yaml_string = absl::StrFormat(yaml_fmt, so_id, genSoPath(so_id), "");
231+
initializeRoute(yaml_string);
232+
233+
codec_client_ = makeHttpConnection(lookupPort("http"));
234+
235+
Http::TestResponseHeaderMapImpl response_headers{
236+
{"server", "envoy"},
237+
{":status", "200"},
238+
};
239+
240+
Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"},
241+
{":path", "/test"},
242+
{":scheme", "http"},
243+
{":authority", "test.com"},
244+
{"custom_header_1", "false"}};
245+
246+
// custom_header_1 has a single value set to "false" which will result in "cluster_0" being
247+
// returned.
248+
auto response = sendRequestAndWaitForResponse(request_headers, 0, response_headers, 0);
249+
250+
ASSERT_TRUE(response->waitForEndStream());
251+
EXPECT_TRUE(response->complete());
252+
EXPECT_EQ(response->headers().getStatusValue(), "200");
253+
254+
cleanupUpstreamAndDownstream();
255+
}
256+
204257
} // namespace
205258
} // namespace Golang
206259
} // namespace Router

contrib/golang/router/cluster_specifier/test/test_data/simple/plugin.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,19 @@ type clusterSpecifier struct {
1212
panicPrefix string
1313
}
1414

15+
func headerHasTrueValue(headers map[string][]string, key string) bool {
16+
values, ok := headers[key]
17+
if !ok {
18+
return false
19+
}
20+
for _, element := range values {
21+
if element == "true" {
22+
return true
23+
}
24+
}
25+
return false
26+
}
27+
1528
func (s *clusterSpecifier) Cluster(header api.RequestHeaderMap) string {
1629
path, _ := header.Get(":path")
1730

@@ -30,5 +43,9 @@ func (s *clusterSpecifier) Cluster(header api.RequestHeaderMap) string {
3043
panic("test")
3144
}
3245

46+
if headerHasTrueValue(header.GetAllHeaders(), "custom_header_1") {
47+
return "cluster_unknown"
48+
}
49+
3350
return "cluster_0"
3451
}

0 commit comments

Comments
 (0)