diff --git a/gateway/src/apicast/policy/maintenance_mode/README.md b/gateway/src/apicast/policy/maintenance_mode/README.md index 3e96f81b7..dd982d0ef 100644 --- a/gateway/src/apicast/policy/maintenance_mode/README.md +++ b/gateway/src/apicast/policy/maintenance_mode/README.md @@ -10,7 +10,9 @@ and message. It's useful for maintenance periods or to temporarily block an API. | status (integer, _optional_) | 503 | Response code | | message (string, _optional_) | Service Unavailable - Maintenance | Response message | -## Example Configuration +## Examples Configuration + +- Custom response message ```json { "name": "maintenance-mode", @@ -20,3 +22,25 @@ and message. It's useful for maintenance periods or to temporarily block an API. } } ``` +- Apply Maintenance Mode for a specific upstream +```json +{ + "name": "maintenance_mode", + "configuration": { + "condition": { + "operations": [ + { + "left_type": "liquid", + "right_type": "plain", + "left": "{{ upstream.host }}{{ upstream.path }}", + "right": "echo-api.3scale.net/test", + "op": "==" + } + ], + "combine_op": "and" + }, + "status": 503, + "message": "Echo API /test is currently Unavailable" + } +} +``` \ No newline at end of file diff --git a/gateway/src/apicast/policy/maintenance_mode/apicast-policy.json b/gateway/src/apicast/policy/maintenance_mode/apicast-policy.json index 79ab484c9..305180a49 100644 --- a/gateway/src/apicast/policy/maintenance_mode/apicast-policy.json +++ b/gateway/src/apicast/policy/maintenance_mode/apicast-policy.json @@ -2,11 +2,79 @@ "$schema": "http://apicast.io/policy-v1.1/schema#manifest#", "name": "Maintenance Mode", "summary": "Rejects incoming requests. Useful for maintenance periods.", - "description": ["A policy which allows you reject incoming requests with a specified status code and message.", - "It's useful for maintenance periods or to temporarily block an API." + "description": [ + "A policy which allows you to reject incoming requests with a specified status code and message. ", + "It's useful for maintenance periods or to temporarily block an API. \n", + "It allows to select a list of Upstream URLs for which to enable the maintenance mode." ], "version": "builtin", "configuration": { + "definitions": { + "operation": { + "type": "object", + "$id": "#/definitions/operation", + "properties": { + "left": { + "type": "string" + }, + "op": { + "description": "Operation to apply. The matches op supports PCRE (Perl compatible regular expressions)", + "type": "string", + "enum": [ + "==", + "!=", + "matches" + ] + }, + "right": { + "type": "string" + }, + "left_type": { + "description": "How to evaluate 'left'", + "type": "string", + "default": "plain", + "oneOf": [ + { + "enum": [ + "plain" + ], + "title": "Evaluate 'left' as plain text." + }, + { + "enum": [ + "liquid" + ], + "title": "Evaluate 'left' as liquid." + } + ] + }, + "right_type": { + "description": "How to evaluate 'right'", + "type": "string", + "default": "plain", + "oneOf": [ + { + "enum": [ + "plain" + ], + "title": "Evaluate 'right' as plain text." + }, + { + "enum": [ + "liquid" + ], + "title": "Evaluate 'right' as liquid." + } + ] + } + }, + "required": [ + "left", + "op", + "right" + ] + } + }, "type": "object", "properties": { "status": { @@ -23,6 +91,32 @@ "type": "string", "description": "Content-Type header for the response", "default": "text/plain; charset=utf-8" + }, + "condition": { + "type": "object", + "title": "Condition", + "required": [ + "combine_op", + "operations" + ], + "properties": { + "combine_op": { + "title": "Combine operation", + "type": "string", + "default": "and", + "enum": [ + "and", + "or" + ] + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/definitions/operation" + }, + "minItems": 1 + } + } } } } diff --git a/gateway/src/apicast/policy/maintenance_mode/maintenance_mode.lua b/gateway/src/apicast/policy/maintenance_mode/maintenance_mode.lua index d651fce07..de84f027e 100644 --- a/gateway/src/apicast/policy/maintenance_mode/maintenance_mode.lua +++ b/gateway/src/apicast/policy/maintenance_mode/maintenance_mode.lua @@ -1,4 +1,4 @@ --- This is a simple policy. It allows you reject incoming requests with a +-- This policy allows to reject incoming requests with a -- specified status code and message. -- It's useful for maintenance periods or to temporarily block an API @@ -11,6 +11,9 @@ local default_status_code = 503 local default_message = "Service Unavailable - Maintenance" local default_message_content_type = "text/plain; charset=utf-8" +local Condition = require('apicast.conditions.condition') +local Operation = require('apicast.conditions.operation') + function _M.new(configuration) local policy = new(configuration) @@ -19,15 +22,35 @@ function _M.new(configuration) policy.message_content_type = default_message_content_type if configuration then + policy.maintenance_upstreams = configuration.upstreams policy.status_code = tonumber(configuration.status) or policy.status_code policy.message = configuration.message or policy.message policy.message_content_type = configuration.message_content_type or policy.message_content_type end + policy:load_condition(configuration) return policy end -function _M:rewrite() +function _M:load_condition(config) + if not config or not config.condition then + return + end + + local operations = {} + for _, operation in ipairs(config.condition.operations or {}) do + table.insert( operations, + Operation.new( + operation.left, + operation.left_type or default_template_type, + operation.op, + operation.right, + operation.right_type or default_template_type)) + end + self.condition = Condition.new( operations, config.condition.combine_op or default_combine_op) +end + +function set_maintenance_mode(self) ngx.header['Content-Type'] = self.message_content_type ngx.status = self.status_code ngx.say(self.message) @@ -35,4 +58,18 @@ function _M:rewrite() return ngx.exit(ngx.status) end +function _M:access(context) + -- Before the condition was not set, so the maintenance mode when condition is + -- nil should work. + if self.condition == nil then + return set_maintenance_mode(self) + end + + context.upstream = context.route_upstream or context:get_upstream() or {} + context.upstream = context.upstream.uri or context.upstream + if self.condition:evaluate(context) then + return set_maintenance_mode(self) + end +end + return _M diff --git a/gateway/src/apicast/policy/upstream/upstream.lua b/gateway/src/apicast/policy/upstream/upstream.lua index 4dfa8d58f..e5dfcd8f2 100644 --- a/gateway/src/apicast/policy/upstream/upstream.lua +++ b/gateway/src/apicast/policy/upstream/upstream.lua @@ -58,6 +58,10 @@ function _M:rewrite(context) end end +function _M:access(context) + context.route_upstream = context[self] or context.route_upstream +end + function _M:content(context) local upstream = context[self] diff --git a/spec/policy/maintenance_mode/maintenance_mode_spec.lua b/spec/policy/maintenance_mode/maintenance_mode_spec.lua index 7721df2af..4384c03e2 100644 --- a/spec/policy/maintenance_mode/maintenance_mode_spec.lua +++ b/spec/policy/maintenance_mode/maintenance_mode_spec.lua @@ -1,30 +1,41 @@ local MaintenancePolicy = require('apicast.policy.maintenance_mode') +local ngx_variable = require('apicast.policy.ngx_variable') + describe('Maintenance mode policy', function() - describe('.rewrite', function() + + local test_host = 'backend.example.org' + local test_host2 = 'backend2.example.org' + local test_path = '/foo/bar/' + local test_path2 = '/bar/foo/' + + describe('.access', function() before_each(function() + local test_upstream = { uri = { scheme = 'https', host = test_host, port = '443', path = test_path } } + ctx = { get_upstream = function() return test_upstream end } stub(ngx, 'say') stub(ngx, 'exit') ngx.header = {} + stub(ngx_variable, 'available_context', function(context) return context end) end) context('when using the defaults', function() local maintenance_policy = MaintenancePolicy.new() it('returns 503', function() - maintenance_policy:rewrite() + maintenance_policy:access(ctx) assert.stub(ngx.exit).was_called_with(503) end) it('returns the default message', function() - maintenance_policy:rewrite() + maintenance_policy:access(ctx) assert.stub(ngx.say).was_called_with('Service Unavailable - Maintenance') end) it('returns the default Content-Type header', function() - maintenance_policy:rewrite() + maintenance_policy:access(ctx) assert.equals('text/plain; charset=utf-8', ngx.header['Content-Type']) end) @@ -37,7 +48,7 @@ describe('Maintenance mode policy', function() { status = custom_code } ) - maintenance_policy:rewrite() + maintenance_policy:access(ctx) assert.stub(ngx.exit).was_called_with(custom_code) end) @@ -50,7 +61,7 @@ describe('Maintenance mode policy', function() { message = custom_msg } ) - maintenance_policy:rewrite() + maintenance_policy:access(ctx) assert.stub(ngx.say).was_called_with(custom_msg) end) @@ -67,10 +78,109 @@ describe('Maintenance mode policy', function() ) - maintenance_policy:rewrite() + maintenance_policy:access(ctx) assert.equals('application/json', ngx.header['Content-Type']) end) end) + + context('when Maintenance Mode is configured for a specific upstream', function() + local maintenance_policy = MaintenancePolicy.new({ + condition = { + operations={{op="==", left="{{ upstream.scheme }}://{{ upstream.host }}{{ upstream.port }}{{ upstream.path }}", left_type="liquid", right="https://"..test_host.."443"..test_path, right_type="plain"}}, + combine_op="and" + } + }) + it('returns 503', function() + maintenance_policy:access(ctx) + assert.stub(ngx.exit).was_called_with(503) + end) + end) + + context('when Maintenance Mode is configured for a specific upstream (different host)', function() + local maintenance_policy = MaintenancePolicy.new({ + condition = { + operations={{op="==", left="{{ upstream.host }}{{ upstream.path }}", left_type="liquid", right=test_host2..test_path, right_type="plain"}}, + combine_op="and" + } + }) + it('does not enable maintenance mode', function() + maintenance_policy:access(ctx) + assert.stub(ngx.say).was_not_called() + end) + end) + + context('when Maintenance Mode is configured for a specific upstream (different path)', function() + local maintenance_policy = MaintenancePolicy.new({ + condition = { + operations={{op="==", left="{{ upstream.host }}{{ upstream.path }}", left_type="liquid", right=test_host..test_path2, right_type="plain"}}, + combine_op="and" + } + }) + it('does not enable maintenance mode', function() + maintenance_policy:access(ctx) + assert.stub(ngx.say).was_not_called() + end) + end) + + context('when Maintenance Mode is configured for a specific upstream (different scheme and port)', function() + local maintenance_policy = MaintenancePolicy.new({ + condition = { + operations={{op="==", left="{{ upstream.scheme }}://{{ upstream.host }}{{ upstream.port }}{{ upstream.path }}", left_type="liquid", right="http://"..test_host.."80"..test_path, right_type="plain", right_type="plain"}}, + combine_op="and" + } + }) + it('does not enable maintenance mode', function() + maintenance_policy:access(ctx) + assert.stub(ngx.say).was_not_called() + end) + end) + + context('when host "matches"', function() + local maintenance_policy = MaintenancePolicy.new({ + condition = { + operations={{op="matches", left=test_host..test_path, left_type="plain", right="{{ upstream.host }}" , right_type="liquid"}}, + combine_op="and" + } + }) + it('returns 503', function() + maintenance_policy:access(ctx) + assert.stub(ngx.exit).was_called_with(503) + end) + end) + + context('OR condition match one', function() + local maintenance_policy = MaintenancePolicy.new({ + condition = { + operations={ + {op="==", left="{{ upstream.host }}{{ upstream.path }}", left_type="liquid", right=test_host2..test_path2, right_type="plain"}, + {op="==", left="{{ upstream.host }}{{ upstream.path }}", left_type="liquid", right=test_host..test_path, right_type="plain"} + }, + combine_op="or" + } + }) + it('returns 503', function() + maintenance_policy:access(ctx) + assert.stub(ngx.exit).was_called_with(503) + end) + end) + + context('no match conditions', function() + local maintenance_policy = MaintenancePolicy.new({ + condition = { + operations={ + {op="==", left="{{ upstream.host }}{{ upstream.path }}", left_type="liquid", right=test_host..test_path2, right_type="plain"}, + {op="==", left="{{ upstream.host }}{{ upstream.path }}", left_type="liquid", right=test_host2..test_path, right_type="plain"} + }, + combine_op="or" + } + }) + it('returns 503', function() + maintenance_policy:access(ctx) + assert.stub(ngx.say).was_not_called() + end) + end) end) end) + + diff --git a/t/apicast-policy-maintenance-mode.t b/t/apicast-policy-maintenance-mode.t index 7f3e8429a..b9d3a300f 100644 --- a/t/apicast-policy-maintenance-mode.t +++ b/t/apicast-policy-maintenance-mode.t @@ -40,7 +40,7 @@ Testing 3 things: } } --- request -GET / +GET /?user_key=value --- response_body Service Unavailable - Maintenance --- error_code: 503 @@ -91,7 +91,7 @@ Testing 3 things: } } --- request -GET / +GET /?user_key=value --- response_body Be back soon --- error_code: 501 @@ -99,8 +99,6 @@ Be back soon [error] === TEST 3: Maintenance policy works when placed after the APIcast policy -In this test we need to send the app credentials, because APIcast will check -that they are there before the maintenance policy runs. --- configuration { "services": [ @@ -122,6 +120,12 @@ that they are there before the maintenance policy runs. } ] } +--- backend + location /transactions/authrep.xml { + content_by_lua_block { + ngx.exit(200) + } + } --- upstream location / { content_by_lua_block { @@ -130,7 +134,7 @@ that they are there before the maintenance policy runs. } } --- request -GET /?user_key=uk +GET /?user_key=value --- response_body Service Unavailable - Maintenance --- error_code: 503 @@ -173,7 +177,7 @@ Service Unavailable - Maintenance } } --- request -GET / +GET /?user_key=value --- response_body { "msg": "Be back soon" } --- response_headers @@ -181,3 +185,227 @@ Content-Type: application/json --- error_code: 503 --- no_error_log [error] + + +=== TEST 5: Maintenance mode is applied with routing policy + matching upstream condition + +--- configuration +{ + "services": [ + { + "id": 42, + "backend_version": 1, + "backend_authentication_type": "service_token", + "backend_authentication_value": "token-value", + "proxy": { + "policy_chain": [ + { + "name": "apicast.policy.routing", + "configuration": { + "rules": [ + { + "url": "http://test:$TEST_NGINX_SERVER_PORT/b1", + "condition": { + "operations": [ + { + "match": "path", + "op": "matches", + "value": "^(/backend1/.*|/backend1/?)" + } + ] + }, + "replace_path": "{{uri | remove_first: '/backend1'}}" + } + ] + } + }, + { + "name": "apicast.policy.maintenance_mode", + "configuration": { + "condition": { + "operations": [ + { + "left_type": "liquid", + "right_type": "plain", + "left": "{{ upstream.host }}:{{ upstream.port }}{{ upstream.path }}", + "right": "test:$TEST_NGINX_SERVER_PORT/b1/", + "op": "==" + } + ], + "combine_op": "and" + } + } + }, + { "name": "apicast.policy.apicast" } + ], + "api_backend": "http://test:$TEST_NGINX_SERVER_PORT/b0", + "proxy_rules": [ + { "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 2 } + ] + } + } + ] +} +--- upstream + location /b1 { + content_by_lua_block { + local assert = require('luassert') + assert.is_true(false) + } + } + +--- request +GET /backend1?user_key=value +--- response_body +Service Unavailable - Maintenance +--- error_code: 503 +--- no_error_log +[error] + + +=== TEST 6: Maintenance mode is not applied with routing policy + non matching upstream condition + +--- configuration +{ + "services": [ + { + "id": 42, + "backend_version": 1, + "backend_authentication_type": "service_token", + "backend_authentication_value": "token-value", + "proxy": { + "policy_chain": [ + { + "name": "apicast.policy.routing", + "configuration": { + "rules": [ + { + "url": "http://test:$TEST_NGINX_SERVER_PORT/b2", + "condition": { + "operations": [ + { + "match": "path", + "op": "matches", + "value": "^(/backend2/.*|/backend2/?)" + } + ] + }, + "replace_path": "{{uri | remove_first: '/backend2'}}" + } + ] + } + }, + { + "name": "apicast.policy.maintenance_mode", + "configuration": { + "condition": { + "operations": [ + { + "left_type": "liquid", + "right_type": "plain", + "left": "{{ upstream.host }}:{{ upstream.port }}{{ upstream.path }}", + "right": "test:$TEST_NGINX_SERVER_PORT/b1/", + "op": "==" + } + ], + "combine_op": "and" + } + } + }, + { "name": "apicast.policy.apicast" } + ], + "api_backend": "http://test:$TEST_NGINX_SERVER_PORT/b0", + "proxy_rules": [ + { "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 2 } + ] + } + } + ] +} +--- backend + location /transactions/authrep.xml { + content_by_lua_block { + ngx.exit(200) + } + } +--- upstream + location /b2 { + echo 'yay, api backend: $http_host'; + } +--- request +GET /backend2?user_key=value +--- response_body env +yay, api backend: test:$TEST_NGINX_SERVER_PORT +--- error_code: 200 +--- no_error_log +[error] + + +=== TEST 7: Maintenance mode works with upstream policy + matching upstream condition + +--- configuration +{ + "services": [ + { + "id": 42, + "backend_version": 1, + "backend_authentication_type": "service_token", + "backend_authentication_value": "token-value", + "proxy": { + "policy_chain": [ + { + "name": "apicast.policy.upstream", + "configuration": + { + "rules": [ { "regex": "/backend1", "url": "http://test:$TEST_NGINX_SERVER_PORT/b1/" } ] + } + }, + { + "name": "apicast.policy.maintenance_mode", + "configuration": { + "condition": { + "operations": [ + { + "left_type": "liquid", + "right_type": "plain", + "left": "{{ upstream.host }}:{{ upstream.port }}{{ upstream.path }}", + "right": "test:$TEST_NGINX_SERVER_PORT/b1/", + "op": "==" + } + ], + "combine_op": "and" + } + } + }, + { "name": "apicast.policy.apicast" } + ], + "api_backend": "http://test:$TEST_NGINX_SERVER_PORT/b2", + "proxy_rules": [ + { "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 2 } + ] + } + } + ] +} +--- backend + location /transactions/authrep.xml { + content_by_lua_block { + ngx.exit(200) + } + } +--- upstream + location /b1 { + content_by_lua_block { + local assert = require('luassert') + assert.is_true(false) + } + } +--- request +GET /backend1?user_key=value +--- response_body +Service Unavailable - Maintenance +--- error_code: 503 +--- no_error_log +[error] + +