diff --git a/doc/policy_tutorial/DEV_ENV_SETUP.md b/doc/policy_tutorial/DEV_ENV_SETUP.md new file mode 100644 index 000000000..d6211df69 --- /dev/null +++ b/doc/policy_tutorial/DEV_ENV_SETUP.md @@ -0,0 +1,104 @@ +# Setting up the development environment + +You need to configure an environment to work with Lua because APIcast policies are created in the Lua programming language. Additionally, you can use an actual APIcast server to perform local tests. + +Following simple steps, you can set up a development environment for APIcast using Docker and Docker Compose. + +### prerequisites +You must install Docker and Docker Compose: + + * Docker version 19.03.8. You can find instructions for installing Docker in the Docker [website](https://docs.docker.com/get-docker/). + * Docker Compose version 1.25.4. You can find instructions for installing Docker Compose on the Docker [website](https://docs.docker.com/compose/install/). + +Instructions for installing Docker Compose can also be found on the Docker [website](https://docs.docker.com/compose/install/). + +### Setting up the development image +After you have installed Docker and Docker Compose, you can configure the APIcast development image. + +. Clone the APIcast git repository. Use the stable branch of APIcast to base the policy on the latest 3scale release. + +```shell +git clone https://github.com/3scale/apicast.git +cd apicast/ +``` + +. To start the APIcast containers using `docker-compose`, use the *make* file provided by 3scale. Run this command in the APIcast directory: +```shell +make development +``` + +```bash +Running on Linux +docker-compose -f docker-compose-devel.yml -f docker-compose-devel-volmount-default.yml up -d +[+] Running 2/2 + ⠿ Container apicast_build_0-redis-1 Started 0.8s + ⠿ Container apicast_build_0-development-1 Started 0.7s +docker-compose -f docker-compose-devel.yml -f docker-compose-devel-volmount-default.yml exec -e COLUMNS="`tput cols`" -e LINES="`tput lines`" --user 1000:1000 development bash +bash-4.4$ +``` +. The Docker container starts in the foreground with a bash session. The next step is to install all the dependencies inside the container, using a *make* command: + +```shell +make dependencies +``` + ++ +* The output will be very long. After a successful completion of the installation of all the dependencies, you will see a message similar to this: ++ + +``` +Complete! Modules were installed into /opt/app-root/src/local +local/bin/carton bundle 2> /dev/null +Bundling modules using /opt/app-root/src/gateway/cpanfile +Complete! Modules were bundled into /opt/app-root/src/vendor/cache +bash-4.4$ +``` + +Now you can run APIcast unit tests to see if everything is up and running and ready to start the development of our policy. + +. To run the Lua unit tests, use the following command **inside** the container: + +```shell +bash-4.4$ make busted +EXTRA_CFLAGS="-DHAVE_EVP_KDF_CTX=1" /usr/local/openresty/luajit/bin/rover install --roverfile=/opt/app-root/src/gateway/Roverfile > /dev/null +/usr/local/openresty/luajit/bin/rover exec bin/busted +●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●◌●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●◌●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●● +1208 successes / 0 failures / 0 errors / 2 pending : 6.209112 seconds + +Pending → spec/policy/rate_limit/redis_shdict_spec.lua @ 33 +Redis Shared Dictionary incr without default + +Pending → spec/resty/http_ng_spec.lua @ 35 +http_ng options method works with default options +``` + +After confirming that the unit tests run successfully, you can start the policy development. + +The project’s source code will be available in the container, and synchronized with your local APIcast directory. You can edit files in your preferred environment and still run whatever you need inside the Docker container. + +The development container for APIcast uses a Docker volume mount to mount the local APIcast directory inside the container. This means all files changed locally in the repository are synchronized with the container and used in the tests and runtime of the development container. + +![APIcast-dev-container-mount](img/apicast-dev-container-mount.png) + +It also means you can use your favorite IDE or editor to develop your APIcast policy. + +### Stopping the development container +Stopping the development environment container is a two step process. In the interactive Bash session, press: + +``` +Ctrl + C +``` + +This exits the foreground bash shell, but the containers are still running. Run the following make command to stop all containers: + +```shell +$ make stop-development +docker-compose -f docker-compose-devel.yml -f docker-compose-devel-volmount-mac.yml down +Stopping apicast_build_0_development_1 ... done +Stopping apicast_build_0_redis_1 ... done +Removing apicast_build_0_development_1 ... done +Removing apicast_build_0_redis_1 ... done +Removing network apicast_build_0_default +``` + +Now you are ready to create a 3scale APIcast policy. The next step in the tutorial is [here](POLICY_SCAFFOLD.md) diff --git a/doc/policy_tutorial/POLICY_IMPLEMENTATION.md b/doc/policy_tutorial/POLICY_IMPLEMENTATION.md new file mode 100644 index 000000000..26004ea87 --- /dev/null +++ b/doc/policy_tutorial/POLICY_IMPLEMENTATION.md @@ -0,0 +1,342 @@ +# Implementing the policy + +In the last two parts of the APIcast policy development tutorial we looked at setting up the development environment and creating the scaffold of the policy. In this part we are actually going to implement and test the policy itself. Now in order to create a policy we need to have it implement some functionality. The policy must actually do something. Now the functionality of this policy is made purely for educational purposes. It is not going to win any killer feature prizes anytime soon. And as a matter of fact this single policy is going to perform two separate functions. Normally this would be split in two separate policies and chained together. But again, for educational purposes these two functionalities are going to be implemented in one policy. The main reason for this is that certain key concepts of policy development can be demonstrated. These concepts are: + +* reading and using configuration properties +* reading query parameters from the HTTP request +* reading headers from the HTTP request +* creating headers in the request send to the upstream proxy +* executing a policy in multiple phases +* passing data between different phases of the policy + +The policy we are going to create is going to transform all HTTP query parameters from the incoming request to HTTP headers send to the upstream response. With a configurable ‘overwrite’ flag we can during configuration indicate if a query parameter conflicts with an existing HTTP header if the HTTP header must be overwritten with the new value or if it should keep the existing HTTP header in place. In addition to this rewrite functionality we are going to provide another secret header in a configuration file. If the request does not contain this header with the value specified in the configuration the server will return a 403 forbidden response. +Schematically the functionality of the policy is outlined as follows: + +![policy-layout](img/policy_layout.png) + +Again, this is not going to win any killer feature prize soon, but it does illustrate the concepts outlined above. + +## Policy configuration + +### Creating the policy configuration schema + +The policy scaffolding process generated a configuration schema which we need to extend to define the properties we can use in our policy. +In our case we need to add the following properties to the schema: +* overwrite of type boolean +* secret of type string +And while we are changing the schema, we might as well provide a neat summary and description of our policy. + +The new configuration schema looks like this: +```json +{ + "$schema": "http://apicast.io/policy-v1/schema#manifest#", + "name": "hello_world", + "summary": "Parameter converter and secret checker", + "description": [ + "Modifies HTTP query parameters in a request to HTTP headers. And checks the existence of the secret header." + ], + "version": "builtin", + "configuration": { + "type": "object", + "properties": { + "overwrite": { + "description": "Overwrite flag used to indicate whether or not an existing header must be overwritten by this policy.", + "type": "boolean" + }, + "secret": { + "description": "The additional static secret used for verifying the request.", + "type": "string" + } + } + } +} +``` + +### Reading the configuration properties in the policy +The parsing of the json properties to a Lua table is handled by the policy framework of APIcast itself. So we only need to worry about reading from the config object. To read the values of the config object simply reference them and place them in the self table. This self variable is accessible from everywhere in the policy and this these config variables can be used in subsequent functions. + +```lua +function _M.new(config) + local self = new(config) + if config then + if config.overwrite == nil then + self.overwrite = true + else + self.overwrite = config.overwrite + end + self.secret = config.secret + end + return self +end +``` + +### Create unit tests +The scaffolding also created some unit tests, but since the policy was empty the unit tests did not do much. Now that the first code is written we can add another unit test. +Alter the accept configuration unit test to add the specific parameters: + +```lua +it('accepts configuration', function() + assert(_M.new({ overwrite = false, secret = "mysecret"})) +end) +``` + +## Implementing the rewrite +No we have the configuration properties available in our policy we can start implementing the rewrite of the query parameters to headers. +We are going to take a step by step approach to the creation of the policy. +* First we are going to define a function part of the policy module to execute in the rewrite phase which reads the HTTP query parameters and stores them in a Lua table. +* Then we create a local helper function to transform the HTTP query parameters to HTTP headers and provide the overwrite check +* lastly we are going to invoke the helper function from within the rewrite function + +### Read request parameters +In order to create a function executing in the ‘rewrite’ phase of Nginx we need to create a function called ‘rewrite’ and store it in the policy module, which has a variable _M. +Even though this is not a beginners Lua tutorial it is worth noting the declaration of the function is done with a : (colon) instead of a . (dot), this is syntactic sugar in the Lua programming language and adds the self variable as a function argument without explicitly defining it. We need the self variable to access the configuration properties we defined above. + +```lua +function _M:rewrite(context) + --read HTTP query params as Lua table + local query_params = ngx.req.get_uri_args() + +end +``` + +### Create or overwrite HTTP headers +Now that we have the rewrite function in place and the query parameter stored in a variable we can create a separate function performing the transformation. Off course this could easily be implemented all inside the rewrite function but creating separate functions often is a bit neater. Also it gives an opportunity to introduce another kind of function. The local function, of which the code looks like this: + +```lua +local function paramsToHeaders(query_params, overwrite) + for k, v in pairs(query_params) do + if overwrite == false and ngx.req.get_headers()[k] ~= nil then + ngx.log(ngx.NOTICE, "existing header found with name " .. k .. " but not overwritten because of setting overwrite is " .. tostring(overwrite)) + else + ngx.req.set_header(k, v) + end + end + ngx.req.set_uri_args = nil +end +``` + +The difference between the rewrite and paramsToHeaders function declaration is that the latter has the local keyword and that the function is not part of the _M module. This essentially creates a ‘private’ function which can only be invoked from other functions inside the policy module. +Now that the functionality is in place we can invoke the new function from within the rewrite method: + +```lua +function _M:rewrite(context) + --read HTTP query params as Lua table + local query_params = ngx.req.get_uri_args() + paramsToHeaders(query_params, self.overwrite) + +end +``` + +**Note: it is important to place the paramsToHeaders function above the rewrite function.** + +### Create unit tests +Now that our policy has some body to it we need to verify our functionality is correct. We can do this by creating some additional unit tests. +But before writing the actual unit tests there is some additional setup required within the unit tests. Because the unit test framework ‘Busted’ used with APIcast does not perform the unit test in the actual Nginx worker process the Nginx specific function calls are not available. So ngx.req.get_uri_args() and ngx.req.set_header() are not able to execute within the unit test. In order to create proper functioning unit tests mock have to be created for these function calls. +The skeleton for the unit tests looks something like this: + +```lua +describe('rewrite with overwrite', function() + local config = { overwrite = true, secret = "mysecret" } + local ngx_req_params = {} + local ngx_req_headers = {} + local context = {} + + before_each(function() + + stub(ngx.req,'get_headers', function() + return ngx_req_headers + end) + + stub(ngx.req, 'set_header', function(name, value) + ngx_req_headers[name] = value + end) + + stub(ngx.req, 'get_uri_args', function() + return ngx_req_params + end) + + end) + + –- unit tests come here + +end) +``` + +In the first line the ‘describe’ we define a block for our unit tests. This is more a logical group of one or more unit tests executing with the same stubs and config variables which are defined below. In this example all unit tests with the override configuration parameter set to ‘true’ are executed in a single block. Off course this is just an example of grouping a set of unit tests based on a configuration parameter, the grouping of unit tests is an arbitrary decision left to the developer. But do keep in mind the before_each function is part of the group. +After and inside the describe we define four different local variables: +* config – this is the Lua table equivalent of the configuration parameters used in this policy. Since we are going to test the rewrite with overwrite the overwrite property is set to true. +* ngx_req_params – an empty Lua table which we are going to use in our mock/stub for storing the HTTP query parameters +* ngx_req_headers – again an empty Lua table which we are going to use in our mock/stub for storing the HTTP headers +* context – an empty Lua table for storing the context. We are going to take a closer look at the context later when we are going to extend the policy to read the secret header. + +After the declaration of the variables the before_each function is defined, this function executes before each unittests and contains the stubs/mocks for the Nginx specific function calls. Essentially creating wrappers over our empty tables. +Now that the setup is in place we can implement a unit test. + +```lua +describe('rewrite with overwrite', function() + local config = { overwrite = true, secret = "mysecret" } + local ngx_req_params = {} + local ngx_req_headers = {} + local context = {} + + before_each(function() + + stub(ngx.req,'get_headers', function() + return ngx_req_headers + end) + + stub(ngx.req, 'set_header', function(name, value) + ngx_req_headers[name] = value + end) + + stub(ngx.req, 'get_uri_args', function() + return ngx_req_params + end) + + end) + + it('test single param', function() + local hello_world_policy = _M.new(config) + --create the test request + ngx_req_params["testkey"] = 'testvalue' + --execute the policy function + hello_world_policy:rewrite(context) + --retrieve the http header and verify the content + local responseheader = ngx.req.get_headers()["testkey"] + assert.same("testvalue", responseheader) + end) + + it('test overwrite header', function() + local hello_world_policy = _M.new(config) + --create the test request + ngx_req_params["testkey"] = 'testvalue' + ngx_req_headers["testkey"] = 'myheader' + --execute the policy function + hello_world_policy:rewrite(context) + --retrieve the http header and verify the content + local responseheader = ngx.req.get_headers()["testkey"] + assert.same("testvalue", responseheader) + end) + + it('test multiple params', function() + local hello_world_policy = _M.new(config) + --create test request + ngx_req_params["param1"] = "value1" + ngx_req_params["param2"] = "value2" + --execute the policy function + hello_world_policy:rewrite(context) + --retrieve the http headers and verify the content + local header1 = ngx.req.get_headers()["param1"] + local header2 = ngx.req.get_headers()["param2"] + assert.same("value1", header1) + assert.same("value2", header2) + end) +end) +``` + +Above three unittests are defined executing the policy and asserting the parameters have successfully been transformed to HTTP headers. +To execute the unit test inside the Docker developer image execute the following command: + +```shell +make busted +``` + +## Extending the policy +Now that we have the rewrite of HTTP query parameters to HTTP headers in place let’s extend the policy to do some additional thing. It is again worth noting that normal policy design would be to scope a single policy to do only one particular thing and use the chaining of multiple policies to create higher levels of functionality. Adding other unrelated functionality breaks this principle, but for educational purposes there is one additional thing we can look at, and that is. +**Execute code in multiple Nginx phases and passing data between those Nginx phases.** + +And in order to do that we are going to read the HTTP secret header in the ‘rewrite’ phase and pass it as a Lua variable to the ‘access’ phase in which we are going to compare it against the value set in the configuration of the policy. + +Let’s start with implementing the ‘access’ phase of the policy: +```lua +function _M:access(context) + + local secret_header = context.secret_header + + if secret_header ~= self.secret then + ngx.log(ngx.NOTICE, "request is not authorized, secrets do not match") + ngx.status = 403 + return ngx.exit(ngx.status) + else + ngx.log(ngx.NOTICE, "request is authorized, secrets match") + end + +end +``` + +The first line within the access function declares a local variable and sets the value to the secret_header from the context variable. Which is passed as an argument to the access function. +Then a simple if/else is executed to evaluate the secret_header variable to the value in the config. When these do not match the status is set to 403 (forbidden) and the process exits with ngx.exit +Now that we have the ‘access’ function in place reading from the context let’s modify the ‘rewrite’ function we created earlier to write the secret_header to the context. + +```lua +function _M:rewrite(context) + + --read HTTP query params as Lua table + local query_params = ngx.req.get_uri_args() + + paramsToHeaders(query_params, self.overwrite) + + local secret_header = ngx.req.get_headers()["secret"] + context.secret_header = secret_header + +end +``` + +Above the modified rewrite function. The last two lines of the function read the ‘secret’ HTTP header and assigns the content to the variable secret_header. Which in the last line is written to the context (which is implemented using a Lua table). + +### Create unit tests +Like the HTTP parameter to HTTP header rewrite this additional functionality needs to be verified. In order to do so some unit tests can be created. + +```lua +describe('secret header test', function() + + local config = {secret = "mysecret" } + local ngx_req_headers = {} + local ngx_req_params = {} + local context = {} + + before_each(function() + + stub(ngx.req,'get_headers', function() + return ngx_req_headers + end) + + stub(ngx.req, 'set_header', function(name, value) + ngx_req_headers[name] = value + end) + + stub(ngx.req, 'get_uri_args', function() + return ngx_req_params + end) + + end) + + it('authorized', function() + local hello_world_policy = _M.new(config) + + ngx_req_headers["secret"] = "mysecret" + + hello_world_policy:rewrite(context) + hello_world_policy:access(context) + + end) + it('not authorized', function() + local hello_world_policy = _M.new(config) + + ngx_req_headers["secret"] = "myownsecret" + + hello_world_policy:rewrite(context) + hello_world_policy:access(context) + + assert.same(ngx.status, 403) + end) +end) +``` + +Since the writing to the context is performed in the ‘rewrite’ phase that phase needs to be executed as well. So the same stubs are required in this unit test as the ones we saw earlier. In the tests itself both the rewrite and access functions are called with the context variable as argument. This context variable is an empty Lua table. The first, and only, value in this table is written to in inside the rewrite phase of the policy. In the last unit test the returning status code is verified to be 403. +Again, like earlier the unit tests can be executed inside the development container using the command: + +```shell +make busted +``` +Now that we have implemented our policy and performed some unit tests we can try and run the policy locally to verify it will run properly. This is described [here](POLICY_RUN_LOCALLY.md) \ No newline at end of file diff --git a/doc/policy_tutorial/POLICY_RUN_LOCALLY.md b/doc/policy_tutorial/POLICY_RUN_LOCALLY.md new file mode 100644 index 000000000..7ba15915e --- /dev/null +++ b/doc/policy_tutorial/POLICY_RUN_LOCALLY.md @@ -0,0 +1,149 @@ +# Run the policy locally + +When you have completed development and unit testing of the policy, it is time to start APIcast with the policy. This way you can verify the policy works inside the NGINX process. You are going to use the APIcast development Docker image to run APIcast with the created policy. + +## Create APIcast configuration + +You can provision APIcast in two ways, either automatically from the 3scale APIManager or via a JSON configuration file. Since the custom policy is not available in the 3scale APIManager, you will configure APIcast with the JSON configuration file. In the configuration file you will leverage the built-in echo service of APIcast to test the policy and receive an upstream response without running a full 3scale APIManager configuration. + +The part of the JSON configuration file detailing the configuration of our policy needs to adhere to the JSON schema we defined earlier. + +The location of the configuration file is arbitrary as long as the APIcast process can access the file. In this example, you will create the following file: **‘hello_world_config.json’** in the **apicast/examples/configuration** directory. + +The contents of the configuration file is: + +```json +{ + "services": [ + { + "proxy": { + "policy_chain": [ + { + "name": "hello_world", + "version": "builtin", + "configuration": { + "overwrite": true, + "secret": "mysecret" + } + }, + { + "name": "apicast.policy.upstream", + "configuration": { + "rules": [ + { + "regex": "/", + "url": "http://echo:8081" + } + ] + } + } + ] + } + } + ] +} +``` + +First, configure the hello_world policy inside the policy chain with overwrite and secret properties. Second, the upstream policy acts as an echo mock service in the policy chain, so you can receive a response. + +### Starting the APIcast server +To start the APIcast server with the hello_world_configuration.json file inside the development container, run the following command: + +```shell +bash-4.2$ bin/apicast --log-level=debug --dev -c examples/configuration/hello_world_config.json +``` + +The bin/apicast executable starts the APIcast server. Set log-level to debug which results in a large amount of debug logging. +If the amount of debug logging is too large, you can set the log-level to **notice**. This results in fewer log lines, but custom log entries in the policy are still logged. + +### Executing test requests +Now that you have the APIcast server up and running, you can test if the hello_world policy works. + +To test this, you must issue an HTTP request to the APIcast server, but the development Docker container does not expose any ports. You can alter the makefile or the Docker Compose file to expose the ports. In this example you will create another bash session in the development container and issue a curl request from inside the container. + +Create a second bash session in a new terminal window and find the Docker container ID of the APIcast development image using the following command: + +```shell +$ docker ps +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +5a72c49671c5 quay.io/3scale/s2i-openresty-centos7:master "container-entrypoin…" 2 hours ago Up 2 hours 8080/tcp apicast_build_0_development_1_802efce654d5 +366c62d0bccf redis "docker-entrypoint.s…" 2 hours ago Up 2 hours 6379/tcp apicast_build_0_redis_1_469bce65a85a +``` + +The make development command used to start the APIcast development container starts two containers. One with the APIcast development environment and another with a Redis cache. You need the container with the following image: **quay.io/3scale/s2i-openresty-centos7:master**. + +In the above example, it has the ID of **5a72c49671c5**. Yours will be different. Now that you know the ID of the container, create a new bash session using the following command: + +```shell +$ docker exec -it 5a72c49671c5 /bin/bash +``` + +Now you have another interactive bash shell in the APIcast development container, you can issue the HTTP request to test the policy from there. + +In the container issue the following HTTP request: + +```shell +$ curl localhost:8080 + +Codestin Search App + +

