Skip to content

Commit 068a8bd

Browse files
authored
Merge pull request #715 from NatLabRockies/brownouts
Add feature for scheduled API brownouts
2 parents fc5ab21 + 0db2b2e commit 068a8bd

13 files changed

Lines changed: 592 additions & 10 deletions

config/schema.cue

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,19 @@ import "path"
565565
ssl_cert?: string
566566
ssl_cert_key?: string
567567
rewrites?: [...string]
568+
569+
#scheduled_brownout: {
570+
path_regex: string
571+
message?: string
572+
status_code?: uint
573+
574+
#schedule: {
575+
start_time: string
576+
end_time: string
577+
}
578+
schedule: [...#schedule]
579+
}
580+
scheduled_brownouts?: [...#scheduled_brownout]
568581
}
569582
hosts: [...#host] | *[]
570583

@@ -691,6 +704,11 @@ import "path"
691704
code: "HTTPS_REQUIRED"
692705
message: "Requests must be made over HTTPS. Try accessing the API at: {{https_url}}"
693706
}
707+
scheduled_brownout: {
708+
status_code: 410
709+
code: "SCHEDULED_BROWNOUT"
710+
message: "This API will be going away. Seek an alternative API. Contact us at {{contact_url}} for assistance."
711+
}
694712
}
695713
}
696714
default_api_backend_settings: #api_backend_settings | *_default_api_backend_settings_value
@@ -743,6 +761,7 @@ import "path"
743761
over_rate_limit?: {...}
744762
internal_server_error?: {...}
745763
https_required?: {...}
764+
scheduled_brownout?: {...}
746765
}
747766
}
748767
#api_backend_sub_settings: {

src/api-umbrella/proxy/error_handler.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,10 @@ return function(ngx_ctx, denied_code, settings, extra_data)
167167
-- also allowed via CORS).
168168
ngx_header["Access-Control-Allow-Origin"] = "*"
169169

170+
if error_data["cache_control"] then
171+
ngx_header["Cache-Control"] = error_data["cache_control"]
172+
end
173+
170174
ngx.status = status_code
171175
ngx_header.content_type = content_type
172176
ngx_print(output)

src/api-umbrella/proxy/hooks/access.lua

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ local referer_validator = require "api-umbrella.proxy.middleware.referer_validat
1717
local resolve_api_key = require "api-umbrella.proxy.middleware.resolve_api_key"
1818
local rewrite_request = require "api-umbrella.proxy.middleware.rewrite_request"
1919
local role_validator = require "api-umbrella.proxy.middleware.role_validator"
20+
local scheduled_brownout = require "api-umbrella.proxy.middleware.scheduled_brownout"
2021
local user_settings = require "api-umbrella.proxy.middleware.user_settings"
2122

2223
local err
@@ -30,6 +31,11 @@ if err then
3031
return error_handler(ngx_ctx, err, settings)
3132
end
3233

34+
err, err_data = scheduled_brownout(ngx_ctx, api)
35+
if err then
36+
return error_handler(ngx_ctx, err, settings, err_data)
37+
end
38+
3339
-- Find the API key set on the request.
3440
err = resolve_api_key(ngx_ctx)
3541
if err then

src/api-umbrella/proxy/hooks/init_preload_modules.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ require "api-umbrella.proxy.middleware.resolve_api_key"
2020
require "api-umbrella.proxy.middleware.rewrite_request"
2121
require "api-umbrella.proxy.middleware.rewrite_response"
2222
require "api-umbrella.proxy.middleware.role_validator"
23+
require "api-umbrella.proxy.middleware.scheduled_brownout"
2324
require "api-umbrella.proxy.middleware.user_settings"
2425
require "api-umbrella.proxy.middleware.website_matcher"
2526
require "api-umbrella.proxy.opensearch_templates_data"

src/api-umbrella/proxy/middleware/api_matcher.lua

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ local function apis_for_request_host(ngx_ctx, active_config)
1414

