diff --git a/proxy/http/remap/Makefile.am b/proxy/http/remap/Makefile.am index 7d20677bbb6..2731e1491ad 100644 --- a/proxy/http/remap/Makefile.am +++ b/proxy/http/remap/Makefile.am @@ -28,13 +28,16 @@ AM_CPPFLAGS += \ -I$(abs_top_srcdir)/proxy/hdrs \ -I$(abs_top_srcdir)/proxy/shared \ -I$(abs_top_srcdir)/proxy/http \ - $(TS_INCLUDES) + $(TS_INCLUDES) \ + @YAMLCPP_INCLUDES@ noinst_LIBRARIES = libhttp_remap.a libhttp_remap_a_SOURCES = \ AclFiltering.cc \ AclFiltering.h \ + NextHopConfig.h \ + NextHopConfig.cc \ RemapConfig.cc \ RemapConfig.h \ RemapPluginInfo.cc \ @@ -50,5 +53,26 @@ libhttp_remap_a_SOURCES = \ UrlRewrite.cc \ UrlRewrite.h +check_PROGRAMS = \ + test_NextHopConfig + +TESTS = $(check_PROGRAMS) + +test_NextHopConfig_CPPFLAGS = $(AM_CPPFLAGS)\ + -I$(abs_top_srcdir)/tests/include \ + -I$(abs_top_srcdir)/lib/tsconfig \ + $(TS_INCLUDES) \ + @YAMLCPP_INCLUDES@ + +test_NextHopConfig_SOURCES = \ + unit-tests/test_NextHopConfig.cc + +test_NextHopConfig_LDADD = \ + $(top_builddir)/proxy/http/remap/libhttp_remap.a \ + $(top_builddir)/lib/tsconfig/libtsconfig.la \ + $(top_builddir)/lib/tsconfig/libtsconfig.la \ + $(top_builddir)/src/tscore/libtscore.la \ + $(top_builddir)/lib/yamlcpp/libyamlcpp.la + clang-tidy-local: $(libhttp_remap_a_SOURCES) $(CXX_Clang_Tidy) diff --git a/proxy/http/remap/NextHopConfig.cc b/proxy/http/remap/NextHopConfig.cc new file mode 100644 index 00000000000..c38bbb8206e --- /dev/null +++ b/proxy/http/remap/NextHopConfig.cc @@ -0,0 +1,379 @@ +/** @file + + Nexhop parent selection strategy yaml parser + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#include "NextHopConfig.h" + +#include +#include +#include +#include +#include + +#include "tscore/Diags.h" + +ts::Errata +NextHopConfig::loadConfig(const char *fileName) +{ + std::stringstream doc; + std::unordered_set include_once; + + try { + loadFile(fileName, doc, include_once); + YAML::Node node = YAML::Load(doc); + config = node.as(); + + } catch (std::exception &ex) { + errata.push(ts::Errata::Message(1, 1, ex.what())); + return errata; + } + return errata; +} + +void +NextHopConfig::loadFile(const std::string fileName, std::stringstream &doc, std::unordered_set &include_once) +{ + const char *sep = " \t"; + char *tok, *last; + std::string line; + + std::ifstream file(fileName); + if (file.is_open()) { + while (std::getline(file, line)) { + if (line[0] == '#') { + tok = strtok_r(const_cast(line.c_str()), sep, &last); + if (tok != nullptr && strcmp(tok, "#include") == 0) { + std::string f = strtok_r(nullptr, sep, &last); + if (include_once.find(f) == include_once.end()) { + include_once.insert(f); + loadFile(f, doc, include_once); + } + } + } else { + doc << line << "\n"; + } + } + file.close(); + } else { + throw std::runtime_error("unable to load '" + fileName + "'"); + } +} + +namespace YAML +{ +// decodes a 'NextHopStrategyConfig' type. +template <> struct convert { + static bool + decode(const Node &node, NextHopStrategyConfig &cfg) + { + YAML::Node strategy; + try { + strategy = node[NH_strategy]; + } catch (std::exception &ex) { + throw std::runtime_error("the required 'strategy' node does not exist in the yaml document."); + } + if (strategy.IsMap()) { + // verify keys + for (auto const &item : strategy) { + if (std::none_of(valid_strategy_keys.begin(), valid_strategy_keys.end(), + [item](std::string s) { return s == item.first.Scalar(); })) { + throw std::invalid_argument("unsupported strategy key: " + item.first.Scalar()); + } + } + std::string policy; + try { + policy = strategy[NH_policy].Scalar(); + } catch (std::exception &ex) { + throw std::invalid_argument("required 'policy' field was not found"); + } + cfg.policy = static_cast(POLICY_DESCRIPTOR.get(policy)); + if (cfg.policy < 0) { + throw std::invalid_argument("unknown policy value '" + policy + "'"); + } + + // parse the groups list. + YAML::Node groups; + try { + groups = strategy[NH_groups]; + } catch (std::exception &ex) { + throw std::invalid_argument("the required 'groups' node is not defined in the strategy."); + } + if (!groups.IsSequence()) { + throw std::invalid_argument("the 'groups' node is not a sequence."); + } else { + cfg.groups.reserve(groups.size()); + for (unsigned int i = 0; i < groups.size(); i++) { + YAML::Node hostList = groups[i]; + if (!hostList.IsSequence()) { + throw std::invalid_argument("the 'hostsList' node in the group list is not a sequence."); + } + // process the hostList + std::vector v; + for (unsigned int j = 0; j < hostList.size(); j++) { + NextHopHost h = hostList[j].as(); + v.push_back(h); + } + cfg.groups.push_back(v); + } + } + + // hash_key is optional. + std::string hashKey; + try { + hashKey = strategy[NH_hashKey].Scalar(); + } catch (std::exception &ex) { + ; + } + if (!hashKey.empty()) { + cfg.hashKey = static_cast(HASH_KEY_DESCRIPTOR.get(hashKey)); + if (cfg.hashKey < 0) { + throw std::invalid_argument("unknown hash_key value '" + hashKey + "'"); + } + } + + // protocol is optional. + std::string protocol; + try { + protocol = strategy[NH_protocol].Scalar(); + } catch (std::exception &ex) { + ; + } + if (!protocol.empty()) { + cfg.protocol = static_cast(PROTOCOL_DESCRIPTOR.get(protocol)); + if (cfg.protocol < 0) { + throw std::invalid_argument("unknown protocol value '" + protocol + "'"); + } + } + + // failover is optional + YAML::Node failover; + try { + failover = strategy[NH_failover]; + } catch (std::exception &ex) { + ; + } + if (!failover.IsNull()) { + if (failover.IsMap()) { + // verify the keys + for (auto const &item : failover) { + if (std::none_of(valid_failover_keys.begin(), valid_failover_keys.end(), + [item](std::string s) { return s == item.first.Scalar(); })) { + throw std::invalid_argument("unsupported failover key: " + item.first.Scalar()); + } + } + cfg.failover = failover.as(); + } else { + throw std::invalid_argument("'failover' is not a map in thhis strategy"); + } + } + } else { + throw std::invalid_argument("the required 'strategy' node does not exist in the yaml document."); + } + + return true; + } +}; + +// decodes a 'NextHopFailOver' type. +template <> struct convert { + static bool + decode(const Node &node, NextHopFailOver &fo) + { + // parse the ring mode + std::string ringMode; + try { + ringMode = node[NH_ringMode].Scalar(); + } catch (std::exception &ex) { + throw std::invalid_argument("the required 'ring_mode' setting is not present in the 'failover' map."); + } + if (!ringMode.empty()) { + fo.ringMode = static_cast(RING_MODE_DESCRIPTOR.get(ringMode)); + if (fo.ringMode < 0) { + throw std::invalid_argument("unknown ring_mode value '" + ringMode + "'"); + } + } + + // parse optional response_codes. + YAML::Node responseCodes; + try { + responseCodes = node[NH_responseCodes]; + } catch (std::exception &ex) { + ; + } + if (!responseCodes.IsNull()) { + if (responseCodes.IsSequence()) { + for (unsigned int i = 0; i < responseCodes.size(); i++) { + int code; + try { + code = responseCodes[i].as(); + } catch (std::exception &ex) { + throw std::invalid_argument("invalid response code value, not an 'int'"); + } + fo.responseCodes.push_back(code); + } + } else { + throw std::invalid_argument("the 'response_codes' node is not a sequence."); + } + } + + // parse the health check list. + YAML::Node healthCheck; + try { + healthCheck = node[NH_health_Check]; + } catch (std::exception &ex) { + throw std::invalid_argument("the required 'health_check' node is not defined in 'failover."); + } + if (!healthCheck.IsNull()) { + if (healthCheck.IsSequence()) { + for (unsigned int i = 0; i < healthCheck.size(); i++) { + std::string value = healthCheck[i].Scalar(); + NextHopHealthCheck hc = static_cast(HEALTH_CHECK_DESCRIPTOR.get(value)); + if (hc < 0) { + throw std::invalid_argument("invalid health check value '" + value + "'"); + } + fo.healthChecks.push_back(hc); + } + } else { + throw std::invalid_argument("the 'health_check' node is not a sequence."); + } + } + return true; + } +}; + +// decodes a 'NextHopHost' type +template <> struct convert { + static bool + decode(const Node &node, NextHopHost &nh) + { + bool ext_found = false; + YAML::Node nd; + + if (!node.IsMap()) { + throw std::invalid_argument("the 'host' node is not a map"); + } else { + // check for the use of an alias extension. + try { + nd = node[NH_alias_extension]; + ext_found = true; + } catch (std::exception &ex) { + nd = node; + } + + // verify the keys + for (auto const &item : nd) { + if (std::none_of(valid_host_keys.begin(), valid_host_keys.end(), + [item](std::string s) { return s == item.first.Scalar(); })) { + throw std::invalid_argument("unsupported host key: " + item.first.Scalar()); + } + } + } + + // parse the host field + try { + std::string host = nd[NH_host].Scalar(); + nh.host = host; + } catch (std::exception &ex) { + throw std::invalid_argument("the required 'host' field is missing in the 'hosts' list."); + } + // parse the health check url field. + std::string healthCheckUrl; + try { + healthCheckUrl = nd[NH_healthCheck][NH_url].Scalar(); + nh.healthCheckUrl = healthCheckUrl; + } catch (std::exception &ex) { + throw std::invalid_argument("the required 'healthcheck' 'url' field is missing for a host in the 'hosts' list."); + } + + // parse the host protocols sequence + YAML::Node protocols; + try { + protocols = nd[NH_protocol]; + } catch (std::exception &ex) { + throw std::invalid_argument("the required 'protocol' sequence field is missing for a host in the 'hosts' list."); + } + if (protocols.IsSequence()) { + for (unsigned int i = 0; i < protocols.size(); i++) { + NextHopHostProtocols np = protocols[i].as(); + nh.protocols.push_back(np); + } + } else { + throw std::invalid_argument("the 'protocol' field is not a sequence for a host in the 'hosts' list."); + } + + // if an alias extension is found, look for the weight field, otherwise defaults are used. + if (ext_found) { + double weight = 0; + try { + weight = node[NH_weight].as(); + nh.weight = weight; + } catch (std::exception &ex) { + throw std::invalid_argument("the required 'weight' field is missing for a host in the 'hosts' list."); + } + } + + return true; + } +}; + +// decodes a 'NextHopHostProtocols' type. +template <> struct convert { + static bool + decode(const Node &node, NextHopHostProtocols &np) + { + bool port_found = true; + + // look for http port. + unsigned int http_port = 0; + unsigned int https_port = 0; + + // look for http port + try { + http_port = node[NH_http].as(); + port_found = true; + np.protocol = NH_http; + np.port = http_port; + } catch (std::exception &ex) { + port_found = false; + } + + // if not an http port, look for https port + if (!port_found) { + try { + https_port = node[NH_https].as(); + np.protocol = NH_https; + np.port = https_port; + port_found = true; + } catch (std::exception &ex) { + port_found = false; + } + } + + if (!port_found) { + throw std::invalid_argument("no protocol or port found for a 'host' in the host list."); + } + + return true; + } +}; + +} // namespace YAML diff --git a/proxy/http/remap/NextHopConfig.h b/proxy/http/remap/NextHopConfig.h new file mode 100644 index 00000000000..851bf4a0987 --- /dev/null +++ b/proxy/http/remap/NextHopConfig.h @@ -0,0 +1,130 @@ +/** @file + + A brief file description + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +#include "tscore/EnumDescriptor.h" +#include "tsconfig/Errata.h" + +enum NextHopSelectionPolicy { POLICY_UNDEFINED = 0, CONSISTENT_HASH, FIRST_LIVE, RR_STRICT, RR_IP, LATCHED }; +static TsEnumDescriptor POLICY_DESCRIPTOR = { + {{"consistent_hash", 1}, {"first_live", 2}, {"rr_strict", 3}, {"rr_ip", 4}, {"latched", 5}}}; + +enum NextHopHashKey { + KEY_UNDEFINED = 0, + CACHE_KEY, + URI, + URL, + HOSTNAME, + PATH, + PATH_QUERY, + PATH_FRAGMENT, +}; +static TsEnumDescriptor HASH_KEY_DESCRIPTOR = { + {{"cache_key", 1}, {"uri", 2}, {"url", 3}, {"hostname", 4}, {"path", 5}, {"path+query", 6}, {"path+fragment", 7}}}; + +enum NextHopProtocol { PROTOCOL_UNDEFINED = 0, HTTP, HTTPS }; +static TsEnumDescriptor PROTOCOL_DESCRIPTOR = {{{"http", 1}, {"https", 2}}}; + +enum NextHopRingMode { RING_MODE_UNDEFINED = 0, ALTERNATE_RINGS, EXHAUST_RINGS }; +static TsEnumDescriptor RING_MODE_DESCRIPTOR = {{{"alternate_rings", 1}, {"exhaust_rings", 2}}}; + +enum NextHopHealthCheck { HEALTH_UNDEFINED = 0, ACTIVE, PASSIVE }; +static TsEnumDescriptor HEALTH_CHECK_DESCRIPTOR = {{{"active", 1}, {"passive", 2}}}; + +// strategy keys +constexpr char NH_alias_extension[] = "<<"; +constexpr char NH_strategy[] = "strategy"; +constexpr char NH_policy[] = "policy"; +constexpr char NH_hashKey[] = "hash_key"; +constexpr char NH_groups[] = "groups"; +constexpr char NH_protocol[] = "protocol"; +constexpr char NH_failover[] = "failover"; +// failover keys +constexpr char NH_ringMode[] = "ring_mode"; +constexpr char NH_responseCodes[] = "response_codes"; +constexpr char NH_health_Check[] = "health_check"; +// host keys +constexpr char NH_host[] = "host"; +constexpr char NH_healthCheck[] = "healthcheck"; +constexpr char NH_url[] = "url"; +constexpr char NH_weight[] = "weight"; +constexpr char NH_http[] = "http"; +constexpr char NH_https[] = "https"; + +// valid strategy keys +static std::set valid_strategy_keys = {NH_policy, NH_hashKey, NH_groups, NH_protocol, NH_failover}; + +// valid failover keys +static std::set valid_failover_keys = {NH_ringMode, NH_responseCodes, NH_health_Check}; + +// valid host keys +static std::set valid_host_keys = {NH_alias_extension, NH_host, NH_protocol, NH_healthCheck, NH_weight}; + +struct NextHopHostProtocols { + NextHopHostProtocols() : port(0) {} + std::string protocol; + unsigned int port; +}; + +struct NextHopHost { + NextHopHost() : weight(1.0) {} + std::string host; + std::string healthCheckUrl; + std::vector protocols; + double weight; +}; + +struct NextHopFailOver { + NextHopRingMode ringMode; + std::vector responseCodes; + std::vector healthChecks; +}; + +struct NextHopStrategyConfig { + NextHopStrategyConfig() : policy{POLICY_UNDEFINED}, hashKey{PATH}, protocol{HTTP} {} + NextHopSelectionPolicy policy; + NextHopHashKey hashKey; + NextHopProtocol protocol; + NextHopFailOver failover; + + std::vector> groups; + ts::Errata errata; +}; + +class NextHopConfig +{ + void loadFile(const std::string fileName, std::stringstream &buf, std::unordered_set &include_once); + ts::Errata errata; + +public: + NextHopConfig() {} + + NextHopStrategyConfig config; + ts::Errata loadConfig(const char *fileName); +}; diff --git a/proxy/http/remap/unit-tests/bad_include.yaml b/proxy/http/remap/unit-tests/bad_include.yaml new file mode 100644 index 00000000000..49c09cabf37 --- /dev/null +++ b/proxy/http/remap/unit-tests/bad_include.yaml @@ -0,0 +1,41 @@ +#/** @file +# +# A brief file description +# +# @section license License +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# */ +# +# bad_include.yaml +# +#include ourhosts.yaml +strategy: + policy: consistent_hash + hash_key: url + groups: + - <<: *g1 + - <<: *g2 + protocol: http + failover: + ring_mode: exhaust_ring + response_codes: + - 404 + - 503 + health_check: + - passive + diff --git a/proxy/http/remap/unit-tests/combined.yaml b/proxy/http/remap/unit-tests/combined.yaml new file mode 100644 index 00000000000..d72300ae5d6 --- /dev/null +++ b/proxy/http/remap/unit-tests/combined.yaml @@ -0,0 +1,80 @@ +#/** @file +# +# A brief file description +# +# @section license License +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# */ +# +# combined.yaml +# +hosts: + p1: &p1 + host: p1-cache.foo.com + protocol: + - http: 80 + - https: 443 + healthcheck: + url: tcp://192.168.1.1:80 + p2: &p2 + host: p2-cache.foo.com + protocol: + - http: 8080 + - https: 8443 + healthcheck: + url: tcp://192.168.1.2:80 + s1: &s1 + host: s1-cache.bar.com + protocol: + - http: 80 + - https: 443 + healthcheck: + url: tcp://192.168.2.1:80 + s2: &s2 + host: s2-cache.bar.com + protocol: + - http: 8080 + - https: 8443 + healthcheck: + url: tcp://192.168.2.2:80 +groups: + - g1: &g1 + - weight: 1.0 + <<: *p1 + - weight: 2.0 + <<: *p2 + - g2: &g2 + - weight: 0.1 + <<: *s1 + - weight: 0.9 + <<: *s2 +strategy: + policy: consistent_hash + hash_key: path+query + groups: + - *g1 + - *g2 + protocol: http + failover: + ring_mode: exhaust_rings + response_codes: + - 404 + - 503 + health_check: + - passive + diff --git a/proxy/http/remap/unit-tests/combined_no_weight_alias_extension.yaml b/proxy/http/remap/unit-tests/combined_no_weight_alias_extension.yaml new file mode 100644 index 00000000000..2b7ad2de51f --- /dev/null +++ b/proxy/http/remap/unit-tests/combined_no_weight_alias_extension.yaml @@ -0,0 +1,76 @@ +#/** @file +# +# A brief file description +# +# @section license License +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# */ +# +# combined_no_weight_alias_extension.yaml +# +hosts: + p1: &p1 + host: p1-cache.foo.com + protocol: + - http: 80 + - https: 443 + healthcheck: + url: tcp://192.168.1.1:80 + p2: &p2 + host: p2-cache.foo.com + protocol: + - http: 8080 + - https: 8443 + healthcheck: + url: tcp://192.168.1.2:80 + s1: &s1 + host: s1-cache.bar.com + protocol: + - http: 80 + - https: 443 + healthcheck: + url: tcp://192.168.2.1:80 + s2: &s2 + host: s2-cache.bar.com + protocol: + - http: 8080 + - https: 8443 + healthcheck: + url: tcp://192.168.2.2:80 +groups: + - g1: &g1 + - *p1 + - *p2 + - g2: &g2 + - *s1 + - *s2 +strategy: + policy: consistent_hash + hash_key: path+query + groups: + - *g1 + - *g2 + protocol: http + failover: + ring_mode: exhaust_rings + response_codes: + - 404 + - 503 + health_check: + - passive + diff --git a/proxy/http/remap/unit-tests/hosts.yaml b/proxy/http/remap/unit-tests/hosts.yaml new file mode 100644 index 00000000000..c6722a18444 --- /dev/null +++ b/proxy/http/remap/unit-tests/hosts.yaml @@ -0,0 +1,65 @@ +#/** @file +# +# A brief file description +# +# @section license License +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# */ +# +# good_hosts.yaml +# +hosts: + p1: &p1 + host: p1-cache.foo.com + protocol: + - http: 80 + - https: 443 + healthcheck: + url: tcp://192.168.1.1:80 + p2: &p2 + host: p2-cache.foo.com + protocol: + - http: 8080 + - https: 8443 + healthcheck: + url: tcp://192.168.1.2:80 + s1: &s1 + host: s1-cache.bar.com + protocol: + - http: 80 + - https: 443 + healthcheck: + url: tcp://192.168.2.1:80 + s2: &s2 + host: s2-cache.bar.com + protocol: + - http: 8080 + - https: 8443 + healthcheck: + url: tcp://192.168.2.2:80 +groups: + - g1: &g1 + - weight: 1.0 + <<: *p1 + - weight: 2.0 + <<: *p2 + - g2: &g2 + - weight: 0.1 + <<: *s1 + - weight: 0.9 + <<: *s2 diff --git a/proxy/http/remap/unit-tests/strategy.yaml b/proxy/http/remap/unit-tests/strategy.yaml new file mode 100644 index 00000000000..06a77d80feb --- /dev/null +++ b/proxy/http/remap/unit-tests/strategy.yaml @@ -0,0 +1,42 @@ +#/** @file +# +# A brief file description +# +# @section license License +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# */ +# +# strategy.yaml +# +#include unit-tests/hosts.yaml +# +strategy: + policy: consistent_hash + hash_key: path+query + groups: + - *g1 + - *g2 + protocol: http + failover: + ring_mode: exhaust_rings + response_codes: + - 404 + - 503 + health_check: + - passive + diff --git a/proxy/http/remap/unit-tests/test_NextHopConfig.cc b/proxy/http/remap/unit-tests/test_NextHopConfig.cc new file mode 100644 index 00000000000..32f59dc8446 --- /dev/null +++ b/proxy/http/remap/unit-tests/test_NextHopConfig.cc @@ -0,0 +1,212 @@ +/** @file + + A brief file description + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#include +#include "NextHopConfig.h" +#include "tsconfig/Errata.h" +#include "tscore/Diags.h" +#include "yaml-cpp/yaml.h" + +#define CATCH_CONFIG_MAIN +#include "catch.hpp" + +TEST_CASE("1", "[loadConfig]") +{ + INFO("Test 1, loading a non-existent file.") + + NextHopConfig nh; + ts::Errata result; + + result = nh.loadConfig("notfound.yaml"); + INFO("messaage text: " + result.top().text()); + REQUIRE(result.top().getCode() == 1); +} + +TEST_CASE("2", "[loadConfig]") +{ + INFO("Test 1, loading a strategy file with a bad include file.") + + NextHopConfig nh; + ts::Errata result; + + result = nh.loadConfig("unit-tests/bad_include.yaml"); + INFO("messaage text: " + result.top().text()); + REQUIRE(result.top().getCode() == 1); +} + +TEST_CASE("3", "[loadConfig]") +{ + INFO("Test 2, loading a good file.") + + NextHopConfig nh; + ts::Errata result; + + result = nh.loadConfig("unit-tests/strategy.yaml"); + INFO("messaage text: " + result.top().text()); + REQUIRE(result.top().getCode() == 0); + + CHECK(nh.config.policy == CONSISTENT_HASH); + CHECK(nh.config.hashKey == PATH_QUERY); + CHECK(nh.config.protocol == HTTP); + CHECK(nh.config.failover.ringMode == EXHAUST_RINGS); + CHECK(nh.config.failover.responseCodes[0] == 404); + CHECK(nh.config.failover.responseCodes[1] == 503); + CHECK(nh.config.failover.healthChecks[0] == PASSIVE); + + CHECK(nh.config.groups[0][0].host == "p1-cache.foo.com"); + CHECK(nh.config.groups[0][0].healthCheckUrl == "tcp://192.168.1.1:80"); + CHECK(nh.config.groups[0][0].weight == 1.0); + CHECK(nh.config.groups[0][0].protocols[0].protocol == "http"); + CHECK(nh.config.groups[0][0].protocols[0].port == 80); + CHECK(nh.config.groups[0][0].protocols[1].protocol == "https"); + CHECK(nh.config.groups[0][0].protocols[1].port == 443); + + CHECK(nh.config.groups[0][1].host == "p2-cache.foo.com"); + CHECK(nh.config.groups[0][1].healthCheckUrl == "tcp://192.168.1.2:80"); + CHECK(nh.config.groups[0][1].weight == 2.0); + CHECK(nh.config.groups[0][1].protocols[0].protocol == "http"); + CHECK(nh.config.groups[0][1].protocols[0].port == 8080); + CHECK(nh.config.groups[0][1].protocols[1].protocol == "https"); + CHECK(nh.config.groups[0][1].protocols[1].port == 8443); + + CHECK(nh.config.groups[1][0].host == "s1-cache.bar.com"); + CHECK(nh.config.groups[1][0].healthCheckUrl == "tcp://192.168.2.1:80"); + CHECK(nh.config.groups[1][0].weight == 0.1); + CHECK(nh.config.groups[1][0].protocols[0].protocol == "http"); + CHECK(nh.config.groups[1][0].protocols[0].port == 80); + CHECK(nh.config.groups[1][0].protocols[1].protocol == "https"); + CHECK(nh.config.groups[1][0].protocols[1].port == 443); + + CHECK(nh.config.groups[1][1].host == "s2-cache.bar.com"); + CHECK(nh.config.groups[1][1].healthCheckUrl == "tcp://192.168.2.2:80"); + CHECK(nh.config.groups[1][1].weight == 0.9); + CHECK(nh.config.groups[1][1].protocols[0].protocol == "http"); + CHECK(nh.config.groups[1][1].protocols[0].port == 8080); + CHECK(nh.config.groups[1][1].protocols[1].protocol == "https"); + CHECK(nh.config.groups[1][1].protocols[1].port == 8443); +} + +TEST_CASE("4", "[loadConfig]") +{ + INFO("Test 2, loading a combined hosts and strategy file with no #include.") + + NextHopConfig nh; + ts::Errata result; + + result = nh.loadConfig("unit-tests/combined.yaml"); + INFO("messaage text: " + result.top().text()); + REQUIRE(result.top().getCode() == 0); + + CHECK(nh.config.policy == CONSISTENT_HASH); + CHECK(nh.config.hashKey == PATH_QUERY); + CHECK(nh.config.protocol == HTTP); + CHECK(nh.config.failover.ringMode == EXHAUST_RINGS); + CHECK(nh.config.failover.responseCodes[0] == 404); + CHECK(nh.config.failover.responseCodes[1] == 503); + CHECK(nh.config.failover.healthChecks[0] == PASSIVE); + + CHECK(nh.config.groups[0][0].host == "p1-cache.foo.com"); + CHECK(nh.config.groups[0][0].healthCheckUrl == "tcp://192.168.1.1:80"); + CHECK(nh.config.groups[0][0].weight == 1.0); + CHECK(nh.config.groups[0][0].protocols[0].protocol == "http"); + CHECK(nh.config.groups[0][0].protocols[0].port == 80); + CHECK(nh.config.groups[0][0].protocols[1].protocol == "https"); + CHECK(nh.config.groups[0][0].protocols[1].port == 443); + + CHECK(nh.config.groups[0][1].host == "p2-cache.foo.com"); + CHECK(nh.config.groups[0][1].healthCheckUrl == "tcp://192.168.1.2:80"); + CHECK(nh.config.groups[0][1].weight == 2.0); + CHECK(nh.config.groups[0][1].protocols[0].protocol == "http"); + CHECK(nh.config.groups[0][1].protocols[0].port == 8080); + CHECK(nh.config.groups[0][1].protocols[1].protocol == "https"); + CHECK(nh.config.groups[0][1].protocols[1].port == 8443); + + CHECK(nh.config.groups[1][0].host == "s1-cache.bar.com"); + CHECK(nh.config.groups[1][0].healthCheckUrl == "tcp://192.168.2.1:80"); + CHECK(nh.config.groups[1][0].weight == 0.1); + CHECK(nh.config.groups[1][0].protocols[0].protocol == "http"); + CHECK(nh.config.groups[1][0].protocols[0].port == 80); + CHECK(nh.config.groups[1][0].protocols[1].protocol == "https"); + CHECK(nh.config.groups[1][0].protocols[1].port == 443); + + CHECK(nh.config.groups[1][1].host == "s2-cache.bar.com"); + CHECK(nh.config.groups[1][1].healthCheckUrl == "tcp://192.168.2.2:80"); + CHECK(nh.config.groups[1][1].weight == 0.9); + CHECK(nh.config.groups[1][1].protocols[0].protocol == "http"); + CHECK(nh.config.groups[1][1].protocols[0].port == 8080); + CHECK(nh.config.groups[1][1].protocols[1].protocol == "https"); + CHECK(nh.config.groups[1][1].protocols[1].port == 8443); +} + +TEST_CASE("5", "[loadConfig]") +{ + // default weight is 1.0 when no weight alias extension is added to the groups + INFO("Test 2, loading a combined hosts and strategy file with no #include and no hosts alias extension for weight.") + + NextHopConfig nh; + ts::Errata result; + + result = nh.loadConfig("unit-tests/combined_no_weight_alias_extension.yaml"); + INFO("messaage text: " + result.top().text()); + REQUIRE(result.top().getCode() == 0); + + CHECK(nh.config.policy == CONSISTENT_HASH); + CHECK(nh.config.hashKey == PATH_QUERY); + CHECK(nh.config.protocol == HTTP); + CHECK(nh.config.failover.ringMode == EXHAUST_RINGS); + CHECK(nh.config.failover.responseCodes[0] == 404); + CHECK(nh.config.failover.responseCodes[1] == 503); + CHECK(nh.config.failover.healthChecks[0] == PASSIVE); + + CHECK(nh.config.groups[0][0].host == "p1-cache.foo.com"); + CHECK(nh.config.groups[0][0].healthCheckUrl == "tcp://192.168.1.1:80"); + CHECK(nh.config.groups[0][0].weight == 1.0); + CHECK(nh.config.groups[0][0].protocols[0].protocol == "http"); + CHECK(nh.config.groups[0][0].protocols[0].port == 80); + CHECK(nh.config.groups[0][0].protocols[1].protocol == "https"); + CHECK(nh.config.groups[0][0].protocols[1].port == 443); + + CHECK(nh.config.groups[0][1].host == "p2-cache.foo.com"); + CHECK(nh.config.groups[0][1].healthCheckUrl == "tcp://192.168.1.2:80"); + CHECK(nh.config.groups[0][1].weight == 1.0); + CHECK(nh.config.groups[0][1].protocols[0].protocol == "http"); + CHECK(nh.config.groups[0][1].protocols[0].port == 8080); + CHECK(nh.config.groups[0][1].protocols[1].protocol == "https"); + CHECK(nh.config.groups[0][1].protocols[1].port == 8443); + + CHECK(nh.config.groups[1][0].host == "s1-cache.bar.com"); + CHECK(nh.config.groups[1][0].healthCheckUrl == "tcp://192.168.2.1:80"); + CHECK(nh.config.groups[1][0].weight == 1.0); + CHECK(nh.config.groups[1][0].protocols[0].protocol == "http"); + CHECK(nh.config.groups[1][0].protocols[0].port == 80); + CHECK(nh.config.groups[1][0].protocols[1].protocol == "https"); + CHECK(nh.config.groups[1][0].protocols[1].port == 443); + + CHECK(nh.config.groups[1][1].host == "s2-cache.bar.com"); + CHECK(nh.config.groups[1][1].healthCheckUrl == "tcp://192.168.2.2:80"); + CHECK(nh.config.groups[1][1].weight == 1.0); + CHECK(nh.config.groups[1][1].protocols[0].protocol == "http"); + CHECK(nh.config.groups[1][1].protocols[0].port == 8080); + CHECK(nh.config.groups[1][1].protocols[1].protocol == "https"); + CHECK(nh.config.groups[1][1].protocols[1].port == 8443); +}