cfft is a testing tool for CloudFront Functions.
cfft is a testing tool for CloudFront Functions. cfft helps you to test CloudFront Functions in development stage.
cfft supports the following features.
- Initialize files for testing CloudFront Functions.
- Test CloudFront Functions in development stage.
- Compare the result with the expect object.
- Ignore fields in the expect object.
- Diff function code.
- Publish function.
- Output JSON for Terraform. See Cooperate with Terraform.
cfft supports management of CloudFront KeyValueStore. See Use CloudFront KeyValueStore.
$ brew install fujiwara/tap/cfft
Download the binary from GitHub Releases.
aquaproj supports cfft. fujiwara/cfft
is available in aqua-registry.
$ aqua init
$ aqua g -i fujiwara/cfft
Usage: cfft <command>
Flags:
-h, --help Show context-sensitive help.
-c, --config="cfft.yaml" config file
--debug enable debug log
--log-format="text" log format (text,json)
Commands:
test
test function
init --name=STRING
initialize files
diff
diff function code
publish
publish function
kvs list
list key values
kvs get <key>
get value of key
kvs put <key> <value>
put value of key
kvs delete <key>
delete key
kvs info
show info of key value store
render
render function code
tf
output JSON for tf
version
show version
Run "cfft <command> --help" for more information on a command.
cfft init
creates a config file and a function file and an example event file.
Usage: cfft init --name=STRING
initialize function
Flags:
-h, --help Show context-sensitive help.
-c, --config="cfft.yaml" config file
--name=STRING function name
--format="json" output event file format (json,jsonnet,yaml)
--event-type="viewer-request" event type (viewer-request,viewer-response)
If the function is already exists in the CloudFront Functions, cfft downloads the function code and creates a config file.
If the function is not found, cfft creates a new config file and a function file and example event file. You can edit the function file and event file and test the function with cfft test --create-if-missing
.
cfft test
executes CloudFront Functions in the DEVELOPMENT stage and compares the result with the expect object if specified.
Usage: cfft test
test function
Flags:
-h, --help Show context-sensitive help.
-c, --config="cfft.yaml" config file
--create-if-missing create function if missing
See examples/add-cache-control directory.
# cfft.yaml
name: my-function
function: function.js
testCases:
- name: add-cache-control
event: event.json
// function.js
async function handler(event) {
const response = event.response;
const headers = response.headers;
// Set the cache-control header
headers['cache-control'] = { value: 'public, max-age=63072000' };
console.log('[on the edge] Cache-Control header set.');
// Return response to viewers
return response;
}
event.json
{
"version": "1.0",
"context": {
"eventType": "viewer-response"
},
"viewer": {
"ip": "1.2.3.4"
},
"request": {
"method": "GET",
"uri": "/index.html",
"headers": {},
"cookies": {},
"querystring": {}
},
"response": {
"statusCode": 200,
"statusDescription": "OK",
"headers": {},
"cookies": {}
}
}
$ cfft test
2024-01-19T22:35:26+09:00 [info] function my-function found
2024-01-19T22:35:26+09:00 [info] function code is not changed
2024-01-19T22:35:26+09:00 [info] [testcase:add-cache-control] testing function
2024-01-19T22:35:26+09:00 [info] [testcase:add-cache-control] ComputeUtilization: 31 optimal
2024-01-19T22:35:26+09:00 [info] [testcase:add-cache-control] [from:my-function] [on the edge] Cache-Control header set.
2024-01-19T22:35:26+09:00 [info] [testcase:add-cache-control] OK
2024-01-19T22:35:27+09:00 [info] 1 testcases passed
cfft executes my-function
with event.json
at CloudFront Functions in development stage.
About an event object, see also CloudFront Functions event structure.
- If my-function is not found,
cfft test --create-if-missing
creates a new function with the name and runtimecloudfront-js-2.0
. - If the function is found and the code is different from the
function.js
, cfft updates the function code. - Shows logs and compute utilization of the function after the execution.
When you specify the expect
element in test cases, cfft compares the result with the expect object.
# cfft.yaml
name: my-function
function: function.js
testCases:
- name: add-cache-control
event: event.json
expect: expect.json
If the result is different from the expect.json
, cfft exits with a non-zero status code.
2024-01-19T22:39:33+09:00 [info] function my-function found
2024-01-19T22:39:33+09:00 [info] function code or kvs association is changed, updating...
2024-01-19T22:39:34+09:00 [info] [testcase:add-cache-control] testing function
2024-01-19T22:39:35+09:00 [info] [testcase:add-cache-control] ComputeUtilization: 29
2024-01-19T22:39:35+09:00 [info] [testcase:add-cache-control] [from:my-function] [on the edge] Cache-Control header set.
--- expect
+++ actual
@@ -4,7 +4,7 @@
"statusDescription": "OK",
"headers": {
"cache-control": {
- "value": "public, max-age=6307200"
+ "value": "public, max-age=63072000"
}
}
}
2024-01-19T22:39:35+09:00 [error] failed to run test case add-cache-control, expect and actual are not equal
expect.json
{
"response": {
"headers": {
"cache-control": {
"value": "public, max-age=6307200"
}
},
"statusDescription": "OK",
"cookies": {},
"statusCode": 200
}
}
If you want to ignore some fields in the expect object, you can use the ignore
element in test cases.
# cfft.yaml
name: my-function
function: function.js
testCases:
- name: add-cache-control
event: event.json
expect: expect.json
ignore: ".response.cookies, .response.headers.date"
The .response.cookies
and .response.headers.date
are ignored in the expect object.
The event and expect file format is JSON, Jsonnet or YAML.
# cfft.yaml
name: my-function
function: function.js
testCases:
- name: add-cache-control
event: event.jsonnet
expect: expect.yaml
cfft supports the following file extensions.
- .json
- .jsonnet
- .yaml
- .yml
cfft supports an HTTP text format for Request and Response objects.
The following example is the HTTP text format of the Request object.
GET /index.html HTTP/1.1
Host: example.com
The request object is converted to the following JSON object.
{
"method": "GET",
"uri": "/index.html",
"headers": {
"host": {
"value": "example.com"
}
}
}
The following example is the HTTP text format of the Response object.
HTTP/1.1 302 Found
Location: https://example.com/
The response object is converted to the following JSON object.
{
"statusCode": 302,
"statusDescription": "Found",
"headers": {
"location": {
"value": "https://example.com/"
}
}
}
You can convert from HTTP text to JSON object with cfft util parse-request
and cfft util parse-response
commands.
$ cfft util parse-request < request.txt
{
"method": "GET",
"uri": "/index.html",
"headers": {
"host": {
"value": "example.com"
}
}
}
For use of the text format, I recommend using YAML or Jsonnet format for the event and expect files instead of plain JSON. YAML and Jsonnet support multiline strings.
# event.yaml
---
version: "1.0"
context:
eventType: viewer-response
viewer:
ip: 1.2.3.4
request: |
GET /index.html HTTP/1.1
Host: example.com
response: |
HTTP/1.1 302 Found
Location: https://example.com/
{
version: '1.0',
context: {
eventType: 'viewer-response',
},
viewer: {
ip: '1.2.3.4',
},
request: |||
GET /index.html HTTP/1.1
Host: example.com
|||,
response: |||
HTTP/1.1 302 Found
Location: https://example.com/
|||,
}
cfft supports chaining multiple functions. The feature is useful to test the combined function.
# cfft.yaml
name: my-function
runtime: cloudfront-js-2.0 # required
function:
event-type: viewer-request
functions:
- function1.js
- function2.js
filter_command: "npx esbuild --minify"
testCases:
## ...
The runtime
must be cloudfront-js-2.0
.
function
element allows you to specify multiple function files.event-type
must beviewer-request
orviewer-response
. required.functions
element is an array of function files.filter_command
is a command to filter the chained function code. optional.
When you specify the multiple functions
in function
, cfft automatically creates a combined function chained with all functions.
The combined function works as the following steps.
- The first function in the
functions
array is evaluated. - The result of the first function is passed to the second function.
- ...(repeat)
When the event-type is viewer-response
and any step returns a response object(includes statusCode
), the response object is returned to the viewer immidiately. The following functions are not evaluated.
The filter_command
is a command to filter the chained function code. The command must accepts the function code from stdin and outputs the filtered function code to stdout. For example, use npx esbuild --minify
to minify the function code.
Note: esbuild --minify
may change identifiers in js code, so it may not work for js file includes import
syntax.
You can review the generated combined function code with cfft render
command.
cfft supports CloudFront KeyVakueStore.
# cfft.yaml
name: function-with-kvs
function: function.js
kvs:
name: hostnames
If you specify the kvs
element in the config file, cfft test --create-if-missing
creates a KeyValueStore with the name if not exsites, and associates the KeyValueStore with the function. You can use the KeyValueStore in the function code.
In a function code, the KVS id is available in the KVS_ID
environment variable.
import cf from 'cloudfront';
const kvsId = "{{ must_env `KVS_ID` }}";
const kvsHandle = cf.kvs(kvsId);
async function handler(event) {
const request = event.request;
const clientIP = event.viewer.ip;
const hostname = (await kvsHandle.exists(clientIP)) ? await kvsHandle.get(clientIP) : 'unknown';
request.headers['x-hostname'] = { value: hostname };
return request;
}
cfft kvs
command manages KVS key values.
cfft kvs list
lists all key values.cfft kvs get <key>
gets the value of the key.cfft kvs put <key> <value>
puts the value of the key.cfft kvs delete <key>
deletes the key.cfft kvs info
shows the information of the KeyValueStore.
cfft diff
compares the function code with the code in the CloudFront Functions in the "DEVELOPMENT" stage.
$ cfft diff
2024-01-19T22:41:18+09:00 [info] function my-function found
--- E3UN6WX5RRO2AG
+++ function.js
@@ -1,5 +1,5 @@
async function handler(event) {
const request = event.request;
- console.log('hello cfft world');
+ console.log('hello cfft');
return request;
}
cfft diff --live
compares the function code with the code in the CloudFront Functions in the "LIVE" stage.
cfft publish
publishes the function to the CloudFront Functions.
$ cfft publish
cfft publish
fails if the local function code differs from the CloudFront Functions code.
Before publishing the function, you need to run cfft diff
to check the difference and run cfft test
to check the function behavior.
cfft render
renders the function code or event object or expect object to STDOUT.
Usage: cfft render [<target>]
render function code
Arguments:
[<target>] render target (function,event,expect)
Flags:
--test-case="" test case name (for target event or expect)
$ cfft render
You can use cfft render
to check the function code after rendering the template syntax.
cfft render event --test-case=foo
renders the event object of the test case named 'foo'.
The --test-case
flag is available only for the event
and expect
targets. If --test-case
is not specified, cfft renders the event or expect object of the first test case.
cfft read files (config, function, event, and expect) with the following template syntax by kayac/go-config.
must_env
function renders the environment variable value.
{{ must_env `FOO` }}
If the environment variable FOO
is not set, cfft exits with a non-zero status code. You can use env
function to set a default value.
{{ env `BAR` `default_of_BAR` }}
See examples/true-client-ip directory to see how to use the template syntax.
testCases:
- name: localhost
event: event.json
expect: expect.json
env:
IP: 127.0.0.1
HOSTNAME: localhost
- name: home
event: event.json
expect: expect.json
env:
IP: 192.168.1.1
HOSTNAME: home
In testCases
, env
overrides the environment variables. These values are used in event.json
and expect.json
.
event.json
{
"version": "1.0",
"context": {
"eventType": "viewer-request"
},
"viewer": {
"ip": "{{ env `IP` `127.0.0.2` }}"
},
"request": {
"method": "GET",
"uri": "/index.html",
"headers": {},
"cookies": {},
"querystring": {}
}
}
expect.json
{
"request": {
"cookies": {},
"headers": {
"true-client-ip": {
"value": "{{ env `IP` `127.0.0.2` }}"
},
"x-hostname": {
"value": "{{ env `HOSTNAME` `unknown` }}"
}
},
"method": "GET",
"querystring": {},
"uri": "/index.html"
}
}
cfft is desined to use with Terraform.
cfft has two methods to cooperate with Terraform, cfft tf
generates tf.json, and cfft tf --external
generates JSON for Terraform's external data sources.
cfft tf
command outputs a JSON defines a Terraform aws_cloudfront_function resource. The JSON file is read by Terraform as JSON Configuration Syntax.
$ cfft tf > cff.tf.json
cff.tf.json
{
"//": "This file is generated by cfft. DO NOT EDIT.",
"resource": {
"aws_cloudfront_function": {
"some-function": {
"name": "some-function",
"runtime": "cloudfront-js-2.0",
"code": "....(function code)....",
"comment": "comment of the function",
}
}
}
}
Terraform creates or updates the function with the JSON. If you want to publish the function into the "LIVE" stage by terraform apply
, set cfft tf --publish
flag.
If you want to run cfft test
before terraform (plan|apply)
, execute cfft test --create-if-missing
to create a function in the DEVELOPMENT stage.
In this case, you have to define the import
block in a .tf
file because the function is already created by cfft, but Terraform does not know the function. After terraform apply
, you can remove the import
block.
import {
to = aws_cloudfront_function.some-function
id = "some-function"
}
When a function code contains ${
, this syntax conflicts with Terraform's interpolation syntax. In this case, cfft outputs the function code into Terraform's variable, and the aws_cloudfront_function resource refers to the variable.
The variable's default value is not parsed as Terraform's interpolation syntax. See also variable-blocks.
{
"//": "This file is generated by cfft. DO NOT EDIT.",
"variable": {
"code_of_some-function": {
"type": "string",
"default": "...(function code)..."
}
},
"resource": {
"aws_cloudfront_function": {
"some-function": {
"name": "some-function",
"code": "${var.code_of_some-function}",
"runtime": "cloudfront-js-2.0"
}
}
}
}
cfft tf --resource-name foo
outputs the JSON with the tf resource name foo
instead of the function name.
cfft tf --external
command outputs a JSON for Terraform external data sources.
$ cfft tf --external
{
"name": "some-function",
"code": "....(function code)....",
"comment": "comment of the function",
"runtime": "cloudfront-js-2.0"
}
You can define the aws_cloudfront_function
resource with the data.external
data source calling cfft tf --external
.
When you run terraform apply
, cfft tf --external
is executed and the function is created or updated. If publish
is true, Terraform will publish the function into the "LIVE" stage.
Note: cfft tf --external
does not output a publish
attribute because the external data source does not accept non-string values.
resource "aws_cloudfront_function" "some-function" {
name = data.external.some-function.result["name"]
runtime = data.external.some-function.result["runtime"]
code = data.external.some-function.result["code"]
comment = data.external.some-function.result["comment"]
publish = true
}
data "external" "some-function" {
program = ["cfft", "--config", "cfft.yaml", "tf", "--external"]
}
If you want to execute cfft test
before terraform apply
, or you use the KeyValueStore, cfft test --create-if-missing
creates a KeyValueStore and associates the KeyValueStore with the function. In this case, you have to define the import
block in a .tf
file because the function is already created by cfft, but Terraform does not know the function. After terraform apply
, you can remove the import
block.
import {
to = aws_cloudfront_function.some-function
id = "some-function"
}
MIT
Fujiwara Shunichiro