1515
local all_apis = active_config["api_backends"] or {}
1616
local apis_for_default_host = {}
17+
local host_normalized = ngx_ctx.host_normalized
1718
for _, api in ipairs(all_apis) do
18-
if matches_hostname(ngx_ctx, api["_frontend_host_normalized"], api["_frontend_host_wildcard_regex"]) then
19+
if matches_hostname(host_normalized, api["_frontend_host_normalized"], api["_frontend_host_wildcard_regex"]) then
1920
table.insert(apis, api)
20-
elseif api["_frontend_host_normalized"] == config["_default_hostname_normalized"]then
21+
elseif api["_frontend_host_normalized"] == config["_default_hostname_normalized"] then
2122
table.insert(apis_for_default_host, api)
2223
end
2324
end
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
local config = require("api-umbrella.utils.load_config")()
2+
3+
local ngx_var = ngx.var
4+
local now = ngx.now
5+
local re_find = ngx.re.find
6+
7+
return function(ngx_ctx, api)
8+
local scheduled_brownouts = api["_scheduled_brownouts"]
9+
if scheduled_brownouts then
10+
local current_time = now()
11+
if config["app_env"] == "test" then
12+
local fake_time = ngx_var.http_x_fake_time
13+
if fake_time then
14+
current_time = tonumber(fake_time)
15+
end
16+
end
17+
18+
local original_uri_path = ngx_ctx.original_uri_path
19+
for _, scheduled_brownout in ipairs(scheduled_brownouts) do
20+
for _, schedule in ipairs(scheduled_brownout["schedule"]) do
21+
if current_time >= schedule["_start_time_timestamp"] and current_time < schedule["_end_time_timestamp"] then
22+
local find_from, _, find_err = re_find(original_uri_path, scheduled_brownout["path_regex"], "jo")
23+
if find_from then
24+
return "scheduled_brownout", {
25+
status_code = scheduled_brownout["status_code"],
26+
message = scheduled_brownout["message"],
27+
cache_control = "no-store",
28+
}
29+
elseif find_err then
30+
ngx.log(ngx.ERR, "regex error: ", find_err)
31+
end
32+
end
33+
end
34+
end
35+
end
36+
37+
return nil
38+
end

src/api-umbrella/proxy/middleware/website_matcher.lua

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ local matches_hostname = require "api-umbrella.utils.matches_hostname"
44
return function(ngx_ctx, active_config)
55
local websites = active_config["website_backends"] or {}
66
local default_website
7+
local host_normalized = ngx_ctx.host_normalized
78
for _, website in ipairs(websites) do
8-
if matches_hostname(ngx_ctx, website["_frontend_host_normalized"], website["_frontend_host_wildcard_regex"]) then
9+
if matches_hostname(host_normalized, website["_frontend_host_normalized"], website["_frontend_host_wildcard_regex"]) then
910
return website
10-
elseif website["_frontend_host_normalized"] == config["_default_hostname_normalized"]then
11+
elseif website["_frontend_host_normalized"] == config["_default_hostname_normalized"] then
1112
default_website = website
1213
end
1314
end

src/api-umbrella/utils/active_config_store/cache_computed_api_backend.lua

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
local append_array = require "api-umbrella.utils.append_array"
12
local escape_regex = require "api-umbrella.utils.escape_regex"
23
local host_normalize = require "api-umbrella.utils.host_normalize"
4+
local matches_hostname = require "api-umbrella.utils.matches_hostname"
35
local mustache_unescape = require "api-umbrella.utils.mustache_unescape"
46
local set_hostname_regex = require "api-umbrella.utils.active_config_store.set_hostname_regex"
57
local split = require("pl.utils").split
@@ -9,7 +11,24 @@ local table_size = require("pl.tablex").size
911
local decode_args = ngx.decode_args
1012
local re_gsub = ngx.re.gsub
1113

