├── .circleci └── config.yml ├── .github └── settings.yml ├── .gitignore ├── .nvmrc ├── .snyk ├── CODEOWNERS ├── Makefile ├── README.md ├── art ├── superman.ascii └── tea.ascii ├── bin └── fastly-tools.js ├── int-tests └── fastlyApi.int.test.js ├── lib ├── exit.js ├── fastly │ ├── LICENSE.md │ └── lib │ │ ├── api.js │ │ └── index.js ├── loadVcl.js ├── logger.js └── symbols.js ├── main.js ├── package-lock.json ├── package.json ├── renovate.json ├── secret-squirrel.js ├── tasks └── deploy.js └── test ├── deploy.task.spec.js ├── fastly.test.js ├── fixtures ├── backends.json └── vcl │ └── main.vcl ├── helpers ├── createTestService.js └── index.js └── mocks └── fastly.mock.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # generator: n-circle2-cli 2 | # template: component 3 | 4 | references: 5 | 6 | container_config_node: 7 | &container_config_node 8 | working_directory: ~/project/build 9 | docker: 10 | - image: cimg/node:18.16 11 | 12 | workspace_root: &workspace_root ~/project 13 | 14 | attach_workspace: 15 | &attach_workspace 16 | attach_workspace: 17 | at: *workspace_root 18 | 19 | npm_cache_keys: 20 | &npm_cache_keys 21 | keys: 22 | - v10-dependency-npm-{{ checksum "package.json" }}- 23 | - v10-dependency-npm-{{ checksum "package.json" }} 24 | - v10-dependency-npm- 25 | 26 | cache_npm_cache: 27 | &cache_npm_cache 28 | save_cache: 29 | key: v10-dependency-npm-{{ checksum "package.json" }}-{{ epoch }} 30 | paths: 31 | - ./node_modules/ 32 | 33 | restore_npm_cache: 34 | &restore_npm_cache 35 | restore_cache: 36 | <<: *npm_cache_keys 37 | 38 | filters_only_main: 39 | &filters_only_main 40 | branches: 41 | only: main 42 | 43 | filters_ignore_tags: 44 | &filters_ignore_tags 45 | tags: 46 | ignore: /.*/ 47 | 48 | filters_version_tag: 49 | &filters_version_tag 50 | tags: 51 | only: 52 | - /^v?\d+\.\d+\.\d+(?:-beta\.\d+)?$/ 53 | branches: 54 | ignore: /.*/ 55 | 56 | filters_only_renovate_nori: 57 | &filters_only_renovate_nori 58 | branches: 59 | only: /(^renovate-.*|^nori/.*)/ 60 | 61 | filters_ignore_tags_renovate_nori: 62 | &filters_ignore_tags_renovate_nori 63 | tags: 64 | ignore: /.*/ 65 | branches: 66 | ignore: /(^renovate-.*|^nori/.*)/ 67 | 68 | version: 2.1 69 | 70 | orbs: 71 | node: circleci/node@4.6.0 72 | 73 | jobs: 74 | 75 | build: 76 | <<: *container_config_node 77 | steps: 78 | - checkout 79 | - run: 80 | name: Checkout next-ci-shared-helpers 81 | command: git clone --depth 1 82 | git@github.com:Financial-Times/next-ci-shared-helpers.git --branch unpin-heroku 83 | .circleci/shared-helpers 84 | - *restore_npm_cache 85 | - run: 86 | name: Install project dependencies 87 | command: make install 88 | - run: 89 | name: Run the project build task 90 | command: make build 91 | - run: 92 | name: shared-helper / generate-build-state-artifacts 93 | command: .circleci/shared-helpers/helper-generate-build-state-artifacts 94 | when: always 95 | - *cache_npm_cache 96 | - store_artifacts: 97 | path: build-state 98 | destination: build-state 99 | - persist_to_workspace: 100 | root: *workspace_root 101 | paths: 102 | - build 103 | 104 | test: 105 | <<: *container_config_node 106 | steps: 107 | - *attach_workspace 108 | - run: 109 | name: Run tests 110 | command: make test 111 | environment: 112 | JEST_JUNIT_OUTPUT: test-results/jest/results.xml 113 | MOCHA_FILE: test-results/mocha/results.xml 114 | - store_test_results: 115 | path: test-results 116 | - store_artifacts: 117 | path: test-results 118 | destination: test-results 119 | 120 | publish: 121 | <<: *container_config_node 122 | steps: 123 | - *attach_workspace 124 | - run: 125 | name: shared-helper / npm-store-auth-token 126 | command: .circleci/shared-helpers/helper-npm-store-auth-token 127 | - run: npx snyk monitor --org=customer-products 128 | --project-name=Financial-Times/fastly-tools 129 | - run: 130 | name: shared-helper / npm-version-and-publish-public 131 | command: .circleci/shared-helpers/helper-npm-version-and-publish-public 132 | 133 | workflows: 134 | 135 | version: 2 136 | 137 | build-test: 138 | when: 139 | not: 140 | equal: 141 | - scheduled_pipeline 142 | - << pipeline.trigger_source >> 143 | jobs: 144 | - build: 145 | filters: 146 | <<: *filters_ignore_tags_renovate_nori 147 | - test: 148 | requires: 149 | - build 150 | 151 | build-test-publish: 152 | when: 153 | not: 154 | equal: 155 | - scheduled_pipeline 156 | - << pipeline.trigger_source >> 157 | jobs: 158 | - build: 159 | filters: 160 | <<: *filters_version_tag 161 | - test: 162 | filters: 163 | <<: *filters_version_tag 164 | requires: 165 | - build 166 | - publish: 167 | context: npm-publish-token 168 | filters: 169 | <<: *filters_version_tag 170 | requires: 171 | - test 172 | 173 | renovate-nori-build-test: 174 | when: 175 | not: 176 | equal: 177 | - scheduled_pipeline 178 | - << pipeline.trigger_source >> 179 | jobs: 180 | - waiting-for-approval: 181 | type: approval 182 | filters: 183 | <<: *filters_only_renovate_nori 184 | - build: 185 | requires: 186 | - waiting-for-approval 187 | - test: 188 | requires: 189 | - build 190 | 191 | nightly: 192 | when: 193 | and: 194 | - equal: 195 | - scheduled_pipeline 196 | - << pipeline.trigger_source >> 197 | - equal: 198 | - nightly 199 | - << pipeline.schedule.name >> 200 | jobs: 201 | - build: 202 | context: next-nightly-build 203 | - test: 204 | requires: 205 | - build 206 | context: next-nightly-build 207 | 208 | notify: 209 | webhooks: 210 | - url: https://ft-next-webhooks.herokuapp.com/circleci2-workflow 211 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | _extends: github-apps-config-next 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | .eslintrc.js 3 | /node_modules 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, which patches or ignores known vulnerabilities. 2 | version: v1.13.5 3 | ignore: {} 4 | patch: {} 5 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/about-codeowners/ for more information about this file. 2 | 3 | * @financial-times/platforms 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | node_modules/@financial-times/n-gage/index.mk: 2 | npm install --no-save --no-package-lock @financial-times/n-gage 3 | touch $@ 4 | 5 | -include node_modules/@financial-times/n-gage/index.mk 6 | 7 | .PHONY: test 8 | 9 | unit-test: 10 | mocha --recursive 11 | 12 | test-int: 13 | mocha int-tests/ 14 | 15 | test: verify unit-test 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!WARNING]
2 | > This package is deprecated as of **2024-02-21**. The [Fastly monorepo](https://github.com/financial-times/fastly) is currently hosting the config for each service. The FT.com CDN config has been migrated to Terraform and it's managed by their own separate [repo](https://github.com/Financial-Times/ft.com-cdn). Consumers of this package should migrate their config to the monorepo. 3 | 4 | # fastly-tools [![CircleCI](https://circleci.com/gh/Financial-Times/fastly-tools.svg?style=svg&circle-token=33bcf2eb98fe2e875cc66de93d7e4a50369c952d)](https://circleci.com/gh/Financial-Times/fastly-tools) 5 | 6 | This library is a command line tool for interacting with the FT.com CDN, [Fastly](https://www.fastly.com/). 7 | 8 | 9 | ## Requirements 10 | 11 | * Node version defined by `engines.node` in `package.json`. Run command `nvm use` to switch your local Node version to the one specified in `.nvmrc`. 12 | 13 | 14 | ## Installation 15 | 16 | ```sh 17 | git clone git@github.com:Financial-Times/fastly-tools.git 18 | cd fastly-tools 19 | make install 20 | ``` 21 | 22 | 23 | ## Development 24 | 25 | ### Testing 26 | 27 | In order to run the tests locally you'll need to run: 28 | 29 | ```sh 30 | make test 31 | ``` 32 | 33 | ### Install from NPM 34 | 35 | ```sh 36 | npm install --save-dev @financial-times/fastly-tools 37 | ``` 38 | 39 | ### Usage 40 | 41 | ```sh 42 | Usage: fastly [options] [command] 43 | 44 | Options: 45 | -h, --help output usage information 46 | 47 | Commands: 48 | deploy [options] [folder] Deploys VCL in [folder] to the specified fastly service. Requires FASTLY_APIKEY env var which can be found in the repo's corresponding Vault directory. 49 | ``` 50 | 51 | ### Deploy 52 | 53 | ```sh 54 | Usage: deploy [options] [folder] 55 | 56 | Deploys VCL in [folder] to the specified fastly service. Requires FASTLY_APIKEY env var which can be found in the repo\'s corresponding Vault directory. 57 | 58 | Options: 59 | -m, --main
Set the name of the main vcl file (the entry point). Defaults to "main.vcl" 60 | -v, --vars A way of injecting environment vars into the VCL. So if you pass --vars AUTH_KEY,FOO the values {$AUTH_KEY} and ${FOO} in the vcl will be replaced with the values of the environment variable. If you include SERVICEID it will be populated with the current --service option 61 | -e, --env Load environment variables from local .env file (use when deploying from a local machine 62 | -s, --service REQUIRED. The ID of the fastly service to deploy to. 63 | -V --verbose Verbose log output 64 | -b --backends Upload the backends specified in via the api 65 | -k --api-keys list of alternate api keys to try should the key stored in process.env.FASTLY_API_KEY hit its rate limit 66 | --skip-conditions list of conditions to skip deleting 67 | -h, --help output usage information 68 | ``` 69 | 70 | ### Example 71 | 72 | For example to deploy to a given environment, you would use the following command: 73 | 74 | ```sh 75 | fastly-tools deploy -V --vars SERVICEID --main main.vcl --service ${FASTLY_STAGING_SERVICE_ID} --api-keys ${FASTLY_STAGING_APIKEY} --backends backends.js ./vcl/ 76 | ``` 77 | -------------------------------------------------------------------------------- /art/superman.ascii: -------------------------------------------------------------------------------- 1 | .=., 2 | ;c =\ 3 | __| _/ 4 | .'-'-._/-'-._ 5 | /.. ____ \\ 6 | /' _ [<_->] ) \\ 7 | ( / \\--\\_>/-/'._ ) 8 | \\-;_/\\__;__/ _/ _/ 9 | '._}|==o==\\{_\\/ 10 | / /-._.--\\ \\_ 11 | // / /| \\ \ \\ 12 | / | | | \\; | \\ \\ 13 | / / | :/ \\: \\ \\_\\ 14 | / | /.'| /: | \\ \\ 15 | | | |--| . |--| \\_\\ 16 | / _/ \ | : | /___--._) \\ 17 | |_(---'-| >-'-| | '-' 18 | /_/ \\_\\ 19 | -------------------------------------------------------------------------------- /art/tea.ascii: -------------------------------------------------------------------------------- 1 | ( ) ( ) ) 2 | ) ( ) ( ( 3 | ( ) ( ) ) 4 | _____________ 5 | <_____________> ___ 6 | | |/ _ \ 7 | | | | | 8 | | |_| | 9 | ___| |\___/ 10 | / \___________/ \ 11 | \_____________________/ 12 | -------------------------------------------------------------------------------- /bin/fastly-tools.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | const program = require('commander'); 4 | 5 | function list (val) { 6 | return val.split(','); 7 | } 8 | 9 | program 10 | .command('deploy [folder]') 11 | .description('Deploys VCL in [folder] to the specified fastly service. Requires FASTLY_APIKEY env var which can be found in the repo\'s corresponding Vault directory.') 12 | .option('-m, --main
', 'Set the name of the main vcl file (the entry point). Defaults to "main.vcl"') 13 | .option('-v, --vars ', 'A way of injecting environment vars into the VCL. So if you pass --vars AUTH_KEY,FOO the values {$AUTH_KEY} and ${FOO} in the vcl will be replaced with the values of the environmemnt variable. If you include SERVICEID it will be populated with the current --service option', list) 14 | .option('-e, --env', 'Load environment variables from local .env file (use when deploying from a local machine') 15 | .option('-s, --service ', 'REQUIRED. The ID of the fastly service to deploy to.') 16 | .option('-V --verbose', 'Verbose log output') 17 | .option('-b --backends ', 'Upload the backends specified in via the api') 18 | .option('-k --api-keys ', 'list of alternate api keys to try should the key stored in process.env.FASTLY_API_KEY hit its rate limit', list) 19 | .option('--skip-conditions ', 'list of conditions to skip deleting', list) 20 | .action(function (folder, options) { 21 | const deploy = require('../tasks/deploy'); 22 | const log = require('../lib/logger')({verbose:options.verbose, disabled:options.disableLogs}); 23 | const exit = require('../lib/exit')(log, true); 24 | 25 | const symbols = require('../lib/symbols'); 26 | deploy(folder, options).catch(err => { 27 | if(typeof err === 'string'){ 28 | log.error(err); 29 | }else if(err.type && err.type === symbols.VCL_VALIDATION_ERROR){ 30 | log.error('VCL Validation Error'); 31 | log.error(err.validation); 32 | }else{ 33 | log.error(err.stack); 34 | } 35 | exit('Bailing...', log); 36 | }); 37 | 38 | }); 39 | 40 | program.parse(process.argv); 41 | -------------------------------------------------------------------------------- /int-tests/fastlyApi.int.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | 'use strict'; 4 | const fetch = require('node-fetch'); 5 | const expect = require('chai').expect; 6 | 7 | function waitFor (ms){ 8 | return new Promise(resolve => setTimeout(resolve, ms)); 9 | } 10 | 11 | 12 | describe('Integration Tests', () => { 13 | 14 | describe('Fastly API', () => { 15 | 16 | let fastly; 17 | let testServiceId; 18 | 19 | function createTestService (){ 20 | return fetch('https://api.fastly.com/service', { 21 | method: 'POST', 22 | headers: { 23 | 'Fastly-Key' : process.env.FASTLY_APIKEY, 24 | 'Content-Type': 'application/x-www-form-urlencoded', 25 | 'Accept': 'application/json' 26 | }, 27 | body: 'name=pauls-test-service' 28 | }).then(response => response.json()); 29 | } 30 | 31 | function deleteTestService (){ 32 | return fetch('https://api.fastly.com/service/' + testServiceId, { 33 | method: 'DELETE', 34 | headers: { 35 | 'Fastly-Key' : process.env.FASTLY_APIKEY, 36 | 'Accept': 'application/json' 37 | } 38 | }); 39 | } 40 | 41 | before(() => { 42 | return createTestService() 43 | .then(svc => { 44 | console.log('--- test service created'); 45 | //console.log(svc); 46 | testServiceId = svc.id; 47 | fastly = require('fastly')(process.env.FASTLY_APIKEY, svc.id); 48 | }); 49 | }); 50 | 51 | after(() => { 52 | return deleteTestService().then(() => console.log('--- test service deleted')); 53 | }); 54 | 55 | describe('Backends', () => { 56 | 57 | it('Should be able to get all backends for service & version', () => { 58 | return fastly.getBackend('1') 59 | .then(response => { 60 | expect(response).to.exist; 61 | expect(response).to.be.an.instanceOf(Array); 62 | }); 63 | }); 64 | 65 | it('Should be able to create a new backend', () => { 66 | let name = 'foo'; 67 | let host = 'blah.ft.com'; 68 | return fastly.createBackend('1', {name:name, hostname:host}) 69 | .then(response => { 70 | expect(response).to.exist; 71 | expect(response.name).to.equal(name); 72 | expect(response.hostname).to.equal(host); 73 | }); 74 | }); 75 | 76 | it('Should be able to delete a backend', () => { 77 | let name = 'blah'; 78 | return fastly.createBackend('1', {name:name, hostname:'blah.blah.com'}) 79 | .then(backend => { 80 | return fastly.deleteBackendByName('1', backend.name); 81 | }) 82 | .then(() => fastly.getBackend('1')) 83 | .then(backends => { 84 | let names = backends.map(b => b.name); 85 | expect(names).not.to.contain(name); 86 | }); 87 | }); 88 | 89 | //todo - backends are limited to 5 unless you request so this test won't pass 90 | it.skip('Should be able to create 7 backends', () => { 91 | let items = [1,2,3,4,5,6,7]; 92 | let backends = items.map(i => { 93 | return {'name':`backend_${i}`, 'hostname': `blah${i}.ft.com`}; 94 | }); 95 | 96 | return Promise.all(backends.map(b => { 97 | return waitFor(1000).then(() => fastly.createBackend('1', b)); 98 | })) 99 | .then(() => { 100 | return fastly.getBackend('1'); 101 | }) 102 | .then(backends => { 103 | console.log(backends); 104 | expect(backends.length).to.equal(items.length); 105 | }); 106 | }); 107 | 108 | 109 | 110 | }); 111 | 112 | }); 113 | 114 | }); 115 | -------------------------------------------------------------------------------- /lib/exit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (log, actuallyExit) => { 4 | return msg => { 5 | log.error(msg); 6 | if (actuallyExit) { 7 | process.exit(1); 8 | } 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /lib/fastly/LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Andrew Sliwinski. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /lib/fastly/lib/api.js: -------------------------------------------------------------------------------- 1 | // Fastly API 2014-07-30, http://docs.fastly.com/api/config', 2 | 3 | // GET = get, POST = update, PUT = create, DELETE = delete 4 | 5 | module.exports = [ 6 | { 7 | method: 'GET', 8 | type: 'backend', 9 | fn: 'getBackendByName', 10 | url: '/service/%service_id/version/%version/backend/%name' 11 | }, 12 | { 13 | method: 'POST', 14 | type: 'backend', 15 | fn: 'createBackend', 16 | url: '/service/%service_id/version/%version/backend', 17 | }, 18 | { 19 | method: 'GET', 20 | type: 'backend', 21 | fn: 'getBackend', 22 | url: '/service/%service_id/version/%version/backend', 23 | }, 24 | { 25 | method: 'DELETE', 26 | type: 'backend', 27 | fn: 'deleteBackendByName', 28 | url: '/service/%service_id/version/%version/backend/%name', 29 | }, 30 | { 31 | method: 'PUT', 32 | type: 'backend', 33 | fn: 'updateBackend', 34 | url: '/service/%service_id/version/%version/backend/%old_name', 35 | }, 36 | { 37 | method: 'GET', 38 | type: 'billing', 39 | fn: 'getBillingByDate', 40 | url: '/billing/year/%year/month/%month', 41 | }, 42 | { 43 | method: 'GET', 44 | type: 'cache', 45 | fn: 'getCacheSettingsByName', 46 | url: '/service/%service_id/version/%version/cache_settings/%name', 47 | }, 48 | { 49 | method: 'GET', 50 | type: 'cache', 51 | fn: 'getCacheSettings', 52 | url: '/service/%service_id/version/%version/cache_settings', 53 | }, 54 | { 55 | method: 'POST', 56 | type: 'cache', 57 | fn: 'updateCacheSettings', 58 | url: '/service/%service_id/version/%version/cache_settings', 59 | }, 60 | { 61 | method: 'DELETE', 62 | type: 'cache', 63 | fn: 'deleteCacheSettings', 64 | url: '/service/%service_id/version/%version/cache_settings/%name', 65 | }, 66 | { 67 | method: 'PUT', 68 | type: 'cache', 69 | fn: 'deleteCacheSettings', 70 | url: '/service/%service_id/version/%version/cache_settings/%old_name', 71 | }, 72 | { 73 | method: 'GET', 74 | type: 'condition', 75 | fn: '', 76 | url: '/service/%service_id/version/%version/condition/%name', 77 | }, 78 | { 79 | method: 'GET', 80 | type: 'condition', 81 | fn: 'getConditions', 82 | url: '/service/%service_id/version/%version/condition', 83 | }, 84 | { 85 | method: 'POST', 86 | type: 'condition', 87 | fn: 'createCondition', 88 | url: '/service/%service_id/version/%version/condition', 89 | }, 90 | { 91 | method: 'DELETE', 92 | type: 'condition', 93 | fn: 'deleteCondition', 94 | url: '/service/%service_id/version/%version/condition/%name', 95 | }, 96 | { 97 | method: 'PUT', 98 | type: 'condition', 99 | fn: '', 100 | url: '/service/%service_id/version/%version/condition/%old_name', 101 | }, 102 | { 103 | method: 'GET', 104 | type: 'content', 105 | fn: '', 106 | url: '/content/edge_check', 107 | }, 108 | { 109 | method: 'GET', 110 | type: 'customer', 111 | fn: '', 112 | url: '/current_customer', 113 | }, 114 | { 115 | method: 'GET', 116 | type: 'customer', 117 | fn: '', 118 | url: '/customer/%id', 119 | }, 120 | { 121 | method: 'PUT', 122 | type: 'customer', 123 | fn: '', 124 | url: '/customer/%id', 125 | }, 126 | { 127 | method: 'GET', 128 | type: 'customer', 129 | fn: '', 130 | url: '/customer/%id/users', 131 | }, 132 | { 133 | method: 'DELETE', 134 | type: 'customer', 135 | fn: '', 136 | url: '/customer/%id', 137 | }, 138 | { 139 | method: 'GET', 140 | type: 'diff', 141 | fn: '', 142 | url: '/service/%service_id/diff/from/%from/to/%to', 143 | }, 144 | { 145 | method: 'GET', 146 | type: 'director', 147 | fn: '', 148 | url: '/service/%service_id/version/%version/director/%name', 149 | }, 150 | { 151 | method: 'GET', 152 | type: 'director', 153 | fn: '', 154 | url: '/service/%service_id/version/%version/director', 155 | }, 156 | { 157 | method: 'POST', 158 | type: 'director', 159 | fn: '', 160 | url: '/service/%service_id/version/%version/director', 161 | }, 162 | { 163 | method: 'DELETE', 164 | type: 'director', 165 | fn: '', 166 | url: '/service/%service_id/version/%version/director/%name', 167 | }, 168 | { 169 | method: 'PUT', 170 | type: 'director', 171 | fn: '', 172 | url: '/service/%service_id/version/%version/director/%name', 173 | }, 174 | { 175 | method: 'GET', 176 | type: 'director', 177 | fn: '', 178 | url: '/service/%service_id/version/%version/director/%director_name/backend/%backend_name', 179 | }, 180 | { 181 | method: 'POST', 182 | type: 'director', 183 | fn: '', 184 | url: '/service/%service_id/version/%version/director/%director_name/backend/%backend_name', 185 | }, 186 | { 187 | method: 'DELETE', 188 | type: 'director', 189 | fn: '', 190 | url: '/service/%service_id/version/%version/director/%director_name/backend/%backend_name', 191 | }, 192 | { 193 | method: 'GET', 194 | type: 'docs', 195 | fn: '', 196 | url: '/docs', 197 | }, 198 | { 199 | method: 'GET', 200 | type: 'docs', 201 | fn: '', 202 | url: '/docs/subject/%endpoint', 203 | }, 204 | { 205 | method: 'GET', 206 | type: 'docs', 207 | fn: '', 208 | url: '/docs/section/%section', 209 | }, 210 | { 211 | method: 'GET', 212 | type: 'domain', 213 | fn: '', 214 | url: '/service/%service_id/version/%version/domain/%name/check', 215 | }, 216 | { 217 | method: 'GET', 218 | type: 'domain', 219 | fn: '', 220 | url: '/service/%service_id/version/%version/domain/check_all', 221 | }, 222 | { 223 | method: 'GET', 224 | type: 'domain', 225 | fn: '', 226 | url: '/service/%service_id/version/%version/domain/%name', 227 | }, 228 | { 229 | method: 'POST', 230 | type: 'domain', 231 | fn: '', 232 | url: '/service/%service_id/version/%version/domain', 233 | }, 234 | { 235 | method: 'GET', 236 | type: 'domain', 237 | fn: '', 238 | url: '/service/%service_id/version/%version/domain', 239 | }, 240 | { 241 | method: 'DELETE', 242 | type: 'domain', 243 | fn: '', 244 | url: '/service/%service_id/version/%version/domain/%name', 245 | }, 246 | { 247 | method: 'PUT', 248 | type: 'domain', 249 | fn: '', 250 | url: '/service/%service_id/version/%version/domain/%old_name', 251 | }, 252 | { 253 | method: 'GET', 254 | type: 'event', 255 | fn: '', 256 | url: '/event_log/%id', 257 | }, 258 | { 259 | method: 'GET', 260 | type: 'header', 261 | fn: 'getHeader', 262 | url: '/service/%service_id/version/%version/header/%name', 263 | }, 264 | { 265 | method: 'GET', 266 | type: 'header', 267 | fn: 'getHeaders', 268 | url: '/service/%service_id/version/%version/header', 269 | }, 270 | { 271 | method: 'POST', 272 | type: 'header', 273 | fn: 'createHeader', 274 | url: '/service/%service_id/version/%version/header', 275 | }, 276 | { 277 | method: 'DELETE', 278 | type: 'header', 279 | fn: 'deleteHeader', 280 | url: '/service/%service_id/version/%version/header/%name', 281 | }, 282 | { 283 | method: 'PUT', 284 | type: 'header', 285 | fn: 'updateHeader', 286 | url: '/service/%service_id/version/%version/header/%old_name', 287 | }, 288 | { 289 | method: 'GET', 290 | type: 'healthcheck', 291 | fn: 'getHealthcheckByName', 292 | url: '/service/%service_id/version/%version/healthcheck/%name', 293 | }, 294 | { 295 | method: 'GET', 296 | type: 'healthcheck', 297 | fn: 'getHealthcheck', 298 | url: '/service/%service_id/version/%version/healthcheck', 299 | }, 300 | { 301 | method: 'POST', 302 | type: 'healthcheck', 303 | fn: 'createHealthcheck', 304 | url: '/service/%service_id/version/%version/healthcheck', 305 | 306 | }, 307 | { 308 | method: 'DELETE', 309 | type: 'healthcheck', 310 | fn: 'deleteHealthcheck', 311 | url: '/service/%service_id/version/%version/healthcheck/%name', 312 | }, 313 | { 314 | method: 'PUT', 315 | type: 'healthcheck', 316 | fn: 'updateHealthcheck', 317 | url: '/service/%service_id/version/%version/healthcheck/%old_name', 318 | }, 319 | { 320 | method: 'GET', 321 | type: 'invitation', 322 | fn: '', 323 | url: '/invitation', 324 | }, 325 | { 326 | method: 'POST', 327 | type: 'invitation', 328 | fn: '', 329 | url: '/invitation', 330 | }, 331 | { 332 | method: 'PUT', 333 | type: 'invitation', 334 | fn: '', 335 | url: '/invitation/%id/cancel', 336 | }, 337 | { 338 | method: 'GET', 339 | type: 'pricing', 340 | fn: '', 341 | url: '/customer/%customer_id/pricing_extra', 342 | }, 343 | { 344 | method: 'GET', 345 | type: 'pricing', 346 | fn: '', 347 | url: '/customer/%customer_id/pricing_extra/%name', 348 | }, 349 | { 350 | method: 'GET', 351 | type: 'request', 352 | fn: '', 353 | url: '/service/%service_id/version/%version/request_settings/%name', 354 | }, 355 | { 356 | method: 'GET', 357 | type: 'request', 358 | fn: '', 359 | url: '/service/%service_id/version/%version/request_settings', 360 | }, 361 | { 362 | method: 'POST', 363 | type: 'request', 364 | fn: '', 365 | url: '/service/%service_id/version/%version/request_settings', 366 | }, 367 | { 368 | method: 'DELETE', 369 | type: 'request', 370 | fn: '', 371 | url: '/service/%service_id/version/%version/request_settings/%name', 372 | }, 373 | { 374 | method: 'PUT', 375 | type: 'request', 376 | fn: '', 377 | url: '/service/%service_id/version/%version/request_settings/%old_name', 378 | }, 379 | { 380 | method: 'GET', 381 | type: 'response', 382 | fn: '', 383 | url: '/service/%service_id/version/%version/response_object/%name', 384 | }, 385 | { 386 | method: 'GET', 387 | type: 'response', 388 | fn: '', 389 | url: '/service/%service_id/version/%version/response_object', 390 | }, 391 | { 392 | method: 'POST', 393 | type: 'response', 394 | fn: '', 395 | url: '/service/%service_id/version/%version/response_object', 396 | }, 397 | { 398 | method: 'DELETE', 399 | type: 'response', 400 | fn: '', 401 | url: '/service/%service_id/version/%version/response_object/%name', 402 | }, 403 | { 404 | method: 'PUT', 405 | type: 'response', 406 | fn: '', 407 | url: '/service/%service_id/version/%version/response_object/%old_name', 408 | }, 409 | { 410 | method: 'GET', 411 | type: 'service', 412 | fn: 'getServices', 413 | url: '/service', 414 | }, 415 | { 416 | method: 'GET', 417 | type: 'service', 418 | fn: '', 419 | url: '/service/%id/details', 420 | }, 421 | { 422 | method: 'GET', 423 | type: 'service', 424 | fn: '', 425 | url: '/service/search', 426 | }, 427 | { 428 | method: 'GET', 429 | type: 'service', 430 | fn: '', 431 | url: '/service/%id', 432 | }, 433 | { 434 | method: 'POST', 435 | type: 'service', 436 | fn: '', 437 | url: '/service', 438 | }, 439 | { 440 | method: 'DELETE', 441 | type: 'service', 442 | fn: '', 443 | url: '/service/%id', 444 | }, 445 | { 446 | method: 'PUT', 447 | type: 'service', 448 | fn: '', 449 | url: '/service/%id', 450 | }, 451 | { 452 | method: 'GET', 453 | type: 'service', 454 | fn: '', 455 | url: '/service/%id/domain', 456 | }, 457 | { 458 | method: 'GET', 459 | type: 'settings', 460 | fn: '', 461 | url: '/service/%service_id/version/%version/settings', 462 | }, 463 | { 464 | method: 'PUT', 465 | type: 'settings', 466 | fn: '', 467 | url: '/service/%service_id/version/%version/settings', 468 | }, 469 | { 470 | method: 'GET', 471 | type: 'stats', 472 | fn: '', 473 | url: '/service/%service/stats/summary', 474 | }, 475 | { 476 | method: 'GET', 477 | type: 'stats', 478 | fn: '', 479 | url: '/service/%service/stats/%type', 480 | }, 481 | { 482 | method: 'POST', 483 | type: 'user', 484 | fn: '', 485 | url: '/current_user/password', 486 | }, 487 | { 488 | method: 'GET', 489 | type: 'user', 490 | fn: '', 491 | url: '/current_user', 492 | }, 493 | { 494 | method: 'GET', 495 | type: 'user', 496 | fn: '', 497 | url: '/user/%id', 498 | }, 499 | { 500 | method: 'POST', 501 | type: 'user', 502 | fn: '', 503 | url: '/user', 504 | }, 505 | { 506 | method: 'PUT', 507 | type: 'user', 508 | fn: '', 509 | url: '/user/%id', 510 | }, 511 | { 512 | method: 'DELETE', 513 | type: 'user', 514 | fn: '', 515 | url: '/user/%id', 516 | }, 517 | { 518 | method: 'POST', 519 | type: 'user', 520 | fn: '', 521 | url: '/user/%login/password/request_reset', 522 | }, 523 | { 524 | method: 'GET', 525 | type: 'vcl', 526 | fn: 'getVcl', 527 | url: '/service/%service_id/version/%version/vcl', 528 | }, 529 | { 530 | method: 'GET', 531 | type: 'vcl', 532 | fn: '', 533 | url: '/service/%service_id/version/%version/vcl/%name', 534 | }, 535 | { 536 | method: 'GET', 537 | type: 'vcl', 538 | fn: '', 539 | url: '/service/%service_id/version/%version/vcl/%name/content', 540 | }, 541 | { 542 | method: 'GET', 543 | type: 'vcl', 544 | fn: '', 545 | url: '/service/%service_id/version/%version/vcl/%name/download', 546 | }, 547 | { 548 | method: 'GET', 549 | type: 'vcl', 550 | fn: '', 551 | url: '/service/%service_id/version/%version/generated_vcl', 552 | }, 553 | { 554 | method: 'GET', 555 | type: 'vcl', 556 | fn: '', 557 | url: '/service/%service_id/version/%version/generated_vcl/content', 558 | }, 559 | { 560 | method: 'POST', 561 | type: 'vcl', 562 | fn: 'updateVcl', 563 | url: '/service/%service_id/version/%version/vcl', 564 | }, 565 | { 566 | method: 'DELETE', 567 | type: 'vcl', 568 | fn: 'deleteVcl', 569 | url: '/service/%service_id/version/%version/vcl/%name', 570 | }, 571 | { 572 | method: 'PUT', 573 | type: 'vcl', 574 | fn: 'setVclAsMain', 575 | url: '/service/%service_id/version/%version/vcl/%name/main', 576 | }, 577 | { 578 | method: 'PUT', 579 | type: 'vcl', 580 | fn: '', 581 | url: '/service/%service_id/version/%version/vcl/%old_name', 582 | }, 583 | { 584 | method: 'GET', 585 | type: 'version', 586 | fn: '', 587 | url: '/service/%service_id/version/%number', 588 | }, 589 | { 590 | method: 'POST', 591 | type: 'version', 592 | fn: 'createVersion', 593 | url: '/service/%service_id/version', 594 | }, 595 | { 596 | method: 'GET', 597 | type: 'version', 598 | fn: 'getVersions', 599 | url: '/service/%service_id/version', 600 | }, 601 | { 602 | method: 'PUT', 603 | type: 'version', 604 | fn: '', 605 | url: '/service/%service_id/version/%number', 606 | }, 607 | { 608 | method: 'PUT', 609 | type: 'version', 610 | fn: 'activateVersion', 611 | url: '/service/%service_id/version/%number/activate', 612 | }, 613 | { 614 | method: 'PUT', 615 | type: 'version', 616 | fn: '', 617 | url: '/service/%service_id/version/%number/deactivate', 618 | }, 619 | { 620 | method: 'PUT', 621 | type: 'version', 622 | fn: 'cloneVersion', 623 | url: '/service/%service_id/version/%number/clone', 624 | }, 625 | { 626 | method: 'GET', 627 | type: 'version', 628 | fn: 'validateVersion', 629 | url: '/service/%service_id/version/%number/validate', 630 | }, 631 | { 632 | method: 'PUT', 633 | type: 'version', 634 | fn: '', 635 | url: '/service/%service_id/version/%number/lock', 636 | }, 637 | { 638 | method: 'GET', 639 | type: 'wordpress', 640 | fn: '', 641 | url: '/service/%service_id/version/%version/wordpress/%name', 642 | }, 643 | { 644 | method: 'GET', 645 | type: 'wordpress', 646 | fn: '', 647 | url: '/service/%service_id/version/%version/wordpress', 648 | }, 649 | { 650 | method: 'POST', 651 | type: 'wordpress', 652 | fn: '', 653 | url: '/service/%service_id/version/%version/wordpress', 654 | }, 655 | { 656 | method: 'DELETE', 657 | type: 'wordpress', 658 | fn: '', 659 | url: '/service/%service_id/version/%version/wordpress/%name', 660 | }, 661 | { 662 | method: 'PUT', 663 | type: 'wordpress', 664 | fn: '', 665 | url: '/service/%service_id/version/%version/wordpress/%old_name', 666 | }, 667 | { 668 | method: 'GET', 669 | type: 'ftp', 670 | fn: 'getLoggingFtp', 671 | url: '/service/%service_id/version/%version/logging/ftp', 672 | }, 673 | { 674 | method: 'DELETE', 675 | type: 'ftp', 676 | fn: 'deleteLoggingFtpByName', 677 | url: '/service/%service_id/version/%version/logging/ftp/%name', 678 | }, 679 | { 680 | method: 'POST', 681 | type: 'ftp', 682 | fn: 'createLoggingFtp', 683 | url: '/service/%service_id/version/%version/logging/ftp', 684 | }, 685 | { 686 | method: 'GET', 687 | type: 'logentries', 688 | fn: 'getLoggingLogentries', 689 | url: '/service/%service_id/version/%version/logging/logentries', 690 | }, 691 | { 692 | method: 'DELETE', 693 | type: 'logentries', 694 | fn: 'deleteLoggingLogentriesByName', 695 | url: '/service/%service_id/version/%version/logging/logentries/%name', 696 | }, 697 | { 698 | method: 'POST', 699 | type: 'logentries', 700 | fn: 'createLoggingLogentries', 701 | url: '/service/%service_id/version/%version/logging/logentries', 702 | }, 703 | { 704 | method: 'GET', 705 | type: 'syslog', 706 | fn: 'getLoggingSyslog', 707 | url: '/service/%service_id/version/%version/logging/syslog', 708 | }, 709 | { 710 | method: 'DELETE', 711 | type: 'syslog', 712 | fn: 'deleteLoggingSyslogByName', 713 | url: '/service/%service_id/version/%version/logging/syslog/%name', 714 | }, 715 | { 716 | method: 'POST', 717 | type: 'syslog', 718 | fn: 'createLoggingSyslog', 719 | url: '/service/%service_id/version/%version/logging/syslog', 720 | }, 721 | { 722 | method: 'GET', 723 | type: 'splunk', 724 | fn: 'getLoggingSplunk', 725 | url: '/service/%service_id/version/%version/logging/splunk', 726 | }, 727 | { 728 | method: 'DELETE', 729 | type: 'splunk', 730 | fn: 'deleteLoggingSplunkByName', 731 | url: '/service/%service_id/version/%version/logging/splunk/%name', 732 | }, 733 | { 734 | method: 'POST', 735 | type: 'splunk', 736 | fn: 'createLoggingSplunk', 737 | url: '/service/%service_id/version/%version/logging/splunk', 738 | }, 739 | { 740 | method: 'GET', 741 | type: 's3', 742 | fn: 'getLoggingS3', 743 | url: '/service/%service_id/version/%version/logging/s3', 744 | }, 745 | { 746 | method: 'DELETE', 747 | type: 's3', 748 | fn: 'deleteLoggingS3ByName', 749 | url: '/service/%service_id/version/%version/logging/s3/%name', 750 | }, 751 | { 752 | method: 'POST', 753 | type: 's3', 754 | fn: 'createLoggingS3', 755 | url: '/service/%service_id/version/%version/logging/s3', 756 | } 757 | ]; 758 | -------------------------------------------------------------------------------- /lib/fastly/lib/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | /** 4 | * Fastly API client. 5 | * 6 | * @package fastly 7 | * @author Andrew Sliwinski 8 | */ 9 | 10 | /** 11 | * Dependencies 12 | */ 13 | const request = require('request'); 14 | const debug = require('debug')('fastly'); 15 | 16 | /** 17 | * Constructor 18 | */ 19 | function Fastly (apikeys, service, options) { 20 | 21 | const opts = options || {}; 22 | apikeys = apikeys || []; 23 | this.apikey = apikeys.shift(); 24 | this.apikeys = apikeys || []; 25 | 26 | this.service = service || ''; 27 | this.verbose = (opts.verbose) ? true : false; 28 | 29 | const self = this; 30 | 31 | // For each API method create a function 32 | require('./api').forEach(function (a) { 33 | if (a.fn && (a.fn !== '')) { 34 | 35 | self[a.fn] = function () { 36 | 37 | const url = a.url; 38 | const method = a.method; 39 | 40 | const interpolateUrl = function (url, tokens) { 41 | return url 42 | .split('/') 43 | .map( function (a) { 44 | return /^\%/.test(a) ? tokens.pop() : a; // pop. not idempotent. 45 | }) 46 | .join('/'); 47 | }; 48 | 49 | const args = [].slice.call(arguments).reverse().concat([self.service]); 50 | 51 | // In the cases where we want a POST body we pass the 52 | // parameters as the last argument, so after the arguments have 53 | // been converted to an array and reversed we can detect an 54 | // object has been passed in an unshift it. 55 | const params = (args[0] && typeof args[0] === 'object') ? args.shift() : null; 56 | 57 | const endPoint = interpolateUrl(url, args); 58 | 59 | return self.request(method, endPoint, params); 60 | }; 61 | } 62 | }); 63 | } 64 | 65 | /** 66 | * Adapter helper method. 67 | * 68 | * @param {string} Method 69 | * @param {string} URL 70 | * @param {params} Optional params to update. 71 | * 72 | * @return {Object} 73 | */ 74 | Fastly.prototype.request = function (method, url, params) { 75 | 76 | return new Promise((resolve, reject) => { 77 | // Allow for optional update params. 78 | if (typeof params === 'function') { 79 | params = null; 80 | } 81 | 82 | debug('Request: ' + method + ', ' + url); 83 | 84 | // Construct headers 85 | const headers = { 'fastly-key': this.apikey }; 86 | 87 | // HTTP request 88 | request({ 89 | method: method, 90 | url: 'https://api.fastly.com' + url, 91 | headers: headers, 92 | form: params 93 | }, (err, response, body) => { 94 | 95 | if (this.verbose) { 96 | debug('Response: ' + response.statusCode + ' ' + body); 97 | } 98 | 99 | if (err) return reject(body); 100 | 101 | if (response.statusCode >= 400) return reject(body); 102 | if (response.statusCode > 302) return resolve(body); 103 | if (response.headers['content-type'] === 'application/json') { 104 | try { 105 | body = JSON.parse(body); 106 | } catch (error) { 107 | return reject(error); 108 | } 109 | } 110 | console.log(method.toUpperCase() + ' to ' + url + ' succeeded'); 111 | resolve(body); 112 | }); 113 | 114 | }) 115 | .catch(err => { 116 | if (err.msg && /You have exceeded your hourly rate limit/.test(err.msg) && this.apikeys.length) { 117 | console.log(method.toUpperCase() + ' to ' + url + ' hit rate limit'); 118 | this.apikey = this.apikeys.shift(); 119 | if (!this.apikey) { 120 | console.log('no backup api keys available'); 121 | throw err; 122 | } 123 | return this.request(method, url, params); 124 | } 125 | console.log(method.toUpperCase() + ' to ' + url + ' failed'); 126 | throw err; 127 | }); 128 | 129 | }; 130 | 131 | // ------------------------------------------------------- 132 | 133 | Fastly.prototype.purge = function (host, url) { 134 | return this.request('POST', '/purge/' + host + url); 135 | }; 136 | 137 | Fastly.prototype.purgeAll = function (service) { 138 | const url = '/service/' + encodeURIComponent(service) + '/purge_all'; 139 | return this.request('POST', url); 140 | }; 141 | 142 | Fastly.prototype.purgeKey = function (service, key) { 143 | const url = '/service/' + encodeURIComponent(service) + '/purge/' + key; 144 | return this.request('POST', url); 145 | }; 146 | 147 | Fastly.prototype.stats = function (service) { 148 | const url = '/service/' + encodeURIComponent(service) + '/stats/summary'; 149 | return this.request('GET', url); 150 | }; 151 | 152 | Fastly.prototype.stats = function (service) { 153 | const url = '/service/' + encodeURIComponent(service) + '/stats/summary'; 154 | return this.request('GET', url); 155 | }; 156 | 157 | /** 158 | * Export 159 | */ 160 | module.exports = function (apikey, service, opts) { 161 | return new Fastly(apikey, service, opts); 162 | }; 163 | -------------------------------------------------------------------------------- /lib/loadVcl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | function replaceVars (vcls, vars) { 6 | return vcls.map(function (vcl) { 7 | vars.forEach(function (v) { 8 | if (!process.env[v]) { 9 | throw new Error(`Environment variable ${v} is required to deploy this vcl`); 10 | } 11 | const regex = new RegExp('\\\$\\\{'+ v.trim()+'\\\}', 'gm'); 12 | vcl.content = vcl.content.replace(regex, process.env[v]); 13 | }); 14 | 15 | return vcl; 16 | }); 17 | } 18 | 19 | 20 | module.exports = function loadVcl (folder, vars){ 21 | let vcls = fs.readdirSync(folder).map(function (name) { 22 | return { 23 | name: name, 24 | content: fs.readFileSync(path.join(folder,name), { encoding: 'utf-8' }) 25 | }; 26 | }); 27 | 28 | // if vars option exists, replace ${VAR} with process.env.VAR 29 | if(vars.length) { 30 | vcls = replaceVars(vcls, vars); 31 | } 32 | 33 | return vcls; 34 | }; 35 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | 'use strict'; 4 | const colors = require('colors'); 5 | const util = require('util'); 6 | const readFileSync = require('fs').readFileSync; 7 | 8 | const TEA = readFileSync(__dirname + '/../art/tea.ascii'); 9 | const SUPERMAN = readFileSync(__dirname + '/../art/superman.ascii'); 10 | 11 | const colorMap = new Map([ 12 | ['verbose', null], 13 | ['info', null], 14 | ['warn', 'yellow'], 15 | ['error', 'red'], 16 | ['success', 'green'] 17 | ]); 18 | 19 | const asciiArt = new Map([ 20 | ['tea', TEA], 21 | ['superman', SUPERMAN] 22 | ]); 23 | 24 | function writelog (str, color){ 25 | if(color){ 26 | str = colors[color](str); 27 | } 28 | 29 | console.log(str); 30 | } 31 | 32 | function isArray (val){ 33 | return val instanceof Array || typeof val === 'object' && val !== null && val.push && val.pop; 34 | } 35 | 36 | function toString (v){ 37 | return typeof v === 'object' ? util.inspect(v, {depth:null}) : String(v); 38 | } 39 | 40 | function combineArgs (args){ 41 | if(!isArray(args)){ 42 | return toString(args); 43 | } 44 | 45 | return args.reduce((previous, current) => { 46 | let val; 47 | if(typeof current === 'object'){} 48 | val = toString(current); 49 | return previous + '\n' + val; 50 | }, ''); 51 | } 52 | 53 | module.exports = function (opts){ 54 | let options = Object.assign( 55 | { 56 | verbose : false, 57 | disabled : false 58 | }, 59 | opts 60 | ); 61 | 62 | let logger = {}; 63 | 64 | for(let item of colorMap){ 65 | if(options.disabled || item[0] === 'verbose' && !options.verbose){ 66 | logger[item[0]] = function (){}; 67 | continue; 68 | } 69 | 70 | logger[item[0]] = (args) => writelog(combineArgs(args), item[1]); 71 | } 72 | 73 | logger.art = options.disabled ? function (){} : (art, context) => { 74 | let color = colorMap.get(context); 75 | let ascii = asciiArt.get(art); 76 | let output = color ? colors[color](ascii) : ascii; 77 | console.log(output); 78 | }; 79 | 80 | return logger; 81 | }; 82 | -------------------------------------------------------------------------------- /lib/symbols.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | VCL_VALIDATION_ERROR: Symbol() 3 | }; 4 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const tasks = require('./tasks/'); 3 | 4 | module.exports = tasks; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@financial-times/fastly-tools", 3 | "version": "0.0.0", 4 | "description": "Command Line Utility for interacting with fastly", 5 | "main": "main.js", 6 | "bin": { 7 | "fastly-tools": "./bin/fastly-tools.js", 8 | "fastly": "./bin/fastly-tools.js" 9 | }, 10 | "scripts": { 11 | "test": "make test", 12 | "prepare": "npx snyk protect || npx snyk protect -d || true" 13 | }, 14 | "engines": { 15 | "node": "16.x || 18.x", 16 | "npm": "7.x || 8.x || 9.x" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/Financial-Times/fastly-tools.git" 21 | }, 22 | "author": "", 23 | "license": "ISC", 24 | "bugs": { 25 | "url": "https://github.com/Financial-Times/fastly-tools/issues" 26 | }, 27 | "homepage": "https://github.com/Financial-Times/fastly-tools#readme", 28 | "devDependencies": { 29 | "@financial-times/n-gage": "^8.3.2", 30 | "chai": "^3.5.0", 31 | "check-engine": "^1.10.1", 32 | "eslint": "^4.19.1", 33 | "lintspaces-cli": "^0.4.0", 34 | "mocha": "^2.4.5", 35 | "npm-prepublish": "^1.2.1", 36 | "proxyquire": "^1.7.4", 37 | "sinon": "^1.17.3", 38 | "snyk": "^1.167.2", 39 | "tap": "^5.7.1" 40 | }, 41 | "dependencies": { 42 | "array.prototype.includes": "^1.0.0", 43 | "co": "^4.6.0", 44 | "colors": "^1.1.2", 45 | "commander": "^2.9.0", 46 | "debug": "^2.2.0", 47 | "dotenv": "^10.0.0", 48 | "node-fetch": "^1.5.0", 49 | "request": "^2.72.0" 50 | }, 51 | "husky": { 52 | "hooks": { 53 | "commit-msg": "node_modules/.bin/secret-squirrel-commitmsg", 54 | "pre-commit": "node_modules/.bin/secret-squirrel", 55 | "pre-push": "make verify -j3" 56 | } 57 | }, 58 | "volta": { 59 | "node": "18.16.0" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>financial-times/renovate-config-next-beta" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /secret-squirrel.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | files: { 3 | allow: [ 4 | 'art/superman.ascii', 5 | 'art/tea.ascii' 6 | ], 7 | allowOverrides: [] 8 | }, 9 | strings: { 10 | deny: [], 11 | denyOverrides: [ 12 | 'deleteLoggingS3ByName', // lib/fastly/lib/api.js:748, tasks/deploy.js:147, test/mocks/fastly.mock.js:41 13 | 'andrew@diy\\.org' // lib/fastly/lib/index.js:5 14 | ] 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /tasks/deploy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const co = require('co'); 3 | require('array.prototype.includes'); 4 | const path = require('path'); 5 | 6 | const loadVcl = require('../lib/loadVcl'); 7 | const symbols = require('../lib/symbols'); 8 | 9 | function task (folder, opts) { 10 | let options = Object.assign({ 11 | main: 'main.vcl', 12 | env: false, 13 | service: null, 14 | vars: [], 15 | verbose: false, 16 | disableLogs: false, 17 | backends: null, 18 | apiKeys: [], 19 | skipConditions: [] 20 | }, opts); 21 | 22 | if (options.env) { 23 | require('dotenv').config(); 24 | } 25 | 26 | const log = require('../lib/logger')({verbose:options.verbose, disabled:options.disableLogs}); 27 | 28 | return co(function*() { 29 | if(!folder) { 30 | throw new Error('Please provide a folder where the .vcl is located'); 31 | } 32 | 33 | if (!options.service) { 34 | throw new Error('the service parameter is required set to the service id of a environment variable name'); 35 | } 36 | 37 | if (process.env.FASTLY_APIKEY) { 38 | options.apiKeys.unshift(process.env.FASTLY_APIKEY); 39 | } 40 | 41 | if (!options.apiKeys.length) { 42 | throw new Error('fastly api key not found. Either set a FASTLY_APIKEY environment variable, or pass in using the --api-keys option'); 43 | } 44 | 45 | const fastlyApiKeys = options.apiKeys; 46 | const serviceId = process.env[opts.service] || opts.service; 47 | 48 | if (!serviceId) { 49 | throw new Error('No service '); 50 | } 51 | 52 | const fastly = require('./../lib/fastly/lib')(fastlyApiKeys, encodeURIComponent(serviceId), {verbose: false}); 53 | 54 | // if service ID is needed use the given serviceId 55 | if (options.vars.includes('SERVICEID')) { 56 | process.env.SERVICEID = serviceId; 57 | } 58 | 59 | const vcls = loadVcl(folder, options.vars); 60 | 61 | // get the current service and active version 62 | const service = yield fastly.getServices().then(services => services.find(s => s.id === serviceId)); 63 | const activeVersion = service.version; 64 | 65 | // clone new version from current active version 66 | log.verbose(`Cloning active version ${activeVersion} of ${service.name}`); 67 | let cloneResponse = yield fastly.cloneVersion(activeVersion); 68 | log.verbose(`Successfully cloned version ${cloneResponse.number}`); 69 | let newVersion = cloneResponse.number; 70 | log.info('Cloned new version'); 71 | 72 | //upload backends via the api 73 | if(options.backends){ 74 | log.verbose(`Backends option specified. Loading backends from ${options.backends}`); 75 | const backendData = require(path.join(process.cwd(), options.backends)); 76 | 77 | log.verbose('Now, delete all existing healthchecks'); 78 | const currentHealthchecks = yield fastly.getHealthcheck(newVersion); 79 | yield Promise.all(currentHealthchecks.map(h => fastly.deleteHealthcheck(newVersion, h.name))); 80 | log.info('Deleted old healthchecks'); 81 | if (backendData.healthchecks) { 82 | log.verbose(`About to upload ${backendData.healthchecks.length} healthchecks`); 83 | yield Promise.all(backendData.healthchecks.map(h => { 84 | log.verbose(`upload healthcheck ${h.name}`); 85 | return fastly.createHealthcheck(newVersion, h).then(() => log.verbose(`✓ Healthcheck ${h.name} uploaded`)); 86 | })); 87 | log.info('Uploaded new healthchecks'); 88 | } 89 | 90 | log.verbose('Now, delete all existing conditions'); 91 | const currentConditions = yield fastly.getConditions(newVersion); 92 | yield Promise.all( 93 | currentConditions 94 | .filter(c => options.skipConditions.indexOf(c.name) === -1) 95 | .map(h => fastly.deleteCondition(newVersion, h.name)) 96 | ); 97 | log.info('Deleted old conditions'); 98 | if (backendData.conditions) { 99 | yield Promise.all(backendData.conditions.map(c => { 100 | log.verbose(`upload condition ${c.name}`); 101 | return fastly.createCondition(newVersion, c).then(() => log.verbose(`✓ Condition ${c.name} uploaded`)); 102 | })); 103 | log.info('Uploaded new conditions'); 104 | } 105 | 106 | if (backendData.headers) { 107 | log.verbose('Now, delete all existing headers'); 108 | const currentHeaders = yield fastly.getHeaders(newVersion); 109 | yield Promise.all(currentHeaders.map(h => fastly.deleteHeader(newVersion, h.name))); 110 | log.info('Deleted old headers'); 111 | yield Promise.all(backendData.headers.map(h => { 112 | log.verbose(`upload header ${h.name}`); 113 | return fastly.createHeader(newVersion, h) 114 | .then(() => log.verbose(`✓ Header ${h.name} uploaded`)); 115 | })); 116 | log.info('Uploaded new headers'); 117 | } 118 | 119 | log.verbose('Now, delete all existing backends'); 120 | const currentBackends = yield fastly.getBackend(newVersion); 121 | yield Promise.all(currentBackends.map(b => fastly.deleteBackendByName(newVersion, b.name))); 122 | log.info('Deleted old backends'); 123 | yield Promise.all(backendData.backends.map(b => { 124 | log.verbose(`upload backend ${b.name}`); 125 | return fastly.createBackend(newVersion, b).then(() => log.verbose(`✓ Backend ${b.name} uploaded`)); 126 | })); 127 | log.info('Uploaded new backends'); 128 | 129 | const loggers = { 130 | 'logentries': { 'get': fastly.getLoggingLogentries, 131 | 'delete': fastly.deleteLoggingLogentriesByName, 132 | 'create': fastly.createLoggingLogentries, 133 | }, 134 | 'ftp': { 'get': fastly.getLoggingFtp, 135 | 'delete': fastly.deleteLoggingFtpByName, 136 | 'create': fastly.createLoggingFtp, 137 | }, 138 | 'syslog': { 'get': fastly.getLoggingSyslog, 139 | 'delete': fastly.deleteLoggingSyslogByName, 140 | 'create': fastly.createLoggingSyslog, 141 | }, 142 | 'splunk': { 'get': fastly.getLoggingSplunk, 143 | 'delete': fastly.deleteLoggingSplunkByName, 144 | 'create': fastly.createLoggingSplunk, 145 | }, 146 | 's3': { 'get': fastly.getLoggingS3, 147 | 'delete': fastly.deleteLoggingS3ByName, 148 | 'create': fastly.createLoggingS3, 149 | }, 150 | }; 151 | 152 | for(const logger in loggers) { 153 | if(loggers.hasOwnProperty(logger)) { 154 | log.verbose(`Now, delete all existing logging ${logger}`); 155 | const currentLoggers = yield loggers[logger].get(activeVersion); 156 | yield Promise.all(currentLoggers.map(l => loggers[logger].delete(newVersion, l.name))); 157 | log.verbose(`Deleted old logging ${logger}`); 158 | if (backendData.logging && backendData.logging[logger]) { 159 | yield Promise.all(backendData.logging[logger].map(l => { 160 | log.verbose(`upload logging ${logger} ${l.name}`); 161 | return loggers[logger].create(newVersion, l) 162 | .then(() => 163 | log.verbose(`✓ Logger ${logger}/${l.name} uploaded`) 164 | ); 165 | })); 166 | log.info(`Uploaded new logging ${logger}`); 167 | } 168 | } 169 | } 170 | } 171 | 172 | // delete old vcl 173 | let oldVcl = yield fastly.getVcl(newVersion); 174 | yield Promise.all(oldVcl.map(vcl => { 175 | log.verbose(`Deleting "${vcl.name}" for version ${newVersion}`); 176 | return fastly.deleteVcl(newVersion, vcl.name); 177 | })); 178 | log.info('Deleted old vcl'); 179 | 180 | //upload new vcl 181 | log.info('Uploading new VCL'); 182 | yield Promise.all(vcls.map(vcl => { 183 | log.verbose(`Uploading new VCL ${vcl.name} with version ${newVersion}`); 184 | return fastly.updateVcl(newVersion, { 185 | name: vcl.name, 186 | content: vcl.content 187 | }); 188 | })); 189 | 190 | // set the main vcl file 191 | log.verbose(`Try to set "${options.main}" as the main entry point`); 192 | yield fastly.setVclAsMain(newVersion, options.main); 193 | log.info(`"${options.main}" set as the main entry point`); 194 | 195 | // validate 196 | log.verbose(`Validate version ${newVersion}`); 197 | let validationResponse = yield fastly.validateVersion(newVersion); 198 | if (validationResponse.status === 'ok') { 199 | log.info(`Version ${newVersion} looks ok`); 200 | yield fastly.activateVersion(newVersion); 201 | } else { 202 | let error = new Error('VCL Validation Error'); 203 | error.type = symbols.VCL_VALIDATION_ERROR; 204 | error.validation = validationResponse.msg; 205 | throw error; 206 | } 207 | 208 | log.success('Your VCL has been deployed.'); 209 | log.art('tea', 'success'); 210 | 211 | }); 212 | } 213 | 214 | module.exports = task; 215 | -------------------------------------------------------------------------------- /test/deploy.task.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | 'use strict'; 4 | const sinon = require('sinon'); 5 | const expect = require('chai').expect; 6 | process.env.FASTLY_APIKEY ='12345'; 7 | const proxyquire = require('proxyquire').noCallThru().noPreserveCache(); 8 | const fastlyMock = require('./mocks/fastly.mock'); 9 | 10 | const path = require('path'); 11 | 12 | describe('Deploy Task', function (){ 13 | 14 | let deployVcl; 15 | 16 | before(function (){ 17 | deployVcl = proxyquire('../tasks/deploy', {'./../lib/fastly/lib' : fastlyMock}); 18 | }); 19 | 20 | afterEach(() => { 21 | for(let method of Object.keys(fastlyMock())){ 22 | if (typeof fastlyMock()[method] === 'function') { 23 | fastlyMock()[method].reset(); 24 | } 25 | } 26 | }); 27 | 28 | it('Should be able to deploy some vcl', function (){ 29 | return deployVcl( 30 | path.resolve(__dirname, './fixtures/vcl')+'/', 31 | { 32 | service:fastlyMock.fakeServiceId, 33 | apiKeys: ['dummy-key'], 34 | skipConditions: [], 35 | disableLogs:true 36 | }) 37 | .then(function (){ 38 | sinon.assert.called(fastlyMock().updateVcl); 39 | }); 40 | }); 41 | 42 | it('Should replace placeholders with environment vars', function (){ 43 | const value = 'value'; 44 | process.env.AUTH_KEY = value; 45 | return deployVcl( 46 | path.resolve(__dirname, './fixtures/vcl')+'/', 47 | { 48 | service:fastlyMock.fakeServiceId, 49 | apiKeys: ['dummy-key'], 50 | skipConditions: [], 51 | vars:['AUTH_KEY'], 52 | disableLogs:true 53 | }) 54 | .then(function (){ 55 | const vcl = fastlyMock().updateVcl.lastCall.args[1].content; 56 | expect(vcl).to.contain(value); 57 | expect(vcl).not.to.contain('${AUTH_KEY}'); 58 | }); 59 | }); 60 | 61 | it('Should upload given backends from .json file via the api', () => { 62 | let fixture = require('./fixtures/backends.json'); 63 | return deployVcl( 64 | path.resolve(__dirname, './fixtures/vcl')+'/', 65 | { 66 | service:fastlyMock.fakeServiceId, 67 | apiKeys: ['dummy-key'], 68 | skipConditions: [], 69 | backends:'test/fixtures/backends.json', 70 | disableLogs:true 71 | }) 72 | .then(function (){ 73 | let callCount = fastlyMock().createBackend.callCount; 74 | expect(callCount).to.equal(fixture.backends.length); 75 | for(let i=0; i { 89 | let fixture = require('./fixtures/backends.json'); 90 | return deployVcl( 91 | path.resolve(__dirname, './fixtures/vcl')+'/', 92 | { 93 | service:fastlyMock.fakeServiceId, 94 | apiKeys: ['dummy-key'], 95 | skipConditions: [], 96 | backends:'test/fixtures/backends.json', 97 | disableLogs:true 98 | }) 99 | .then(function (){ 100 | let callCount = fastlyMock().createHealthcheck.callCount; 101 | expect(callCount).to.equal(fixture.healthchecks.length); 102 | for(let i=0; i { 110 | let fixture = require('./fixtures/backends.json'); 111 | return deployVcl( 112 | path.resolve(__dirname, './fixtures/vcl')+'/', 113 | { 114 | service:fastlyMock.fakeServiceId, 115 | apiKeys: ['dummy-key'], 116 | skipConditions: [], 117 | backends:'test/fixtures/backends.json', 118 | disableLogs:true 119 | }) 120 | .then(function (){ 121 | let callCount = fastlyMock().createCondition.callCount; 122 | expect(callCount).to.equal(fixture.conditions.length); 123 | for(let i=0; i a.fn).map(a => a.fn); 4 | 5 | test('unit', function (t) { 6 | t.type(fastly, 'function', 'module is a function'); 7 | 8 | const ready = fastly(['apikey1', 'apikey2']); 9 | 10 | t.type(ready, 'object', 'module exposes an object'); 11 | t.type(ready.request, 'function', 'request method exists'); 12 | 13 | methods.forEach(m => { 14 | t.type(ready[m], 'function', `${m} method exists`); 15 | }); 16 | 17 | t.end(); 18 | }); 19 | -------------------------------------------------------------------------------- /test/fixtures/backends.json: -------------------------------------------------------------------------------- 1 | { 2 | "backends": [ 3 | { 4 | "name": "falcon_eu", 5 | "connect_timeout": 3000, 6 | "port": "80", 7 | "hostname": "eu.prod.ft.com", 8 | "first_byte_timeout": 1000, 9 | "max_conn": 200, 10 | "between_bytes_timeout": 1000, 11 | "healthcheck": "falcon_eu_healthcheck", 12 | "request_condition": "near_eu" 13 | }, 14 | { 15 | "name": "falcon_us", 16 | "connect_timeout": 3000, 17 | "port": "80", 18 | "hostname": "us.prod.ft.com", 19 | "first_byte_timeout": 1000, 20 | "max_conn": 200, 21 | "between_bytes_timeout": 1000, 22 | "healthcheck": "falcon_us_healthcheck", 23 | "request_condition": "near_us" 24 | }, 25 | { 26 | "name": "ig", 27 | "connect_timeout": 3000, 28 | "port": "80", 29 | "hostname": "ig.ft.com", 30 | "first_byte_timeout": 1000, 31 | "max_conn": 200, 32 | "between_bytes_timeout": 1000, 33 | "healthcheck": "ig_healthcheck" 34 | }, 35 | { 36 | "name": "ads_s3_bucket", 37 | "connect_timeout": 3000, 38 | "port": "80", 39 | "hostname": "com.ft.ads-static-content.s3-website-eu-west-1.amazonaws.com", 40 | "first_byte_timeout": 1000, 41 | "max_conn": 200, 42 | "between_bytes_timeout": 1000, 43 | "healthcheck": "ads_s3_bucket_healthcheck" 44 | }, 45 | { 46 | "name": "access", 47 | "connect_timeout": 3000, 48 | "port": "80", 49 | "hostname": "access.ft.com", 50 | "first_byte_timeout": 1000, 51 | "max_conn": 200, 52 | "between_bytes_timeout": 1000, 53 | "healthcheck": "access_healthcheck" 54 | }, 55 | { 56 | "name": "access_test", 57 | "connect_timeout": 3000, 58 | "port": "80", 59 | "hostname": "access.test.ft.com", 60 | "first_byte_timeout": 1000, 61 | "max_conn": 200, 62 | "between_bytes_timeout": 1000, 63 | "healthcheck": "access_test_healthcheck" 64 | }, 65 | { 66 | "name": "test", 67 | "connect_timeout": 3000, 68 | "port": "80", 69 | "hostname": "ft-next-narcissus.herokuapp.com", 70 | "first_byte_timeout": 1000, 71 | "max_conn": 200, 72 | "between_bytes_timeout": 1000, 73 | "healthcheck": "test_healthcheck" 74 | } 75 | ], 76 | "healthchecks": [ 77 | { 78 | "name": "falcon_eu_healthcheck", 79 | "method": "HEAD", 80 | "path": "/itm/site_status.html", 81 | "http_version": "1.1", 82 | "host": "eu.prod.ft.com", 83 | "threshold": 1, 84 | "window": 2, 85 | "timeout": 5000, 86 | "initial": 1, 87 | "expected_response": 200, 88 | "interval": 1000 89 | }, 90 | { 91 | "name": "falcon_us_healthcheck", 92 | "method": "HEAD", 93 | "path": "/itm/site_status.html", 94 | "http_version": "1.1", 95 | "host": "eu.prod.ft.com", 96 | "threshold": 1, 97 | "window": 2, 98 | "timeout": 5000, 99 | "initial": 1, 100 | "expected_response": 200, 101 | "interval": 1000 102 | }, 103 | { 104 | "name": "ig_healthcheck", 105 | "method": "HEAD", 106 | "path": "/sureroute.html", 107 | "http_version": "1.1", 108 | "host": "ig.ft.com", 109 | "threshold": 1, 110 | "window": 2, 111 | "timeout": 5000, 112 | "initial": 1, 113 | "expected_response": 200, 114 | "interval": 1000 115 | }, 116 | { 117 | "name": "ads_s3_bucket_healthcheck", 118 | "method": "HEAD", 119 | "path": "/indexdg.html", 120 | "http_version": "1.1", 121 | "host": "com.ft.ads-static-content.s", 122 | "threshold": 1, 123 | "window": 2, 124 | "timeout": 5000, 125 | "initial": 1, 126 | "expected_response": 200, 127 | "interval": 1000 128 | }, 129 | { 130 | "name": "access_healthcheck", 131 | "method": "HEAD", 132 | "path": "/__gtg", 133 | "http_version": "1.1", 134 | "host": "access.ft.com", 135 | "threshold": 1, 136 | "window": 2, 137 | "timeout": 5000, 138 | "initial": 1, 139 | "expected_response": 200, 140 | "interval": 1000 141 | }, 142 | { 143 | "name": "fastft_healthcheck", 144 | "method": "HEAD", 145 | "path": "/__gtg/", 146 | "http_version": "1.1", 147 | "host": "fastft.glb.ft.com", 148 | "threshold": 1, 149 | "window": 2, 150 | "timeout": 5000, 151 | "initial": 1, 152 | "expected_response": 200, 153 | "interval": 1000 154 | }, 155 | { 156 | "name": "access_test_healthcheck", 157 | "method": "HEAD", 158 | "path": "/__gtg", 159 | "http_version": "1.1", 160 | "host": "access.test.ft.com", 161 | "threshold": 1, 162 | "window": 2, 163 | "timeout": 5000, 164 | "initial": 1, 165 | "expected_response": 200, 166 | "interval": 1000 167 | }, 168 | { 169 | "name": "test_healthcheck", 170 | "method": "HEAD", 171 | "path": "/__gtg", 172 | "http_version": "1.1", 173 | "host": "ft-next-narcissus.herokuapp.com", 174 | "threshold": 1, 175 | "window": 2, 176 | "timeout": 5000, 177 | "initial": 1, 178 | "expected_response": 200, 179 | "interval": 1000 180 | } 181 | ], 182 | "conditions": [ 183 | { 184 | "name": "near_eu", 185 | "statement": "!(req.http.X-Geoip-Continent ~ \"(NA|SA|OC)\")", 186 | "type": "REQUEST", 187 | "priority": 10 188 | }, 189 | { 190 | "name": "near_us", 191 | "statement": "(req.http.X-Geoip-Continent ~ \"(NA|SA|OC)\")", 192 | "type": "REQUEST", 193 | "priority": 10 194 | } 195 | ] 196 | } 197 | -------------------------------------------------------------------------------- /test/fixtures/vcl/main.vcl: -------------------------------------------------------------------------------- 1 | set req.http.Authorisation = "${AUTH_KEY}"; -------------------------------------------------------------------------------- /test/helpers/createTestService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const fetch = require('node-fetch'); 3 | 4 | module.exports = function createFastlyService (){ 5 | return fetch( 6 | 'https://api.fastly.com/service', 7 | { 8 | method: 'POST', 9 | headers : { 10 | 'Fastly-Key': process.env.FASTLY_API_KEY, 11 | 'Content-Type': 'application/x-www-form-urlencoded', 12 | 'Accept': 'application/json' 13 | }, 14 | body : 'name=test-service' 15 | } 16 | ).then(response => { 17 | if(!response.ok){ 18 | throw new Error('Failed to create new Service'); 19 | } 20 | 21 | return response.json(); 22 | }).then(json => { 23 | global.FASTLY_TEST_SERVICE = json; // eslint-disable-line no-undef 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /test/helpers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | 'createTestService' : require('./createTestService') 5 | }; 6 | -------------------------------------------------------------------------------- /test/mocks/fastly.mock.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const sinon = require('sinon'); 3 | 4 | const fakeServiceId = '1234567'; 5 | 6 | function mockPromiseMethod (obj, name, value){ 7 | obj[name] = sinon.stub().returns(Promise.resolve(value)); 8 | } 9 | 10 | const methods = { 11 | 'getServices': [{id:fakeServiceId}], 12 | 'cloneVersion': {number:1}, 13 | 'getVcl': [{name:'blah.vcl'}], 14 | 'deleteVcl' : null, 15 | 'updateVcl' : null, 16 | 'setVclAsMain': null, 17 | 'validateVersion': {status:'ok'}, 18 | 'activateVersion': null, 19 | 'createBackend': null, 20 | 'getBackend': [{name:'fake-backend'}], 21 | 'deleteBackendByName': null, 22 | 'getHealthcheck': [{name:'fake-healthcheck'}], 23 | 'deleteHealthcheck' : null, 24 | 'createHealthcheck': null, 25 | 'getConditions': [{name:'fake-condition'}], 26 | 'createCondition': null, 27 | 'deleteCondition': null, 28 | 'getLoggingLogentries': [{name:'fake-logentry'}], 29 | 'deleteLoggingLogentriesByName': null, 30 | 'createLoggingLogentries': null, 31 | 'getLoggingFtp': [{name:'fake-ftp'}], 32 | 'deleteLoggingFtpByName': null, 33 | 'createLoggingFtp': null, 34 | 'getLoggingSyslog': [{name:'fake-syslog'}], 35 | 'deleteLoggingSyslogByName': null, 36 | 'createLoggingSyslog': null, 37 | 'getLoggingSplunk': [{name:'fake-splunk'}], 38 | 'deleteLoggingSplunkByName': null, 39 | 'createLoggingSplunk': null, 40 | 'getLoggingS3': [{name:'fake-s3'}], 41 | 'deleteLoggingS3ByName': null, 42 | 'createLoggingS3': null, 43 | }; 44 | 45 | let mock = {}; 46 | let called = false; 47 | 48 | module.exports = function () { 49 | if (called) { 50 | return mock; 51 | } 52 | 53 | mock = {apikeys: ['dummy-second-key'], apikey: 'dummy-key'}; 54 | const func = mockPromiseMethod.bind(null, mock); 55 | Object.keys(methods).forEach(function (key) { 56 | func(key, methods[key]); 57 | }); 58 | 59 | called = true; 60 | return mock; 61 | }; 62 | 63 | module.exports.fakeServiceId = fakeServiceId; 64 | --------------------------------------------------------------------------------