├── .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 [](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 |
--------------------------------------------------------------------------------