403 Forbidden

+
openresty/1.13.6.2
+ + +``` + +The response will be a 403 Forbidden. Look at the logs to see what has happened. + +```shell +$ bin/apicast --log-level=notice --dev -c examples/configuration/hello_world_config.json +loading production environment configuration: /home/centos/gateway/config/production.lua +loading development environment configuration: /home/centos/gateway/config/development.lua +2019/04/29 09:32:33 [notice] 257#257: [lua] environment.lua:194: add(): loading environment configuration: /home/centos/gateway/config/production.lua +2019/04/29 09:32:33 [notice] 257#257: [lua] environment.lua:194: add(): loading environment configuration: /home/centos/gateway/config/development.lua +2019/04/29 09:32:33 [notice] 257#257: using the "epoll" event method +2019/04/29 09:32:33 [notice] 257#257: openresty/1.13.6.2 +2019/04/29 09:32:33 [notice] 257#257: built by gcc 4.8.5 20150623 (Red Hat 4.8.5-28) (GCC) +2019/04/29 09:32:33 [notice] 257#257: OS: Linux 4.4.0-146-generic +2019/04/29 09:32:33 [notice] 257#257: getrlimit(RLIMIT_NOFILE): 1048576:1048576 +2019/04/29 09:43:17 [notice] 257#257: *6 [lua] hello_world.lua:53: request is not authorized, secrets do not match, client: 127.0.0.1, server: _, request: "GET / HTTP/1.1", host: "localhost:8080" +[29/Apr/2019:09:43:17 +0000] localhost:8080 127.0.0.1:40946 "GET / HTTP/1.1" 403 175 (0.000) 0 +``` + +APIcast is running with the notice log-level, when started with the debug log-level considerable more log events will be present. The one you need is the second from the bottom. + +Which states: **‘request is not authorized, secrets do not match’**. This is put in the log by the following line in the policy code: + +```lua +if secret_header ~= self.secret then + ngx.log(ngx.NOTICE, "request is not authorized, secrets do not match") + ngx.status = 403 + return ngx.exit(ngx.status) +``` + +The policy is now executing. You must provide the secret header in the request to pass the validation. Issue the following HTTP request: + +```shell +$ curl localhost:8080 -H 'secret: mysecret' +GET / HTTP/1.1 +X-Real-IP: 127.0.0.1 +Host: echo +User-Agent: curl/7.29.0 +Accept: */* +secret: mysecret +``` + +You should receive a valid 200 response from the echo server. The rewrite of query parameters to header is not tested, since the request did not contain any query parameters. Issue a new request with a query parameter to see the transformation at work. Issue the following request: + +```shell +$ curl localhost:8080?myparam=myvalue -H 'secret: mysecret' +GET /?myparam=myvalue HTTP/1.1 +X-Real-IP: 127.0.0.1 +Host: echo +User-Agent: curl/7.29.0 +Accept: */* +secret: mysecret +myparam: myvalue +``` + +You will see in the response, the header: myparam:myheader \ No newline at end of file diff --git a/doc/policy_tutorial/POLICY_SCAFFOLD.md b/doc/policy_tutorial/POLICY_SCAFFOLD.md new file mode 100644 index 000000000..852cbb415 --- /dev/null +++ b/doc/policy_tutorial/POLICY_SCAFFOLD.md @@ -0,0 +1,94 @@ +# Generate a policy scaffold +In the first part of the tutorial about APIcast policy development, you set up a development environment. Now you have a functioning development environment, you can start development of the APIcast policy. You will use the scaffolding utility provided by APIcast to generate a policy scaffold. + +First, create a new git branch of the APIcast source cloned in the previous part. This is an optional step, but developing a new feature or changing code in general in a new branch is a good habit to get into. Create a new branch and start the development container. + +```shell +$ git checkout -b policy-development-tutorial +Switched to a new branch 'policy-development-tutorial' +$ make development +``` + +To generate the scaffold of the policy, use the APIcast utility located in the bin/ directory of the development container. +In the development container, run the following command: + +```shell +$ bin/apicast generate policy hello_world +``` + +where hello_world is the name of the policy. + +```shell +bash-4.4$ bin/apicast generate policy hello_world +source: /home/centos/examples/scaffold/policy +destination: /home/centos + +exists: spec +exists: spec/policy +created: spec/policy/hello_world +created: spec/policy/hello_world/hello_world_spec.lua +exists: t +created: t/apicast-policy-hello_world.t +exists: gateway +exists: gateway/src +exists: gateway/src/apicast +exists: gateway/src/apicast/policy +created: gateway/src/apicast/policy/hello_world +created: gateway/src/apicast/policy/hello_world/apicast-policy.json +created: gateway/src/apicast/policy/hello_world/init.lua +created: gateway/src/apicast/policy/hello_world/hello_world.lua +``` + +You will see from the output of the generate policy command, files have been created. These artefacts related to the policy, are located in three different directories: + +* t/ – this directory contains all Nginx integration tests +* src/gateway/apicast/policy – this directory contains the source code and configuration schemas of all policies. Our policy resides in the subdirectory of hello_world +* spec/policy – this directory contains the unit tests of all policies. The unit tests for our policy resides in the subdirectory of hello_world + +The policy scaffolding utility not only generates a scaffold for the policy, but also the files for a configuration schema, unit tests, and integration tests. + +The source code of the policy in the directory src/gateway/apicast/policy/hello_world contains three files. + +* init.lua: All policies contain this init.lua file. It contains 1 line importing (required in Lua) our policy. It should not be modified. +* aplicast-policy.json: The APIcast gateway is configured using a JSON document. Policies requiring configuration also use this JSON document. The apicast-policy.json file is a JSON schema file where configuration properties for the policy can be defined. The next section looks into configuration properties and this file in more detail. +```json +{ + "$schema": "http://apicast.io/policy-v1/schema#manifest#", + "name": "hello_world", + "summary": "TODO: write policy summary", + "description": [ + "TODO: Write policy description" + ], + "version": "builtin", + "configuration": { + "type": "object", + "properties": { } + } +} +``` +* hello_world.lua: This is the actual source code of the policy, which at the moment does not contain much. +```lua +-- This is a hello_world description. +local policy = require('apicast.policy') +local _M = policy.new('hello_world') +local new = _M.new +--- Initialize a hello_world +-- @tparam[opt] table config Policy configuration. +function _M.new(config) + local self = new(config) + return self +end +return _M +``` + +The first two lines import the APIcast policy module and instantiate a new policy with hello_world as an argument. This returns a module and implements it using a Lua table. Lua is not an object oriented language, but tables, especially metatables, can mimic objects. The third line stores a reference to a function new, which is defined below. The new function takes a configuration variable as an argument, but for now nothing happens with is. The new method returns itself. Finally, the module representing the policy is returned. This is done so other components importing this policy module retrieve the table and can invoke all functions and variables stored in the policy. +We won’t cover all the files in details here since we are going to touch these in upcoming series when we flesh out our policy with functionality. +As a final verification to check if everything is working, run the unit tests again. + +``` +bash-4.4$ make busted +``` + +You will see the number of successes in the unit test outcome will have increased by 2 after generating the scaffold for the policy. + +In the next part you will create the implementation of the policy. This is described [here](POLICY_IMPLEMENTATION.md) diff --git a/doc/policy_tutorial/README.md b/doc/policy_tutorial/README.md new file mode 100644 index 000000000..087922961 --- /dev/null +++ b/doc/policy_tutorial/README.md @@ -0,0 +1,29 @@ +# APIcast policy development tutorial +This repository contains the code and configuration of an APIcast policy used in the tutorial described in this README. +Its purposes are to provide a first introduction to the world of policy development. + +In this tutorial you will dive into the development and testing of a custom APIcast policy. In the first part you will setup a development environment so you can start the development of the policy. + +Before beginning, first take a look what an APIcast policy is, described [here](../policies.md). + +The APIcast gateway is based on [NGINX](https://www.nginx.com/) and more specifically [OpenResty](http://openresty.org/en/), which is a distribution of NGINX compiled with various modules, most notable the [lua-nginx-module](https://github.com/openresty/lua-nginx-module). + +The lua-nginx-module provides the ability to enhance a NGINX server by executing scripts using the [Lua programming language](https://www.lua.org/). This is done by providing a Lua hook for each of the NGINX phases. NGINX works using an event loop and a state model where every request, as well as the starting of the server and its worker processes, goes through various phases. Each phase can execute a specific Lua function. + +An overview of the various phases and corresponding Lua hooks was in the README of the lua-nginx-module: https://github.com/openresty/lua-nginx-module#directives + +![Nginx phases](img/nginx-phases.png) + +Since the APIcast gateway uses OpenResty, a way to leverage these Lua hooks in the NGINX server is provided by something called policies. As described in the APIcast README: + +**“The behaviour of APIcast is customizable via policies. A policy basically tells APIcast what it should do in each of the NGINX phases.”** + +The code in this repo follows the APIcast directory structure. +To use this code it must be integrated in the APIcast code. +Therefore the code and configuration in this repository act for reference purposes only. + +The tutorial was 4 distinct sections: +1. [setup the development environment](DEV_ENV_SETUP.md) +2. [generate a policy scaffold](POLICY_SCAFFOLD.md) +3. [create and test the policy](POLICY_IMPLEMENTATION.md) +4. [run the policy locally](POLICY_RUN_LOCALLY.md) \ No newline at end of file diff --git a/doc/policy_tutorial/apicast/examples/configuration/hello_world_config.json b/doc/policy_tutorial/apicast/examples/configuration/hello_world_config.json new file mode 100644 index 000000000..b85a746e0 --- /dev/null +++ b/doc/policy_tutorial/apicast/examples/configuration/hello_world_config.json @@ -0,0 +1,29 @@ +{ + "services": [ + { + "proxy": { + "policy_chain": [ + { + "name": "hello_world", + "version": "builtin", + "configuration": { + "overwrite": true, + "secret": "mysecret" + } + }, + { + "name": "apicast.policy.upstream", + "configuration": { + "rules": [ + { + "regex": "/", + "url": "http://echo:8081" + } + ] + } + } + ] + } + } + ] +} diff --git a/doc/policy_tutorial/apicast/gateway/src/apicast/policy/hello_world/apicast-policy.json b/doc/policy_tutorial/apicast/gateway/src/apicast/policy/hello_world/apicast-policy.json new file mode 100644 index 000000000..9978e1ae9 --- /dev/null +++ b/doc/policy_tutorial/apicast/gateway/src/apicast/policy/hello_world/apicast-policy.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://apicast.io/policy-v1/schema#manifest#", + "name": "hello_world", + "summary": "Parameter converter and secret checker", + "description": [ + "Modifies HTTP query parameters in a request to HTTP headers. And checks the existence of the secret header." + ], + "version": "builtin", + "configuration": { + "type": "object", + "properties": { + "overwrite": { + "description": "Overwrite flag used to indicate whether or not an existing header must be overwritten by this policy. The default is true.", + "type": "boolean" + }, + "secret": { + "description": "The additional static secret used for verifying the request.", + "type": "string" + } + } + } +} diff --git a/doc/policy_tutorial/apicast/gateway/src/apicast/policy/hello_world/hello_world.lua b/doc/policy_tutorial/apicast/gateway/src/apicast/policy/hello_world/hello_world.lua new file mode 100644 index 000000000..7cda4602d --- /dev/null +++ b/doc/policy_tutorial/apicast/gateway/src/apicast/policy/hello_world/hello_world.lua @@ -0,0 +1,61 @@ +-- This is a hello_world description. + +local policy = require('apicast.policy') +local _M = policy.new('hello_world') + +local new = _M.new +--- Initialize a hello_world +-- @tparam [ o p t ] table config Policy configuration. +function _M.new(config) + local self = new(config) + + if config then + + if config.overwrite == nil then + self.overwrite = true + else + self.overwrite = config.overwrite + end + self.secret = config.secret + end + + return self +end + +local function paramsToHeaders(query_params, overwrite) + for k, v in pairs(query_params) do + if overwrite == false and ngx.req.get_headers()[k] ~= nil then + ngx.log(ngx.NOTICE, "existing header found with name " .. k .. " but not overwritten because of setting overwrite is " .. tostring(overwrite)) + else + ngx.req.set_header(k, v) + end + end + + ngx.req.set_uri_args = nil +end + +function _M:rewrite(context) + + --read HTTP query params as Lua table + local query_params = ngx.req.get_uri_args() + + paramsToHeaders(query_params, self.overwrite) + + local secret_header = ngx.req.get_headers()["secret"] + context.secret_header = secret_header +end + +function _M:access(context) + + local secret_header = context.secret_header + + if secret_header ~= self.secret then + ngx.log(ngx.NOTICE, "request is not authorized, secrets do not match") + ngx.status = 403 + return ngx.exit(ngx.status) + else + ngx.log(ngx.NOTICE, "request is authorized, secrets match") + end +end + +return _M \ No newline at end of file diff --git a/doc/policy_tutorial/apicast/gateway/src/apicast/policy/hello_world/init.lua b/doc/policy_tutorial/apicast/gateway/src/apicast/policy/hello_world/init.lua new file mode 100644 index 000000000..af52d25dc --- /dev/null +++ b/doc/policy_tutorial/apicast/gateway/src/apicast/policy/hello_world/init.lua @@ -0,0 +1 @@ +return require('hello_world') diff --git a/doc/policy_tutorial/apicast/spec/policy/hello_world/hello_world_spec.lua b/doc/policy_tutorial/apicast/spec/policy/hello_world/hello_world_spec.lua new file mode 100644 index 000000000..e193be4c1 --- /dev/null +++ b/doc/policy_tutorial/apicast/spec/policy/hello_world/hello_world_spec.lua @@ -0,0 +1,170 @@ +local _M = require('apicast.policy.hello_world') + +describe('hello_world policy', function() + describe('.new', function() + it('works without configuration', function() + assert(_M.new()) + end) + + it('accepts configuration', function() + assert(_M.new({ overwrite = false, secret = "mysecret"})) + end) + end) + + describe('rewrite with overwrite', function() + local config = { overwrite = true, secret = "mysecret" } + + local ngx_req_params = {} + local ngx_req_headers = {} + local context = {} + + before_each(function() + + stub(ngx.req,'get_headers', function() + return ngx_req_headers + end) + + stub(ngx.req, 'set_header', function(name, value) + ngx_req_headers[name] = value + end) + + stub(ngx.req, 'get_uri_args', function() + return ngx_req_params + end) + end) + + it('test single param', function() + local hello_world_policy = _M.new(config) + + --create the test request + ngx_req_params["testkey"] = 'testvalue' + + --execute the policy function + hello_world_policy:rewrite(context) + + --retrieve the http header and verify the content + local responseheader = ngx.req.get_headers()["testkey"] + assert.same("testvalue", responseheader) + + end) + + it('test overwrite header', function() + local hello_world_policy = _M.new(config) + + --create the test request + ngx_req_params["testkey"] = 'testvalue' + ngx_req_headers["testkey"] = 'myheader' + + --execute the policy function + hello_world_policy:rewrite(context) + + --retrieve the http header and verify the content + local responseheader = ngx.req.get_headers()["testkey"] + assert.same("testvalue", responseheader) + + end) + + it('test multiple params', function() + local hello_world_policy = _M.new(config) + + --create test request + ngx_req_params["param1"] = "value1" + ngx_req_params["param2"] = "value2" + + --execute the policy function + hello_world_policy:rewrite(context) + + --retrieve the http headers and verify the content + local header1 = ngx.req.get_headers()["param1"] + local header2 = ngx.req.get_headers()["param2"] + assert.same("value1", header1) + assert.same("value2", header2) + + end) + + end) + + describe('rewrite without overwrite',function() + local config = { overwrite = false, secret = "mysecret" } + + local ngx_req_params = {} + local ngx_req_headers = {} + + local context = {} + + before_each(function() + + stub(ngx.req,'get_headers', function() + return ngx_req_headers + end) + + stub(ngx.req, 'set_header', function(name, value) + ngx_req_headers[name] = value + end) + + stub(ngx.req, 'get_uri_args', function() + return ngx_req_params + end) + + end) + + it('test with single param', function() + local hello_world_policy = _M.new(config) + + --create the test request + ngx_req_params["testkey"] = 'testvalue' + ngx_req_headers["testkey"] = 'myheader' + + --execute the policy function + hello_world_policy:rewrite(context) + + --retrieve the http header and verify the content + local responseheader = ngx.req.get_headers()["testkey"] + assert.same("myheader", responseheader) + end) + + end) + + describe('secret header test', function() + local config = {secret = "mysecret" } + local ngx_req_headers = {} + local ngx_req_params = {} + + local context = {} + + before_each(function() + stub(ngx.req,'get_headers', function() + return ngx_req_headers + end) + + stub(ngx.req, 'set_header', function(name, value) + ngx_req_headers[name] = value + end) + + stub(ngx.req, 'get_uri_args', function() + return ngx_req_params + end) + end) + + it('authorized', function() + local hello_world_policy = _M.new(config) + + ngx_req_headers["secret"] = "mysecret" + + hello_world_policy:rewrite(context) + hello_world_policy:access(context) + end) + + it('not authorized', function() + local hello_world_policy = _M.new(config) + + ngx_req_headers["secret"] = "myownsecret" + + hello_world_policy:rewrite(context) + hello_world_policy:access(context) + + assert.same(ngx.status, 403) + end) + + end) +end) diff --git a/doc/policy_tutorial/img/apicast-dev-container-mount.png b/doc/policy_tutorial/img/apicast-dev-container-mount.png new file mode 100644 index 000000000..d0b8398a1 Binary files /dev/null and b/doc/policy_tutorial/img/apicast-dev-container-mount.png differ diff --git a/doc/policy_tutorial/img/nginx-phases.png b/doc/policy_tutorial/img/nginx-phases.png new file mode 100644 index 000000000..39c345f8f Binary files /dev/null and b/doc/policy_tutorial/img/nginx-phases.png differ diff --git a/doc/policy_tutorial/img/policy_layout.png b/doc/policy_tutorial/img/policy_layout.png new file mode 100644 index 000000000..1e2233a2f Binary files /dev/null and b/doc/policy_tutorial/img/policy_layout.png differ