12-
return function(api)
14+
local function cache_scheduled_brownouts(config, api)
15+
local hosts = config["hosts"]
16+
if hosts then
17+
local frontend_host_normalized = api["_frontend_host_normalized"]
18+
for _, host in ipairs(hosts) do
19+
local scheduled_brownouts = host["scheduled_brownouts"]
20+
if scheduled_brownouts and (frontend_host_normalized == host["_hostname_normalized"] or matches_hostname(frontend_host_normalized, host["_hostname_normalized"], host["_hostname_wildcard_regex"])) then
21+
if api["_scheduled_brownouts"] == nil then
22+
api["_scheduled_brownouts"] = {}
23+
end
24+
25+
append_array(api["_scheduled_brownouts"], scheduled_brownouts)
26+
end
27+
end
28+
end
29+
end
30+
31+
return function(config, api)
1332
if not api then return end
1433

1534
if api["frontend_host"] then
@@ -118,4 +137,6 @@ return function(api)
118137
end
119138
end
120139
end
140+
141+
cache_scheduled_brownouts(config, api)
121142
end

src/api-umbrella/utils/active_config_store/parse_api_backends.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ local function parse_api(api)
1212
api["id"] = stable_object_hash(api)
1313
end
1414

15-
cache_computed_api_backend(api)
15+
cache_computed_api_backend(config, api)
1616
cache_computed_api_backend_sub_settings(config, api["sub_settings"], deep_merge_overwrite_arrays(deepcopy(config["default_api_backend_settings"]), api["settings"]))
1717
cache_computed_api_backend_settings(config, api["settings"], config["default_api_backend_settings"])
1818
end

src/api-umbrella/utils/generate_runtime_config.lua

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ local host_normalize = require "api-umbrella.utils.host_normalize"
1111
local invert_table = require "api-umbrella.utils.invert_table"
1212
local is_empty = require "api-umbrella.utils.is_empty"
1313
local isfile = require("pl.path").isfile
14+
local iso8601_to_timestamp = require("api-umbrella.utils.time").iso8601_to_timestamp
1415
local json_decode = require("cjson").decode
1516
local json_encode = require "api-umbrella.utils.json_encode"
1617
local mkdir_p = require "api-umbrella.utils.mkdir_p"
@@ -19,6 +20,7 @@ local path_exists = require "api-umbrella.utils.path_exists"
1920
local path_join = require "api-umbrella.utils.path_join"
2021
local pl_utils = require "pl.utils"
2122
local random_token = require "api-umbrella.utils.random_token"
23+
local set_hostname_regex = require "api-umbrella.utils.active_config_store.set_hostname_regex"
2224
local shell_blocking_capture = require("shell-games").capture
2325
local stat = require "posix.sys.stat"
2426
local strip = require("pl.stringx").strip
@@ -179,6 +181,8 @@ local function set_computed_config(config)
179181

180182
local default_host_exists = false
181183
for _, host in ipairs(config["hosts"]) do
184+
set_hostname_regex(host, "hostname")
185+
182186
if host["default"] then
183187
default_host_exists = true
184188
end
@@ -188,6 +192,15 @@ local function set_computed_config(config)
188192
else
189193
host["_nginx_server_name"] = host["hostname"]
190194
end
195+
196+
if host["scheduled_brownouts"] then
197+
for _, scheduled_brownout in ipairs(host["scheduled_brownouts"]) do
198+
for _, schedule in ipairs(scheduled_brownout["schedule"]) do
199+
schedule["_start_time_timestamp"] = iso8601_to_timestamp(schedule["start_time"])
200+
schedule["_end_time_timestamp"] = iso8601_to_timestamp(schedule["end_time"])
201+
end
202+
end
203+
end
191204
end
192205

193206
-- If a default host hasn't been explicitly defined, then add a default

0 commit comments

Comments
 (0)