├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── deploy └── kubelessDeploy.js ├── deployFunction └── kubelessDeployFunction.js ├── examples ├── cert-manager-https │ ├── README.md │ ├── handler.py │ ├── package.json │ └── serverless.yml ├── custom-ingress │ ├── README.md │ ├── handler.py │ ├── package.json │ └── serverless.yml ├── event-trigger-python │ ├── README.md │ ├── handler.py │ ├── package.json │ └── serverless.yml ├── get-python │ ├── README.md │ ├── handler.py │ ├── package.json │ └── serverless.yml ├── get-ruby │ ├── README.md │ ├── package.json │ ├── serverless.yml │ └── version.rb ├── http-custom-path │ ├── README.md │ ├── handler.py │ ├── package.json │ └── serverless.yml ├── multi-python │ ├── README.md │ ├── handler.py │ ├── package.json │ └── serverless.yml ├── node-chaining-functions │ ├── README.md │ ├── handler.js │ ├── package.json │ └── serverless.yml ├── post-go │ ├── README.md │ ├── go.mod │ ├── handler.go │ ├── package.json │ └── serverless.yml ├── post-nodejs │ ├── README.md │ ├── handler.js │ ├── package.json │ └── serverless.yml ├── post-php-s3 │ ├── README.md │ ├── handler.php │ ├── package.json │ └── serverless.yml ├── post-php │ ├── README.md │ ├── handler.php │ ├── package.json │ └── serverless.yml ├── post-python │ ├── README.md │ ├── handler.py │ ├── package.json │ └── serverless.yml ├── post-ruby │ ├── README.md │ ├── package.json │ ├── ping.rb │ └── serverless.yml ├── scheduled-node │ ├── README.md │ ├── handler.js │ ├── package.json │ └── serverless.yml └── todo-app │ ├── LICENSE │ ├── README.md │ ├── backend │ ├── README.md │ ├── img │ │ ├── gce_firewall_rule_edit.png │ │ └── gce_firewall_rules.png │ ├── package.json │ ├── serverless.yml │ ├── todos-create.js │ ├── todos-delete.js │ ├── todos-read-all.js │ ├── todos-read-one.js │ └── todos-update.js │ ├── frontend │ └── react-redux │ │ ├── README.md │ │ ├── app │ │ ├── index.html │ │ └── js │ │ │ ├── actions │ │ │ ├── constants.js │ │ │ ├── error.js │ │ │ ├── index.js │ │ │ └── todos.js │ │ │ ├── app.jsx │ │ │ ├── components │ │ │ ├── app.jsx │ │ │ ├── shared │ │ │ │ └── error.jsx │ │ │ └── todos │ │ │ │ ├── index.jsx │ │ │ │ └── new.jsx │ │ │ ├── reducers │ │ │ ├── error.js │ │ │ ├── index.js │ │ │ └── todos.js │ │ │ └── store.js │ │ ├── package.json │ │ └── webpack.config.js │ └── todos-1.gif ├── index.js ├── info └── kubelessInfo.js ├── invoke └── kubelessInvoke.js ├── lib ├── config.js ├── crd.js ├── deploy.js ├── get-info.js ├── get-logs.js ├── helpers.js ├── invoke.js ├── remove.js ├── strategy.js └── strategy │ ├── base64_zip_content.js │ └── s3_zip_content.js ├── logs └── kubelessLogs.js ├── package-lock.json ├── package.json ├── provider └── kubelessProvider.js ├── remove └── kubelessRemove.js ├── scripts ├── install-minikube.sh ├── integration-tests.sh └── release.sh └── test ├── config.test.js ├── examples-test.js ├── helpers.test.js ├── kafka-novols.jsonnet ├── kubelessDeploy.test.js ├── kubelessDeployFunction.test.js ├── kubelessDeployStrategy.test.js ├── kubelessInfo.test.js ├── kubelessInvoke.test.js ├── kubelessLogs.test.js ├── kubelessRemove.test.js ├── lib ├── load-kube-config.js ├── mocks.js ├── rm.js └── serverless.js └── minio.yml /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | /examples/ -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "airbnb", 3 | "plugins": [], 4 | "rules": { 5 | "func-names": "off", 6 | "strict": "off", 7 | "prefer-rest-params": "off", 8 | "react/require-extension" : "off", 9 | "import/no-extraneous-dependencies" : "off", 10 | "no-console": "off" 11 | }, 12 | "env": { 13 | "mocha": true, 14 | "jest": true 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | # ESLint cache 4 | .eslintcache 5 | coverage 6 | .serverless 7 | # Test leftovers 8 | node-dependencies-* 9 | ksonnet-lib 10 | kubeless_linux-amd64.zip 11 | test/kubeless.jsonnet 12 | test/kafka-zookeeper.jsonnet 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '12' 4 | script: 5 | - npm run lint 6 | - npm test 7 | - "./scripts/integration-tests.sh" 8 | services: 9 | - docker 10 | env: 11 | global: 12 | - KUBELESS_VERSION: v1.0.7 13 | - KUBELESS_KAFKA_VERSION: v1.0.2 14 | - MINIKUBE_VERSION: v0.25.2 15 | - REPO_DOMAIN: serverless 16 | - REPO_NAME: serverless-kubeless 17 | - secure: s+L8ndj0uMNwqbLvbHePHeMJw2LI8DdEdcq1vJ98hNwHOWQc2mHVB4utG9EZFkaL+RAZYduldSJqr443d2BugxrkmzhLUlM5vDks+zHeKecwTah2uuaMUXVT/y/cWDDTVp3phqSqWbHBMG6u0ImvTVWHpnkux55S3QJTHevvhdodpO6VDTsJCEB3e1d2hHxi0L9tJrFXzQRpooV8IUuODwKBJyhK4CD7rvu0D1gBgHaUNnNLrCy4YTaFl19q5NdZUtrQDC7rpSPOhFI9CBFX8GiFq6nY3XzFASwq/JtKc3K7OLIC7Wqb6JpuvFhG6S1yhBzp73pnoE9U0Bi+YMa3L+nPoh58dCB2ldNCCCMbx7R6PWq/TwYzLvgZZ7queC2kbvCTrtU6JJfmb0CxmX1fnUIpCsNeyXaPuo4Ly6WJeAID32z79CwMo9NH0uOVTLy3LTrLcEfELhBRL5+WkMvKmXUt8yN/jEIa/H38pQN5Y/AnJ0KznO8RZ2nLhi1cR+xUkxfPVZ22Wr2XkbzJDZih/mZR+5GQBfUHWgpUChK+e8dOhplk+4PZJEO6Myja7ykXBPYtL9CV/Xi+1nQqLmfhyChiES201KusJr1IrFklslzCzgrSH8Dv2yaYUTe/Ub/I3gWIhKOXY66gkpmB6MSBJUfMK3uR4/wYfCvbtBTugsY= 18 | install: 19 | - npm install -g serverless 20 | - npm install 21 | after_success: 22 | - | 23 | if [[ "$TRAVIS_BRANCH" == master && \ 24 | "$TRAVIS_PULL_REQUEST" == false && \ 25 | -n "$ACCESS_TOKEN" ]]; then 26 | ./scripts/release.sh 27 | fi 28 | deploy: 29 | provider: npm 30 | email: kubernetes@bitnami.com 31 | api_key: 32 | secure: FRnE09xWf09t1GRPKWVIlnRFmIV7txT6O+rR7w8dnq0NLIgJSd2O8PAm+D7KgMc1TI4Gg4PCF4XDHYPgkBBOFtOKyrcN9djameHK9u98u0RHV4NG+LIvSh6X32HqOT+0YmF4fckFNUsnme/gOJ0dvJpMuNHzCR7rAwPIMu4LG+d1v5WVGq1i5rMqmXpQBtWDIio+nguPtuBIyrWzoEaKJHxGBgdr6+UJ3BW551RB8dvlU0OnBH1YalIvh5+rbw1vMCS0yfIbQSq+NzLUIod7aswWHJx7HJ1HUNvK7RSOMB406dh5/PNjccnvlfMW5RcxYYrp3kB6pTJEuBE/9cUKJwCiuhNulA45oWoo05ITwXGOXLz1xHpIY3xHQJZVUvBJ/WnXplqUGKTGRaQrN13Njclh6KYvzQfMTjlLhoZyUrysqfyDyuzuzneYCPEDmRoh/9Y9J1kRtNLBoXHgRf+ysMFCfTsQW743GbQPlXk8ZD2cQHFn9AKl3nKbhNAwW07A/NU+xDj6JEPYikjn2Nd2bGbWCMR5oJ1sRmTrqYIfSVjYfpn3yt/WwGxxEvNHrz7U69kOtPn/oKY241s/UQdg9U+Vqv8x62GRRE0sCSq9ro2eNwbENBCu9xMO+3iOz8f4/U32NeY0Ij2D0gm2vUBbcZacpfBK72UxR1Gw6fZqT28= 33 | on: 34 | tags: true 35 | repo: serverless/serverless-kubeless 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2017 Bitnami. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **📦 Archived - This repository is archived and preserved for reference only. No updates, issues, or pull requests will be accepted. If you have questions, please reach out to our support team.** 2 | 3 | --- 4 | 5 | # Kubeless Serverless Plugin 6 | 7 | This plugin brings [Kubeless](https://github.com/kubeless/kubeless) support within the [Serverless Framework](https://github.com/serverless). 8 | 9 | Kubeless is a Kubernetes-native Serverless solution. 10 | 11 | ## Pre requisites 12 | 13 | Make sure you have a kubernetes endpoint running and kubeless installed. You can find the installation intructions [here](https://github.com/kubeless/kubeless#installation). 14 | 15 | Once you have Kubeless running in your cluster you can install serverless 16 | 17 | ```bash 18 | $ npm install serverless -g 19 | ``` 20 | 21 | ## Try out the example 22 | 23 | Clone this repo and check the example function 24 | 25 | ```bash 26 | $ git clone https://github.com/serverless/serverless-kubeless 27 | $ cd serverless-kubeless/examples/get-python 28 | $ cat serverless.yml 29 | service: hello 30 | 31 | provider: 32 | name: kubeless 33 | runtime: python2.7 34 | 35 | plugins: 36 | - serverless-kubeless 37 | 38 | functions: 39 | hello: 40 | description: 'Hello function' 41 | handler: handler.hello 42 | ``` 43 | 44 | Download dependencies 45 | 46 | ```bash 47 | $ npm install 48 | ``` 49 | 50 | Deploy function. 51 | 52 | ```bash 53 | $ serverless deploy 54 | Serverless: Packaging service... 55 | Serverless: Excluding development dependencies... 56 | Serverless: Deploying function hello... 57 | Serverless: Function hello successfully deployed 58 | ``` 59 | 60 | The function will be deployed to k8s via kubeless. 61 | 62 | ```bash 63 | $ kubectl get function 64 | NAME AGE 65 | hello 50s 66 | 67 | $ kubectl get pod 68 | NAME READY STATUS RESTARTS AGE 69 | hello-1815473417-1ttt7 1/1 Running 0 1m 70 | ``` 71 | 72 | Now you will be able to call the function: 73 | 74 | ```bash 75 | $ serverless invoke -f hello -l 76 | Serverless: Calling function: hello... 77 | -------------------------------------------------------------------- 78 | hello world 79 | ``` 80 | 81 | You can also check the logs for the function: 82 | 83 | ```bash 84 | $ serverless logs -f hello 85 | 172.17.0.1 - - [12/Jul/2017:09:47:18 +0000] "GET /healthz HTTP/1.1" 200 2 "" "Go-http-client/1.1" 0/118 86 | 172.17.0.1 - - [12/Jul/2017:09:47:21 +0000] "GET /healthz HTTP/1.1" 200 2 "" "Go-http-client/1.1" 0/93 87 | 172.17.0.1 - - [12/Jul/2017:09:47:24 +0000] "GET /healthz HTTP/1.1" 200 2 "" "Go-http-client/1.1" 0/108 88 | 172.17.0.1 - - [12/Jul/2017:09:47:25 +0000] "GET / HTTP/1.1" 200 11 "" "" 0/316 89 | ``` 90 | 91 | Or you can obtain the function information: 92 | 93 | ```bash 94 | $ serverless info 95 | Service Information "hello" 96 | Cluster IP: 10.0.0.51 97 | Type: ClusterIP 98 | Ports: 99 | Name: http-function-port 100 | Protocol: TCP 101 | Port: 8080 102 | Target Port: 8080 103 | Function Info 104 | Description: Hello function 105 | Handler: handler.hello 106 | Runtime: python2.7 107 | Trigger: HTTP 108 | Dependencies: 109 | ``` 110 | 111 | You can access the function through its HTTP interface as well using `kubectl proxy` and accessing: 112 | 113 | ```bash 114 | $ curl http://127.0.0.1:8001/api/v1/namespaces/default/services/hello/proxy/ 115 | hello world 116 | ``` 117 | 118 | If you have a change in your function and you want to redeploy it you can run: 119 | 120 | ```bash 121 | $ serverless deploy function -f hello 122 | Serverless: Redeploying hello... 123 | Serverless: Function hello successfully deployed 124 | ``` 125 | 126 | Finally you can remove the function. 127 | 128 | ```bash 129 | $ serverless remove 130 | Serverless: Removing function: hello... 131 | Serverless: Function hello successfully deleted 132 | ``` 133 | 134 | ## Kubernetes secrets 135 | 136 | Kubernetes secret objects let you store and manage sensitive information, such as passwords, OAuth tokens, and ssh keys. 137 | Putting this information in a secret is safer and more flexible than putting it verbatim in a Pod definition or in a container image. To use secrets follow the next steps: 138 | 139 | 1. Create a K8s secret: 140 | `kubectl create secret generic secret-file --from-file=secret.txt -n namespace-name` 141 | 142 | 2. Add the secret key into the provider definition in the serverless.yml file below the `secrets` key. You can specify an array of secrets and they will be mounted at root level in the pod file system using the path `/`: 143 | 144 | ```yaml 145 | ... 146 | functions: 147 | my-handler: 148 | secrets: 149 | - secret-file 150 | ... 151 | ``` 152 | 153 | 3. Now inside your pod, you will be able to access the route `/secret-file/secret.txt`. 154 | -------------------------------------------------------------------------------- /deploy/kubelessDeploy.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Bitnami. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const _ = require('lodash'); 20 | const BbPromise = require('bluebird'); 21 | const Config = require('../lib/config'); 22 | const deploy = require('../lib/deploy'); 23 | const Strategy = require('../lib/strategy'); 24 | const fs = require('fs'); 25 | const helpers = require('../lib/helpers'); 26 | const JSZip = require('jszip'); 27 | 28 | class KubelessDeploy { 29 | constructor(serverless, options) { 30 | this.serverless = serverless; 31 | this.options = options || {}; 32 | this.provider = this.serverless.getProvider('kubeless'); 33 | 34 | this.hooks = { 35 | 'before:package:createDeploymentArtifacts': () => BbPromise.bind(this) 36 | .then(this.excludes), 37 | 'deploy:deploy': () => BbPromise.bind(this) 38 | .then(this.validate) 39 | .then(this.deployFunction), 40 | }; 41 | // Store the result of loading the Zip file 42 | this.loadZip = _.memoize(JSZip.loadAsync); 43 | } 44 | 45 | excludes() { 46 | const exclude = this.serverless.service.package.exclude || []; 47 | exclude.push('node_modules/**'); 48 | this.serverless.service.package.exclude = exclude; 49 | } 50 | 51 | validate() { 52 | const unsupportedOptions = ['stage', 'region']; 53 | helpers.warnUnsupportedOptions( 54 | unsupportedOptions, 55 | this.options, 56 | this.serverless.cli.log.bind(this.serverless.cli) 57 | ); 58 | return BbPromise.resolve(); 59 | } 60 | 61 | getFileContent(zipFile, relativePath) { 62 | return this.loadZip(fs.readFileSync(zipFile)).then( 63 | (zip) => zip.file(relativePath).async('string') 64 | ); 65 | } 66 | 67 | checkSize(pkg) { 68 | const stat = fs.statSync(pkg); 69 | // Maximum size for a etcd entry is 1 MB and right now Kubeless is storing files as 70 | // etcd entries 71 | const oneMB = 1024 * 1024; 72 | if (stat.size > oneMB) { 73 | this.serverless.cli.log( 74 | `WARNING! Function zip file is ${Math.round(stat.size / oneMB)}MB. ` + 75 | 'The maximum size allowed is 1MB: please use package.exclude directives to include ' + 76 | 'only the required files' 77 | ); 78 | } 79 | } 80 | 81 | getPkg(description, funcName) { 82 | const pkg = this.options.package || 83 | this.serverless.service.package.path || 84 | this.serverless.service.package.artifact || 85 | description.package.artifact || 86 | this.serverless.config.serverless.service.artifact; 87 | 88 | 89 | // if using the package option and packaging inidividually 90 | // then we're expecting a directory where artifacts for all the finctions live 91 | if (this.options.package && this.serverless.service.package.individually) { 92 | if (fs.lstatSync(pkg).isDirectory()) { 93 | return `${pkg + funcName}.zip`; 94 | } 95 | const errMsg = 'Expecting the Paramater to be a directory ' + 96 | 'for individualy packaged functions'; 97 | this.serverless.cli.log(errMsg); 98 | throw new Error(errMsg); 99 | } 100 | return pkg; 101 | } 102 | 103 | 104 | deployFunction() { 105 | const runtime = this.serverless.service.provider.runtime; 106 | const populatedFunctions = []; 107 | const kubelessConfig = new Config(); 108 | return new BbPromise((resolve, reject) => { 109 | kubelessConfig.init().then(() => { 110 | _.each(this.serverless.service.functions, (description, name) => { 111 | const pkg = this.getPkg(description, name); 112 | 113 | this.checkSize(pkg); 114 | 115 | if (description.handler) { 116 | const depFile = helpers.getRuntimeDepfile(description.runtime || runtime, 117 | kubelessConfig); 118 | 119 | (new Strategy(this.serverless)).factory().deploy(description, pkg) 120 | .catch(reject) 121 | .then(deployOptions => { 122 | this.getFileContent(pkg, depFile) 123 | .catch(() => { 124 | // No requirements found 125 | }) 126 | .then((requirementsContent) => { 127 | populatedFunctions.push(_.assign({}, description, deployOptions, { 128 | id: name, 129 | deps: requirementsContent, 130 | image: description.image || this.serverless.service.provider.image, 131 | events: _.map(description.events, (event) => { 132 | const type = _.keys(event)[0]; 133 | if (type === 'trigger') { 134 | return _.assign({ type }, { trigger: event[type] }); 135 | } else if (type === 'schedule') { 136 | return _.assign({ type }, { schedule: event[type] }); 137 | } 138 | return _.assign({ type }, event[type]); 139 | }), 140 | })); 141 | if (populatedFunctions.length === 142 | _.keys(this.serverless.service.functions).length) { 143 | resolve(); 144 | } 145 | }); 146 | }); 147 | } else { 148 | populatedFunctions.push(_.assign({}, description, { id: name })); 149 | if (populatedFunctions.length === _.keys(this.serverless.service.functions).length) { 150 | resolve(); 151 | } 152 | } 153 | }); 154 | }); 155 | }).then(() => deploy( 156 | populatedFunctions, 157 | runtime, 158 | this.serverless.service.service, 159 | { 160 | namespace: this.serverless.service.provider.namespace, 161 | hostname: this.serverless.service.provider.hostname, 162 | defaultDNSResolution: this.serverless.service.provider.defaultDNSResolution, 163 | ingress: this.serverless.service.provider.ingress, 164 | cpu: this.serverless.service.provider.cpu, 165 | memorySize: this.serverless.service.provider.memorySize, 166 | affinity: this.serverless.service.provider.affinity, 167 | tolerations: this.serverless.service.provider.tolerations, 168 | force: this.options.force, 169 | verbose: this.options.verbose, 170 | log: this.serverless.cli.log.bind(this.serverless.cli), 171 | timeout: this.serverless.service.provider.timeout, 172 | environment: this.serverless.service.provider.environment, 173 | } 174 | )); 175 | } 176 | } 177 | 178 | module.exports = KubelessDeploy; 179 | -------------------------------------------------------------------------------- /deployFunction/kubelessDeployFunction.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Bitnami. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const _ = require('lodash'); 20 | const BbPromise = require('bluebird'); 21 | const KubelessDeploy = require('../deploy/kubelessDeploy'); 22 | 23 | class KubelessDeployFunction extends KubelessDeploy { 24 | constructor(serverless, options) { 25 | super(serverless, options); 26 | if (this.options.v) this.options.verbose = true; 27 | this.options.force = true; 28 | this.hooks = { 29 | 'deploy:function:initialize': () => BbPromise.bind(this) 30 | .then(this.excludes), 31 | 'deploy:function:packageFunction': () => this.serverless.pluginManager 32 | .spawn('package:function'), 33 | 'deploy:function:deploy': () => BbPromise.bind(this) 34 | .then(this.validate) 35 | .then(this.deployFunction), 36 | }; 37 | } 38 | 39 | deployFunction() { 40 | // Pick only the function that we are interested in 41 | this.serverless.service.functions = _.pick( 42 | this.serverless.service.functions, 43 | this.options.function 44 | ); 45 | if (_.isEmpty(this.serverless.service.functions)) { 46 | throw new Error( 47 | `The function ${this.options.function} is not present in the current description` 48 | ); 49 | } 50 | this.serverless.cli.log(`Redeploying ${this.options.function}...`); 51 | return super.deployFunction(); 52 | } 53 | } 54 | 55 | module.exports = KubelessDeployFunction; 56 | -------------------------------------------------------------------------------- /examples/cert-manager-https/README.md: -------------------------------------------------------------------------------- 1 | # Cert manager https example 2 | 3 | In this example we will deploy a function that will have ssl automatically setup by cert manager. 4 | 5 | ## Prerequisites 6 | 7 | * Cert Manager install with [cluster issuer shim](https://cert-manager.readthedocs.io/en/latest/reference/ingress-shim.html) setup. 8 | * Optional [external dns](https://github.com/kubernetes-incubator/external-dns) setup to automate dns configuration. 9 | 10 | ## Deploy 11 | 12 | ```console 13 | $ npm install 14 | $ serverless deploy 15 | ``` -------------------------------------------------------------------------------- /examples/cert-manager-https/handler.py: -------------------------------------------------------------------------------- 1 | def hello(event, context): 2 | print event['data'] 3 | -------------------------------------------------------------------------------- /examples/cert-manager-https/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-events", 3 | "version": "1.0.0", 4 | "description": "Example function for serverless kubeless", 5 | "dependencies": { 6 | "serverless-kubeless": "^0.7.0" 7 | }, 8 | "devDependencies": {}, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "", 13 | "license": "Apache-2.0" 14 | } 15 | -------------------------------------------------------------------------------- /examples/cert-manager-https/serverless.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: cert-manager-https 3 | 4 | plugins: 5 | - serverless-kubeless 6 | 7 | provider: 8 | name: kubeless 9 | hostname: example.com 10 | runtime: nodejs8 11 | ingress: 12 | additionalAnnotations: 13 | kubernetes.io/tls-acme: "true" 14 | tlsConfig: 15 | - hosts: 16 | - "example.com" 17 | secretName: ingress-example-com-certs 18 | 19 | functions: 20 | hello: 21 | handler: handler.hello 22 | events: 23 | - http: 24 | method: get 25 | path: hello 26 | -------------------------------------------------------------------------------- /examples/custom-ingress/README.md: -------------------------------------------------------------------------------- 1 | # Simple Hello World function with custom kubernetes Ingress class & annotations 2 | 3 | In this example we will deploy a function that with custom annotations in its ingress metadata. Check the `serverless.yml` file to see the specific syntax. 4 | 5 | ## Pre requisites 6 | 7 | We will need to have an Ingress controller deployed. 8 | 9 | ## Background info 10 | 11 | An Ingress controller will typically have a class name, for example: 12 | 13 | ``` 14 | kubernetes.io/ingress.class: nginx 15 | # or perhaps 16 | kubernetes.io/ingress.class: azure/application-gateway 17 | ``` 18 | 19 | An Ingress controller will also have annotations to control their behavior, like these: 20 | 21 | ``` 22 | nginx.ingress.kubernetes.io/example-rule-1: true 23 | # or another ingress annotation might look like this 24 | appgw.ingress.kubernetes.io/rule-example-2: false 25 | ``` 26 | 27 | An Ingress class isn't nessesarily the same as it's name in annotations. 28 | (seen above with `azure/application-gateway` and `appgw`) 29 | 30 | ## Deployment 31 | 32 | ```console 33 | $ npm install 34 | $ serverless deploy 35 | Serverless: Packaging service... 36 | Serverless: Excluding development dependencies... 37 | Serverless: Function hello successfully deployed 38 | ``` 39 | 40 | Make sure you have `kubectl` installed and that it can access your cluster (See documentation for your cloud provider if using a managed k8s service.) 41 | 42 | We can now get the Ingress information for our new function. (If this doesn't work, as a workout use `edit` instead of `describe`) 43 | 44 | ``` 45 | $ kubectl describe Ingress hello 46 | apiVersion: extensions/v1beta1 47 | kind: Ingress 48 | metadata: 49 | annotations: 50 | ingressName.ingress.kubernetes.io/rewrite-target: / 51 | ingressName.ingress.kubernetes.io/example-rule-1: "true" 52 | ingressName.ingress.kubernetes.io/example-rule-2: "false" 53 | kubernetes.io/ingress.class: foo/bar-ingress-class 54 | creationTimestamp: "2019-08-07T11:09:20Z" 55 | generation: 2 56 | name: hello 57 | namespace: default 58 | resourceVersion: "191340" 59 | selfLink: /apis/extensions/v1beta1/namespaces/default/ingresses/hello 60 | uid: aaaaa000-a000-a000-a000-a000a000a000 61 | spec: 62 | rules: 63 | - host: example.com 64 | http: 65 | paths: 66 | - backend: 67 | serviceName: hello 68 | servicePort: 8080 69 | path: /hello/ 70 | status: 71 | loadBalancer: {} 72 | ``` -------------------------------------------------------------------------------- /examples/custom-ingress/handler.py: -------------------------------------------------------------------------------- 1 | def hello(event, context): 2 | return "hello world" 3 | -------------------------------------------------------------------------------- /examples/custom-ingress/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello", 3 | "version": "1.0.0", 4 | "description": "Example function for serverless kubeless", 5 | "dependencies": { 6 | "serverless-kubeless": "^0.7.2" 7 | }, 8 | "devDependencies": {}, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "", 13 | "license": "Apache-2.0" 14 | } 15 | -------------------------------------------------------------------------------- /examples/custom-ingress/serverless.yml: -------------------------------------------------------------------------------- 1 | service: hello 2 | 3 | provider: 4 | name: kubeless 5 | runtime: python2.7 6 | defaultDNSResolution: 'xip.io' 7 | ingress: 8 | class: "foo/bar-ingress" # Becomes: `kubernetes.io/ingress.class: foo/bar-ingress` 9 | name: "ingressName" # Becomes: `ingressName.ingress.kubernetes.io/rewrite-target: /` 10 | additionalAnnotations: 11 | "ingressName.ingress.kubernetes.io/example-rule-1": true 12 | "ingressName.ingress.kubernetes.io/example-rule-2": false 13 | 14 | 15 | plugins: 16 | - serverless-kubeless 17 | 18 | functions: 19 | hello: 20 | handler: handler.hello 21 | events: 22 | - http: 23 | path: /hello 24 | -------------------------------------------------------------------------------- /examples/event-trigger-python/README.md: -------------------------------------------------------------------------------- 1 | # Simple Event Triggered function 2 | 3 | In this example we will deploy a function that will be triggered whenever a message is published under a certain topic. 4 | 5 | The topic in which the function will be listening is defined in the `events` section of the `serverless.yml` 6 | 7 | ```console 8 | $ npm install 9 | $ serverless deploy 10 | $ kubeless topic publish --topic hello_topic --data 'hello world!' # push a message into the queue 11 | $ serverless logs -f events 12 | hello world! 13 | ``` 14 | -------------------------------------------------------------------------------- /examples/event-trigger-python/handler.py: -------------------------------------------------------------------------------- 1 | def events(event, context): 2 | print event['data'] 3 | -------------------------------------------------------------------------------- /examples/event-trigger-python/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-events", 3 | "version": "1.0.0", 4 | "description": "Example function for serverless kubeless", 5 | "dependencies": { 6 | "serverless-kubeless": "^0.7.0" 7 | }, 8 | "devDependencies": {}, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "", 13 | "license": "Apache-2.0" 14 | } 15 | -------------------------------------------------------------------------------- /examples/event-trigger-python/serverless.yml: -------------------------------------------------------------------------------- 1 | service: events 2 | 3 | provider: 4 | name: kubeless 5 | runtime: python2.7 6 | 7 | plugins: 8 | - serverless-kubeless 9 | 10 | functions: 11 | events: 12 | handler: handler.events 13 | events: 14 | - trigger: 'hello_topic' 15 | -------------------------------------------------------------------------------- /examples/get-python/README.md: -------------------------------------------------------------------------------- 1 | # Simple Hello World function 2 | 3 | ```console 4 | $ npm install 5 | $ serverless deploy 6 | $ serverless invoke -f echo -l 7 | Serverless: Calling function: echo... 8 | -------------------------------------------------------------------- 9 | hello world 10 | $ serverless remove 11 | ``` 12 | -------------------------------------------------------------------------------- /examples/get-python/handler.py: -------------------------------------------------------------------------------- 1 | def hello(event, context): 2 | return "hello world" 3 | -------------------------------------------------------------------------------- /examples/get-python/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello", 3 | "version": "1.0.0", 4 | "description": "Example function for serverless kubeless", 5 | "dependencies": { 6 | "serverless-kubeless": "^0.7.0" 7 | }, 8 | "devDependencies": {}, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "", 13 | "license": "Apache-2.0" 14 | } 15 | -------------------------------------------------------------------------------- /examples/get-python/serverless.yml: -------------------------------------------------------------------------------- 1 | service: hello 2 | 3 | provider: 4 | name: kubeless 5 | runtime: python3.7 6 | 7 | plugins: 8 | - serverless-kubeless 9 | 10 | functions: 11 | hello: 12 | description: 'Hello function' 13 | handler: handler.hello 14 | -------------------------------------------------------------------------------- /examples/get-ruby/README.md: -------------------------------------------------------------------------------- 1 | # Simple Ruby function 2 | 3 | This function calls the Github API for retrieving the latest version of kubeless 4 | 5 | ```console 6 | $ npm install 7 | $ serverless deploy 8 | $ serverless invoke -f version -l 9 | Serverless: Calling function: version... 10 | -------------------------------------------------------------------- 11 | 0.0.16 12 | $ serverless remove 13 | ``` 14 | -------------------------------------------------------------------------------- /examples/get-ruby/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "version", 3 | "version": "1.0.0", 4 | "description": "Example function for serverless kubeless", 5 | "dependencies": { 6 | "serverless-kubeless": "^0.7.0" 7 | }, 8 | "devDependencies": {}, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "", 13 | "license": "Apache-2.0" 14 | } 15 | -------------------------------------------------------------------------------- /examples/get-ruby/serverless.yml: -------------------------------------------------------------------------------- 1 | service: version 2 | 3 | provider: 4 | name: kubeless 5 | runtime: ruby2.4 6 | 7 | plugins: 8 | - serverless-kubeless 9 | 10 | functions: 11 | version: 12 | handler: version.run 13 | -------------------------------------------------------------------------------- /examples/get-ruby/version.rb: -------------------------------------------------------------------------------- 1 | # Obtains the latest Kubeless release published 2 | def run(event, context) 3 | require "net/https" 4 | require "uri" 5 | require "json" 6 | 7 | # Fetch release info 8 | uri = URI.parse("https://api.github.com/repos/bitnami/kubeless/releases") 9 | http = Net::HTTP.new(uri.host, uri.port) 10 | request = Net::HTTP::Get.new(uri.request_uri) 11 | http.use_ssl = true 12 | http.verify_mode = OpenSSL::SSL::VERIFY_PEER 13 | response = http.request(request) 14 | 15 | # Follow redirects if needed 16 | if response.code == "301" 17 | response = Net::HTTP.get_response(URI.parse(response.header['location'])) 18 | end 19 | 20 | # Parse response 21 | output = JSON.parse(response.body) 22 | puts output 23 | # Create a Hash for output 24 | output_hash = { :version => output[0]['tag_name'] } 25 | 26 | return output_hash[:version] 27 | end 28 | -------------------------------------------------------------------------------- /examples/http-custom-path/README.md: -------------------------------------------------------------------------------- 1 | # Simple Hello World function available in a certain HTTP path 2 | 3 | In this example we will deploy a function that will be available under the path `/hello`. Check the `serverless.yml` file to see the specific syntax. 4 | 5 | ## Pre requisites 6 | 7 | We will need to have an Ingress controller deployed in order to be able to deploy your function in a specific path. If you don't have it yet and you are working with minikube you can enable the addon executing: 8 | 9 | ``` 10 | minikube addons enable ingress 11 | ``` 12 | 13 | ## Deployment 14 | 15 | ```console 16 | $ npm install 17 | $ serverless deploy -v 18 | Serverless: Packaging service... 19 | Serverless: Deploying function hello... 20 | Serverless: Deployed Ingress rule to map /hello 21 | Serverless: Waiting for function hello to be fully deployed. Pods status: {"waiting":{"reason":"PodInitializing"}} 22 | Serverless: Function hello successfully deployed 23 | ``` 24 | 25 | As we can see in the logs an Ingress Rule has been deployed to run our function at `/hello`. If no host is specified, by default it will use `API_URL.nip.io` being `API_URL` the URL/IP of the Kubernetes IP. We can know the specific URL in which the function will be listening executing `serverless info`: 26 | ```console 27 | $ serverless info 28 | Service Information "hello" 29 | Cluster IP: 10.0.0.161 30 | Type: NodePort 31 | Ports: 32 | Protocol: TCP 33 | Port: 8080 34 | Target Port: 8080 35 | Node Port: 31444 36 | Function Info 37 | URL: 192.168.99.100.nip.io/hello 38 | Handler: handler.hello 39 | Runtime: python2.7 40 | Trigger: HTTP 41 | Dependencies: 42 | ``` 43 | 44 | Note that if you don't specify a hostname in your `serverless.yaml` it will be configured to use a DNS service like [`nip.io`](http://nip.io) setting the property `defaultDNSResolution` in the provider section. You can also change the default DNS resolutor to a different service like [`xip.io`](http//xip.io). 45 | 46 | Depending on the Ingress configuration the URL may be redirected to use the HTTPS protocol. You can call your function with a browser or executing: 47 | ```console 48 | $ curl 192.168.99.100.nip.io/hello 49 | hello world 50 | ``` 51 | 52 | ## GKE and Firewall limitation 53 | 54 | For some providers like Google you may need to add a firewall rule for allowing the traffic for the port 80 and 443 so you can connect to the IP the ingress controller provides. 55 | 56 | Note that even though GCE has its own ingress controller available by default it is not suitable for our use case since the annotation `ingress.kubernetes.io/rewrite-target` is not interpreted by that controller. You will need to deploy an Nginx controller like the one explained in the [pre requisites section](#pre-requisites). 57 | -------------------------------------------------------------------------------- /examples/http-custom-path/handler.py: -------------------------------------------------------------------------------- 1 | def hello(event, context): 2 | return "hello world" 3 | -------------------------------------------------------------------------------- /examples/http-custom-path/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-http", 3 | "version": "1.0.0", 4 | "description": "Example function for serverless kubeless", 5 | "dependencies": { 6 | "serverless-kubeless": "^0.7.0" 7 | }, 8 | "devDependencies": {}, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "", 13 | "license": "Apache-2.0" 14 | } 15 | -------------------------------------------------------------------------------- /examples/http-custom-path/serverless.yml: -------------------------------------------------------------------------------- 1 | service: hello-http 2 | 3 | provider: 4 | name: kubeless 5 | runtime: python2.7 6 | defaultDNSResolution: 'xip.io' 7 | 8 | plugins: 9 | - serverless-kubeless 10 | 11 | functions: 12 | hello-http: 13 | handler: handler.hello 14 | events: 15 | - http: 16 | path: /hello 17 | -------------------------------------------------------------------------------- /examples/multi-python/README.md: -------------------------------------------------------------------------------- 1 | # Multiple Functions in a Single Service 2 | 3 | A service can have multiple functions which can come from the same file. 4 | 5 | ``` 6 | functions: 7 | foo: 8 | handler: handler.foo 9 | bar: 10 | handler: handler.bar 11 | ``` 12 | 13 | 14 | ```console 15 | $ npm install 16 | $ serverless deploy 17 | Serverless: Packaging service... 18 | Serverless: Function foo successfully deployed 19 | Serverless: Function bar successfully deployed 20 | ``` 21 | 22 | You can invoke each function 23 | 24 | ```console 25 | $ serverless invoke -f foo -d '{"foo":"bar"}' -l 26 | Serverless: Calling function: foo... 27 | -------------------------------------------------------------------- 28 | foo 29 | $ serverless invoke -f bar -d '{"bar":"foo"}' -l 30 | Serverless: Calling function: bar... 31 | -------------------------------------------------------------------- 32 | bar 33 | ``` 34 | 35 | You can access the logs of each function 36 | 37 | ```console 38 | $ serverless logs -f bar 39 | Bottle v0.12.13 server starting up (using CherryPyServer())... 40 | Listening on http://0.0.0.0:8080/ 41 | Hit Ctrl-C to quit. 42 | 172.17.0.1 - - [18/Jul/2017:13:36:26 +0000] "GET /healthz HTTP/1.1" 200 2 "" "Go-http-client/1.1" 0/178 43 | {u'toto': u'tata'} 44 | 172.17.0.1 - - [18/Jul/2017:13:36:26 +0000] "POST / HTTP/1.1" 200 3 "" "" 0/336 45 | 46 | $ serverless logs -f foo 47 | Bottle v0.12.13 server starting up (using CherryPyServer())... 48 | Listening on http://0.0.0.0:8080/ 49 | Hit Ctrl-C to quit. 50 | 172.17.0.1 - - [18/Jul/2017:13:36:17 +0000] "GET /healthz HTTP/1.1" 200 2 "" "Go-http-client/1.1" 0/87 51 | {u'toto': u'tata'} 52 | 172.17.0.1 - - [18/Jul/2017:13:36:18 +0000] "POST / HTTP/1.1" 200 3 "" "" 0/352 53 | ``` 54 | 55 | Finally, remove the service and associated functions 56 | 57 | ```console 58 | $ serverless remove 59 | ``` 60 | -------------------------------------------------------------------------------- /examples/multi-python/handler.py: -------------------------------------------------------------------------------- 1 | def foo(event, context): 2 | print event['data'] 3 | return 'foo' 4 | 5 | 6 | def bar(event, context): 7 | print event['data'] 8 | return'bar' 9 | -------------------------------------------------------------------------------- /examples/multi-python/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multi-functions", 3 | "version": "1.0.0", 4 | "description": "Example function for serverless kubeless", 5 | "dependencies": { 6 | "serverless-kubeless": "^0.7.0" 7 | }, 8 | "devDependencies": {}, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "", 13 | "license": "Apache-2.0" 14 | } 15 | -------------------------------------------------------------------------------- /examples/multi-python/serverless.yml: -------------------------------------------------------------------------------- 1 | service: multi 2 | 3 | provider: 4 | name: kubeless 5 | runtime: python2.7 6 | 7 | plugins: 8 | - serverless-kubeless 9 | 10 | functions: 11 | foo: 12 | handler: handler.foo 13 | bar: 14 | handler: handler.bar 15 | -------------------------------------------------------------------------------- /examples/node-chaining-functions/README.md: -------------------------------------------------------------------------------- 1 | # Chaining Example 2 | 3 | This example concatenates three different functions (capitalize, pad and reverse) and return its result. 4 | 5 | ```console 6 | $ npm install 7 | $ serverless deploy 8 | $ serverless invoke -f chained_seq -l --data 'hello world!' 9 | Serverless: Calling function: chained_seq... 10 | Serverless: Calling function: capitalize... 11 | Serverless: Calling function: pad... 12 | Serverless: Calling function: reverse... 13 | -------------------------------------------------------------------- 14 | ****!dlrow olleH**** 15 | $ serverless remove 16 | ``` 17 | -------------------------------------------------------------------------------- /examples/node-chaining-functions/handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | module.exports = { 6 | capitalize(event, context) { 7 | return _.capitalize(event.data); 8 | }, 9 | pad(event, context) { 10 | return _.pad(event.data, 20, '*'); 11 | }, 12 | reverse(event, context) { 13 | return event.data.split('').reverse().join(''); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /examples/node-chaining-functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chain", 3 | "version": "1.0.0", 4 | "description": "Example function for serverless kubeless", 5 | "dependencies": { 6 | "lodash": "^4.1.0" 7 | }, 8 | "devDependencies": { 9 | "serverless-kubeless": "^0.7.0" 10 | }, 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "author": "", 15 | "license": "Apache-2.0" 16 | } 17 | -------------------------------------------------------------------------------- /examples/node-chaining-functions/serverless.yml: -------------------------------------------------------------------------------- 1 | service: chain 2 | 3 | provider: 4 | name: kubeless 5 | runtime: nodejs6 6 | 7 | plugins: 8 | - serverless-kubeless 9 | 10 | functions: 11 | capitalize: 12 | handler: handler.capitalize 13 | pad: 14 | handler: handler.pad 15 | reverse: 16 | handler: handler.reverse 17 | chained_seq: 18 | sequence: 19 | - capitalize 20 | - pad 21 | - reverse 22 | -------------------------------------------------------------------------------- /examples/post-go/README.md: -------------------------------------------------------------------------------- 1 | # Simple Hello World function 2 | 3 | This function returns the given data. 4 | 5 | ```console 6 | $ npm install 7 | $ serverless deploy 8 | $ serverless invoke -f go-echo -l --data 'hello!' 9 | Serverless: Calling function: go-echo... 10 | -------------------------------------------------------------------- 11 | hello! 12 | $ serverless remove 13 | ``` 14 | -------------------------------------------------------------------------------- /examples/post-go/go.mod: -------------------------------------------------------------------------------- 1 | module kubeless 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/kubeless/kubeless v1.0.7 7 | github.com/sirupsen/logrus v1.6.0 8 | ) 9 | -------------------------------------------------------------------------------- /examples/post-go/handler.go: -------------------------------------------------------------------------------- 1 | package kubeless 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "github.com/kubeless/kubeless/pkg/functions" 6 | ) 7 | 8 | // Handler returns the given data. 9 | func Handler(event functions.Event, context functions.Context) (string, error) { 10 | logrus.Println(event) 11 | return event.Data, nil 12 | } 13 | -------------------------------------------------------------------------------- /examples/post-go/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "go-echo", 3 | "version": "1.0.0", 4 | "description": "Example function for serverless kubeless", 5 | "dependencies": { 6 | "serverless-kubeless": "^0.10.1" 7 | }, 8 | "devDependencies": {}, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "", 13 | "license": "Apache-2.0" 14 | } 15 | -------------------------------------------------------------------------------- /examples/post-go/serverless.yml: -------------------------------------------------------------------------------- 1 | service: go-echo 2 | 3 | provider: 4 | name: kubeless 5 | runtime: go1.14 6 | 7 | plugins: 8 | - serverless-kubeless 9 | 10 | functions: 11 | go-echo: 12 | handler: handler.Handler 13 | -------------------------------------------------------------------------------- /examples/post-nodejs/README.md: -------------------------------------------------------------------------------- 1 | # Simple Hello World function 2 | 3 | This function returns the given data capitalizing the first word. 4 | 5 | ```console 6 | $ npm install 7 | $ serverless deploy 8 | $ serverless invoke -f capitalize -l --data 'hello world!' 9 | Serverless: Calling function: capitalize... 10 | -------------------------------------------------------------------- 11 | Hello world! 12 | $ serverless remove 13 | ``` 14 | -------------------------------------------------------------------------------- /examples/post-nodejs/handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | module.exports = { 6 | capitalize(event, context) { 7 | return _.capitalize(event.data); 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /examples/post-nodejs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "capitalize", 3 | "version": "1.0.0", 4 | "description": "Example function for serverless kubeless", 5 | "dependencies": { 6 | "lodash": "^4.1.0" 7 | }, 8 | "devDependencies": { 9 | "serverless-kubeless": "^0.7.0" 10 | }, 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "author": "", 15 | "license": "Apache-2.0" 16 | } 17 | -------------------------------------------------------------------------------- /examples/post-nodejs/serverless.yml: -------------------------------------------------------------------------------- 1 | service: capitalize 2 | 3 | provider: 4 | name: kubeless 5 | runtime: nodejs6 6 | 7 | plugins: 8 | - serverless-kubeless 9 | 10 | functions: 11 | capitalize: 12 | handler: handler.capitalize 13 | -------------------------------------------------------------------------------- /examples/post-php-s3/README.md: -------------------------------------------------------------------------------- 1 | # Simple Hello World function 2 | 3 | This function returns the given data capitalizing the first word. 4 | 5 | ```console 6 | $ npm install 7 | $ serverless deploy 8 | $ serverless invoke -f php-echo -l --data 'hello!' 9 | Serverless: Calling function: php-echo... 10 | -------------------------------------------------------------------- 11 | hello! 12 | $ serverless remove 13 | ``` 14 | -------------------------------------------------------------------------------- /examples/post-php-s3/handler.php: -------------------------------------------------------------------------------- 1 | data); 5 | } 6 | 7 | -------------------------------------------------------------------------------- /examples/post-php-s3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "php-echo", 3 | "version": "1.0.0", 4 | "description": "Example function for serverless kubeless", 5 | "dependencies": { 6 | "serverless-kubeless": "^0.7.0" 7 | }, 8 | "devDependencies": {}, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "", 13 | "license": "Apache-2.0" 14 | } 15 | -------------------------------------------------------------------------------- /examples/post-php-s3/serverless.yml: -------------------------------------------------------------------------------- 1 | service: php-echo-s3 2 | 3 | provider: 4 | name: kubeless 5 | runtime: php7.2 6 | deploy: 7 | strategy: S3ZipContent 8 | options: 9 | accessKeyId: minio 10 | secretAccessKey: minio123 11 | endpoint: http://10.98.211.80:9000 12 | bucket: kubeless 13 | region: eu-central-1 14 | s3ForcePathStyle: True 15 | 16 | plugins: 17 | - serverless-kubeless 18 | 19 | functions: 20 | php-echo-s3: 21 | handler: handler.foo 22 | -------------------------------------------------------------------------------- /examples/post-php/README.md: -------------------------------------------------------------------------------- 1 | # Simple Hello World function 2 | 3 | This function returns the given data capitalizing the first word. 4 | 5 | ```console 6 | $ npm install 7 | $ serverless deploy 8 | $ serverless invoke -f php-echo -l --data 'hello!' 9 | Serverless: Calling function: php-echo... 10 | -------------------------------------------------------------------- 11 | hello! 12 | $ serverless remove 13 | ``` 14 | -------------------------------------------------------------------------------- /examples/post-php/handler.php: -------------------------------------------------------------------------------- 1 | data); 5 | } 6 | 7 | -------------------------------------------------------------------------------- /examples/post-php/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "php-echo", 3 | "version": "1.0.0", 4 | "description": "Example function for serverless kubeless", 5 | "dependencies": { 6 | "serverless-kubeless": "^0.7.0" 7 | }, 8 | "devDependencies": {}, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "", 13 | "license": "Apache-2.0" 14 | } 15 | -------------------------------------------------------------------------------- /examples/post-php/serverless.yml: -------------------------------------------------------------------------------- 1 | service: php-echo 2 | 3 | provider: 4 | name: kubeless 5 | runtime: php7.2 6 | 7 | plugins: 8 | - serverless-kubeless 9 | 10 | functions: 11 | php-echo: 12 | handler: handler.foo 13 | -------------------------------------------------------------------------------- /examples/post-python/README.md: -------------------------------------------------------------------------------- 1 | # Simple Echo function 2 | 3 | ```console 4 | $ npm install 5 | $ serverless deploy 6 | $ serverless invoke -f echo --data '{"foo":"bar"}' -l 7 | Serverless: Calling function: echo... 8 | -------------------------------------------------------------------- 9 | { foo: 'bar' } 10 | $ serverless remove 11 | ``` 12 | -------------------------------------------------------------------------------- /examples/post-python/handler.py: -------------------------------------------------------------------------------- 1 | def echo(event, context): 2 | print event 3 | return event['data'] 4 | -------------------------------------------------------------------------------- /examples/post-python/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "echo", 3 | "version": "1.0.0", 4 | "description": "Example function for serverless kubeless", 5 | "dependencies": { 6 | "serverless-kubeless": "^0.7.0" 7 | }, 8 | "devDependencies": {}, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "", 13 | "license": "Apache-2.0" 14 | } 15 | -------------------------------------------------------------------------------- /examples/post-python/serverless.yml: -------------------------------------------------------------------------------- 1 | service: echo 2 | 3 | provider: 4 | name: kubeless 5 | runtime: python2.7 6 | 7 | plugins: 8 | - serverless-kubeless 9 | 10 | functions: 11 | echo: 12 | handler: handler.echo 13 | -------------------------------------------------------------------------------- /examples/post-ruby/README.md: -------------------------------------------------------------------------------- 1 | # Simple Ruby function 2 | 3 | This function receives a POST request and returns response 4 | 5 | ```console 6 | $ npm install 7 | $ serverless deploy 8 | $ serverless invoke -f ping --data 'ping' -l 9 | Serverless: Calling function: version... 10 | -------------------------------------------------------------------- 11 | pong 12 | $ serverless remove 13 | ``` 14 | -------------------------------------------------------------------------------- /examples/post-ruby/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ping", 3 | "version": "1.0.0", 4 | "description": "Example function for serverless kubeless", 5 | "dependencies": { 6 | "serverless-kubeless": "^0.7.0" 7 | }, 8 | "devDependencies": {}, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "", 13 | "license": "Apache-2.0" 14 | } 15 | -------------------------------------------------------------------------------- /examples/post-ruby/ping.rb: -------------------------------------------------------------------------------- 1 | def run(event, context) 2 | if event[:data] == 'ping' 3 | return 'pong' 4 | else 5 | return 'not ping pong!' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /examples/post-ruby/serverless.yml: -------------------------------------------------------------------------------- 1 | service: ping 2 | 3 | provider: 4 | name: kubeless 5 | runtime: ruby2.4 6 | 7 | plugins: 8 | - serverless-kubeless 9 | 10 | functions: 11 | ping: 12 | handler: ping.run 13 | -------------------------------------------------------------------------------- /examples/scheduled-node/README.md: -------------------------------------------------------------------------------- 1 | # Simple Hello World function 2 | 3 | This function schedules a function to be executed every minute. 4 | 5 | You can set the period between executions following the cron format. 6 | 7 | ```console 8 | $ npm install 9 | $ serverless deploy 10 | $ sls logs -f clock -t 11 | Loading /kubeless/handler.js 12 | ... 13 | ::ffff:172.17.0.1 - - [15/Sep/2017:14:29:03 +0000] "GET /healthz HTTP/1.1" 200 2 "-" "Go-http-client/1.1" 14 | 14:29 15 | ::ffff:172.17.0.8 - - [15/Sep/2017:14:29:09 +0000] "GET / HTTP/1.1" 200 - "-" "Wget" 16 | ::ffff:172.17.0.1 - - [15/Sep/2017:14:29:33 +0000] "GET /healthz HTTP/1.1" 200 2 "-" "Go-http-client/1.1" 17 | ::ffff:172.17.0.1 - - [15/Sep/2017:14:30:03 +0000] "GET /healthz HTTP/1.1" 200 2 "-" "Go-http-client/1.1" 18 | 14:30 19 | ::ffff:172.17.0.8 - - [15/Sep/2017:14:30:09 +0000] "GET / HTTP/1.1" 200 - "-" "Wget" 20 | $ serverless remove 21 | ``` 22 | -------------------------------------------------------------------------------- /examples/scheduled-node/handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | module.exports = { 6 | printClock(event, context) { 7 | const now = new Date().toTimeString( 8 | { hour: '2-digit', minute: '2-digit' } 9 | ).slice(0, 5); 10 | console.log(now); 11 | return now; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /examples/scheduled-node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clock", 3 | "version": "1.0.0", 4 | "description": "Example function for serverless kubeless", 5 | "dependencies": { 6 | "lodash": "^4.1.0" 7 | }, 8 | "devDependencies": { 9 | "serverless-kubeless": "^0.7.0" 10 | }, 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "author": "", 15 | "license": "Apache-2.0" 16 | } 17 | -------------------------------------------------------------------------------- /examples/scheduled-node/serverless.yml: -------------------------------------------------------------------------------- 1 | service: clock 2 | 3 | provider: 4 | name: kubeless 5 | runtime: nodejs6 6 | 7 | plugins: 8 | - serverless-kubeless 9 | 10 | functions: 11 | clock: 12 | handler: handler.printClock 13 | events: 14 | - schedule: "* * * * *" 15 | -------------------------------------------------------------------------------- /examples/todo-app/README.md: -------------------------------------------------------------------------------- 1 | # Todos 2 | 3 |

4 | Todos demo 5 |

6 | 7 | This is the source code of the Serverless `Todos` service. 8 | 9 | You'll find two directories here. Deploy them in the following order: 10 | 11 | 1. The [backend](backend) directory contains the whole Serverless service and it's corresponding function code. 12 | 2. The [frontend](frontend) directory contains the frontend you can connect to your backend to use the Todos service through your web browser. 13 | 14 | # Source 15 | 16 | This is a modified version of Philipp Muens Todo example from his serverless [book](https://github.com/pmuens/serverless-book/blob/master/06-serverless-by-example/02-a-serverless-todo-application.md). Modified to run on [Kubeless](https://github.com/kubeless/kubeless) 17 | -------------------------------------------------------------------------------- /examples/todo-app/backend/README.md: -------------------------------------------------------------------------------- 1 | # Serverless backend 2 | 3 | Do the following to deploy and use the backend: 4 | 5 | 1. Install kubeless following the instruction from the main [README.md](../../../README.md) 6 | 2. Install an Ingress Controller. If you don't have it yet and you are working with minikube you can enable the addon executing: 7 | ``` 8 | minikube addons enable ingress 9 | ``` 10 | 3. Deploy a MongoDB service. It will be used to store the state of our application: 11 | ```console 12 | $ curl -sL https://raw.githubusercontent.com/bitnami/bitnami-docker-mongodb/3.4.7-r0/kubernetes.yml | kubectl create -f - 13 | ``` 14 | 4. Run `npm install` to install the used npm packages 15 | 5. Run `serverless deploy` to deploy the `todo` service in our kubernetes cluster 16 | ```console 17 | $ serverless deploy 18 | Serverless: Packaging service... 19 | Serverless: Deploying function delete... 20 | Serverless: Deploying function update... 21 | Serverless: Deploying function read-one... 22 | Serverless: Deploying function create... 23 | Serverless: Deploying function read-all... 24 | Serverless: Function delete successfully deployed 25 | Serverless: Function read-all successfully deployed 26 | Serverless: Function update successfully deployed 27 | Serverless: Function create successfully deployed 28 | Serverless: Function read-one successfully deployed 29 | ``` 30 | 31 | # Running the Backend in GKE 32 | 33 | In case your cluster is running on GCE you need to perform some additional steps. First you need to follow the [guide for deploying an Ingress Controller](https://github.com/kubernetes/ingress-nginx/blob/master/docs/deploy/index.md). Make sure you execute the "Mandatory commands", the ones for "Install without RBAC roles" and also "GCE - GKE" (using RBAC). If you successfully follow the guide you should be able to see the Ingress Controller running in the `ingress-nginx` namespace: 34 | 35 | ``` 36 | $ kubectl get pods -n ingress-nginx 37 | NAME READY STATUS RESTARTS AGE 38 | default-http-backend-66b447d9cf-zs2zn 1/1 Running 0 13m 39 | nginx-ingress-controller-6fb4c56b69-cpd5b 1/1 Running 3 12m 40 | ``` 41 | 42 | After a couple of minutes you will see that the Ingress rule has an `address` associated: 43 | 44 | ``` 45 | $ kubectl get ingress 46 | NAME HOSTS ADDRESS PORTS AGE 47 | todos 35.196.179.155.xip.io 35.229.122.182 80 7m 48 | ``` 49 | 50 | Note that the `HOST` is not correct since the IP that the Ingress provided us is different. To modify it execute `kubectl edit ingress todos`. That will open an editor in which you can change the key `host: 35.196.179.155.xip.io` for `host: 35.229.122.182.xip.io` or simply remove the key and the value to make it compatible with any host. Once you do that you should be able to access the functions: 51 | 52 | ``` 53 | $ kubectl get ingress 54 | NAME HOSTS ADDRESS PORTS AGE 55 | todos 35.229.122.182.xip.io 35.229.122.182 80 7m 56 | $ curl 35.229.122.182.xip.io/read-all 57 | [] 58 | ``` 59 | 60 | This host is the one that should be used as `API_URL` in the frontend. 61 | -------------------------------------------------------------------------------- /examples/todo-app/backend/img/gce_firewall_rule_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless/serverless-kubeless/336fb2ada1f324dc6782d411642ca1b43d90794e/examples/todo-app/backend/img/gce_firewall_rule_edit.png -------------------------------------------------------------------------------- /examples/todo-app/backend/img/gce_firewall_rules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless/serverless-kubeless/336fb2ada1f324dc6782d411642ca1b43d90794e/examples/todo-app/backend/img/gce_firewall_rules.png -------------------------------------------------------------------------------- /examples/todo-app/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todos", 3 | "version": "0.1.0", 4 | "description": "Todo service built with Serverless", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "author": "", 9 | "license": "MIT", 10 | "dependencies": { 11 | "lodash": "^4.17.4", 12 | "mongodb": "^3.6.1", 13 | "uuid": "^2.0.3" 14 | }, 15 | "devDependencies": { 16 | "serverless-kubeless": "^0.7.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/todo-app/backend/serverless.yml: -------------------------------------------------------------------------------- 1 | service: todos 2 | 3 | provider: 4 | name: kubeless 5 | runtime: nodejs6 6 | defaultDNSResolution: 'xip.io' 7 | 8 | plugins: 9 | - serverless-kubeless 10 | 11 | functions: 12 | create: 13 | handler: todos-create.create 14 | events: 15 | - http: 16 | path: /create 17 | read-all: 18 | handler: todos-read-all.readAll 19 | events: 20 | - http: 21 | path: /read-all 22 | read-one: 23 | handler: todos-read-one.readOne 24 | events: 25 | - http: 26 | path: /read 27 | update: 28 | handler: todos-update.update 29 | events: 30 | - http: 31 | path: /update 32 | delete: 33 | handler: todos-delete.delete 34 | events: 35 | - http: 36 | path: /delete 37 | -------------------------------------------------------------------------------- /examples/todo-app/backend/todos-create.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mongodb = require('mongodb'); 4 | const uuid = require('uuid'); 5 | 6 | const MongoClient = mongodb.MongoClient; 7 | const url = 'mongodb://mongodb:27017/todo_app'; 8 | 9 | module.exports = { 10 | create: (event, context) => new Promise((resolve, reject) => { 11 | const data = event.data; 12 | data.id = uuid.v1(); 13 | data.updatedAt = new Date().getTime(); 14 | MongoClient.connect(url, (cerr, db) => { 15 | if (cerr) { 16 | reject(cerr); 17 | } else { 18 | db.collection('todos').insertOne(data, (errInsert) => { 19 | if (errInsert) { 20 | reject(errInsert); 21 | } else { 22 | resolve(JSON.stringify(data)); 23 | db.close(); 24 | } 25 | }); 26 | } 27 | }); 28 | }), 29 | }; 30 | -------------------------------------------------------------------------------- /examples/todo-app/backend/todos-delete.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const mongodb = require('mongodb'); 5 | 6 | const MongoClient = mongodb.MongoClient; 7 | const url = 'mongodb://mongodb:27017/todo_app'; 8 | 9 | module.exports = { 10 | delete: (event, context) => new Promise((resolve, reject) => { 11 | MongoClient.connect(url, (err, db) => { 12 | if (err) { 13 | reject(err); 14 | } else { 15 | db.collection('todos', (errC, doc) => { 16 | if (errC) { 17 | reject(ferr); 18 | } else { 19 | doc.find().toArray((ferr, docEntries) => { 20 | if (ferr) { 21 | reject(ferr); 22 | } else { 23 | const entry = _.find(docEntries, e => e.id === event.extensions.request.query.id); 24 | doc.deleteOne(entry, (derr) => { 25 | if (derr) { 26 | reject(derr); 27 | } else { 28 | db.close(); 29 | resolve(JSON.stringify(entry)); 30 | } 31 | }); 32 | } 33 | }); 34 | } 35 | }); 36 | } 37 | }); 38 | }), 39 | }; 40 | -------------------------------------------------------------------------------- /examples/todo-app/backend/todos-read-all.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const mongodb = require('mongodb'); 5 | 6 | const MongoClient = mongodb.MongoClient; 7 | const url = 'mongodb://mongodb:27017/todo_app'; 8 | 9 | module.exports = { 10 | readAll: (event, context) => new Promise((resolve, reject) => { 11 | MongoClient.connect(url, (err, db) => { 12 | if (err) { 13 | reject(err); 14 | } else { 15 | db.collection('todos').find().toArray((ferr, docEntries) => { 16 | if (ferr) { 17 | reject(ferr); 18 | } else { 19 | db.close(); 20 | resolve(JSON.stringify(docEntries)); 21 | } 22 | }); 23 | } 24 | }); 25 | }), 26 | }; 27 | -------------------------------------------------------------------------------- /examples/todo-app/backend/todos-read-one.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const mongodb = require('mongodb'); 5 | 6 | const MongoClient = mongodb.MongoClient; 7 | const url = 'mongodb://mongodb:27017/todo_app'; 8 | 9 | module.exports = { 10 | readOne: (event, context) => new Promise((resolve, reject) => { 11 | MongoClient.connect(url, (err, db) => { 12 | if (err) { 13 | reject(err); 14 | } else { 15 | db.collection('todos').find().toArray((ferr, docEntries) => { 16 | if (ferr) { 17 | reject(ferr); 18 | } else { 19 | const entry = _.find(docEntries, e => e.id === event.extensions.request.query.id); 20 | db.close(); 21 | resolve(JSON.stringify(entry)); 22 | } 23 | }); 24 | } 25 | }); 26 | }), 27 | }; 28 | -------------------------------------------------------------------------------- /examples/todo-app/backend/todos-update.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const mongodb = require('mongodb'); 5 | const uuid = require('uuid'); 6 | 7 | const MongoClient = mongodb.MongoClient; 8 | const url = 'mongodb://mongodb:27017/todo_app'; 9 | 10 | module.exports = { 11 | update: (event, context) => new Promise((resolve, reject) => { 12 | const data = event.data; 13 | MongoClient.connect(url, (err, db) => { 14 | if (err) { 15 | reject(err); 16 | } else { 17 | db.collection('todos', (errC, doc) => { 18 | if (errC) { 19 | reject(errC); 20 | } else { 21 | doc.find().toArray((ferr, docEntries) => { 22 | if (ferr) { 23 | reject(ferr); 24 | } else { 25 | const entry = _.find(docEntries, e => e.id === event.extensions.request.query.id); 26 | const newEntry = _.cloneDeep(entry); 27 | _.assign(newEntry, data, { id: uuid.v1(), updatedAt: new Date().getTime() }); 28 | doc.updateOne(entry, { $set: newEntry }, (uerr) => { 29 | if (uerr) { 30 | reject(uerr); 31 | } else { 32 | db.close(); 33 | resolve(JSON.stringify(newEntry)); 34 | } 35 | }); 36 | } 37 | }); 38 | } 39 | }); 40 | } 41 | }); 42 | }), 43 | }; 44 | -------------------------------------------------------------------------------- /examples/todo-app/frontend/react-redux/README.md: -------------------------------------------------------------------------------- 1 | # React-Redux frontend 2 | 3 | This is a frontend for our `todo` application which is implemented with the help of [React](http://reactjs.org) and [Redux](http://reduxjs.org). 4 | 5 | Do the following to setup and use the frontend 6 | 7 | 1. Make sure that you've deployed the backend of the `todo` application 8 | 2. Run `npm install` to install the used npm packages 9 | 3. Go to `app/js/actions/index.js` and update the `API_URL` with the endpoint of your deployed `todo` Serverless service (e.g. `http://192.168.99.100.nip.io`) 10 | * Note: You can find the application hostname executing `serverless info` in the backend folder and checking the field `URL` of any function. 11 | 4. Run `npm start` 12 | 5. Open up a browser on [localhost:8080](http://localhost:8080) and play around with the application 13 | -------------------------------------------------------------------------------- /examples/todo-app/frontend/react-redux/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Todos 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/todo-app/frontend/react-redux/app/js/actions/constants.js: -------------------------------------------------------------------------------- 1 | // error 2 | export const ERROR = 'ERROR'; 3 | export const RESET_ERROR = 'RESET_ERROR'; 4 | 5 | // todos 6 | export const CREATE_TODO = 'CREATE_TODO'; 7 | export const GET_TODOS = 'GET_TODOS'; 8 | export const GET_TODO = 'GET_TODO'; 9 | export const UPDATE_TODO = 'UPDATE_TODO'; 10 | export const DELETE_TODO = 'DELETE_TODO'; 11 | -------------------------------------------------------------------------------- /examples/todo-app/frontend/react-redux/app/js/actions/error.js: -------------------------------------------------------------------------------- 1 | import { RESET_ERROR } from './constants'; 2 | 3 | export function resetError() { 4 | return { 5 | type: RESET_ERROR 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/todo-app/frontend/react-redux/app/js/actions/index.js: -------------------------------------------------------------------------------- 1 | export const API_URL = 'http://backend'; 2 | console.log(process.env); 3 | 4 | if (!API_URL) { 5 | console.error('Set `API_URL` in `app/js/actions/index.js` to your deployed endpoint'); 6 | } 7 | -------------------------------------------------------------------------------- /examples/todo-app/frontend/react-redux/app/js/actions/todos.js: -------------------------------------------------------------------------------- 1 | import 'whatwg-fetch'; 2 | 3 | import { API_URL } from './index'; 4 | 5 | import { 6 | ERROR, 7 | CREATE_TODO, 8 | GET_TODOS, 9 | GET_TODO, 10 | UPDATE_TODO, 11 | DELETE_TODO, 12 | } from './constants'; 13 | 14 | export function createTodo(todo) { 15 | return (dispatch) => fetch(`${API_URL}/create`, { 16 | headers: { 17 | 'Content-Type': 'application/json' 18 | }, 19 | method: 'POST', 20 | body: JSON.stringify(todo), 21 | }) 22 | .then(response => response.json()) 23 | .then(json => dispatch({ 24 | type: CREATE_TODO, 25 | payload: json, 26 | })) 27 | .catch(exception => dispatch({ 28 | type: ERROR, 29 | payload: exception.message, 30 | })); 31 | } 32 | 33 | export function getTodos() { 34 | return (dispatch) => fetch(`${API_URL}/read-all`, { 35 | method: 'GET', 36 | }) 37 | .then(response => response.json()) 38 | .then(json => dispatch({ 39 | type: GET_TODOS, 40 | payload: json, 41 | })) 42 | .catch(exception => dispatch({ 43 | type: ERROR, 44 | payload: exception.message, 45 | })); 46 | } 47 | 48 | export function getTodo(todo) { 49 | return (dispatch) => fetch(`${API_URL}/read?id=${todo.id}`, { 50 | method: 'GET', 51 | }) 52 | .then(response => response.json()) 53 | .then(json => dispatch({ 54 | type: GET_TODO, 55 | payload: json, 56 | })) 57 | .catch(exception => dispatch({ 58 | type: ERROR, 59 | payload: exception.message, 60 | })); 61 | } 62 | 63 | export function updateTodo(todo) { 64 | return (dispatch) => fetch(`${API_URL}/update?id=${todo.id}`, { 65 | headers: { 66 | 'Content-Type': 'application/json' 67 | }, 68 | method: 'POST', 69 | body: JSON.stringify(todo), 70 | }) 71 | .then(response => response.json()) 72 | .then(json => dispatch({ 73 | type: UPDATE_TODO, 74 | payload: json, 75 | previousID: todo.id, 76 | })) 77 | .catch(exception => dispatch({ 78 | type: ERROR, 79 | payload: exception.message, 80 | })); 81 | } 82 | 83 | export function deleteTodo(id) { 84 | return (dispatch) => fetch(`${API_URL}/delete?id=${id}`, { 85 | method: 'GET', 86 | }) 87 | .then(response => response.json()) 88 | .then(json => dispatch({ 89 | type: DELETE_TODO, 90 | payload: json, 91 | })) 92 | .catch(exception => dispatch({ 93 | type: ERROR, 94 | payload: exception.message, 95 | })); 96 | } 97 | -------------------------------------------------------------------------------- /examples/todo-app/frontend/react-redux/app/js/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | 5 | import store from './store'; 6 | import App from './components/app'; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | 12 | , document.getElementById('root') 13 | ); 14 | -------------------------------------------------------------------------------- /examples/todo-app/frontend/react-redux/app/js/components/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Component } from 'react'; 3 | 4 | import Error from './shared/error'; 5 | import TodosIndex from './todos/index'; 6 | import TodosNew from './todos/new'; 7 | 8 | export default class App extends Component { 9 | render() { 10 | return ( 11 |
12 |
13 | 14 | 15 | 16 | 17 |
18 |
19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/todo-app/frontend/react-redux/app/js/components/shared/error.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { resetError } from '../../actions/error'; 4 | 5 | const styles = { 6 | backgroundColor: '#FC9D9A', 7 | padding: '1em', 8 | marginBottom: '1em' 9 | }; 10 | 11 | class Error extends Component { 12 | componentDidMount() { 13 | setTimeout(() => { 14 | this.props.resetError(); 15 | }, 5000); 16 | } 17 | 18 | handleDismissClick(event) { 19 | event.preventDefault(); 20 | this.props.resetError(); 21 | } 22 | 23 | render() { 24 | const { message } = this.props; 25 | 26 | if (!message.length) { 27 | return null; 28 | } 29 | 30 | return ( 31 |
32 |
33 | An error occurred: "{message}" 34 | Dismiss 35 |
36 |
37 | ); 38 | } 39 | } 40 | 41 | const mapStateToProps = ({ error }) => ({ message: error.message }); 42 | 43 | export default connect(mapStateToProps, { resetError })(Error); 44 | -------------------------------------------------------------------------------- /examples/todo-app/frontend/react-redux/app/js/components/todos/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import _ from 'lodash'; 4 | 5 | import { 6 | getTodos, 7 | updateTodo, 8 | deleteTodo 9 | } from '../../actions/todos'; 10 | 11 | const noDataAvailableStyles = { 12 | marginTop: '20px', 13 | textAlign: 'center' 14 | }; 15 | 16 | const deleteTodoStyles = { 17 | marginLeft: '5px' 18 | }; 19 | 20 | class TodosIndex extends Component { 21 | componentWillMount() { 22 | this.props.getTodos(); 23 | } 24 | 25 | deleteTodo(event) { 26 | event.preventDefault(); 27 | 28 | const id = event.currentTarget.getAttribute('data-todo-id'); 29 | 30 | if (confirm('Do you really want to delete this todo?')) { 31 | this.props.deleteTodo(id); 32 | } 33 | } 34 | 35 | updateTodo(event) { 36 | event.preventDefault(); 37 | 38 | const id = event.currentTarget.getAttribute('data-todo-id'); 39 | const body = event.currentTarget.innerText; 40 | 41 | const todo = { 42 | id, 43 | body 44 | } 45 | 46 | this.props.updateTodo(todo); 47 | } 48 | 49 | render() { 50 | const { todos } = this.props; 51 | 52 | const sortedTodos = todos.length ? _.orderBy(todos, 'updatedAt', ['desc']) : []; 53 | 54 | return ( 55 |
56 |
57 | {sortedTodos.length ? ( 58 |
    59 | { sortedTodos.map((todo) => { 60 | return ( 61 |
  • 62 | {todo.body} 63 | Delete 64 |
  • 65 | ) 66 | } 67 | )} 68 |
69 | ) :
There are currently no todos available to display
} 70 |
71 |
72 | ) 73 | } 74 | } 75 | 76 | function mapStateToProps(state) { 77 | return { todos: state.todos.todos }; 78 | } 79 | 80 | export default connect(mapStateToProps, { getTodos, updateTodo, deleteTodo })(TodosIndex); 81 | -------------------------------------------------------------------------------- /examples/todo-app/frontend/react-redux/app/js/components/todos/new.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { createTodo } from '../../actions/todos'; 5 | 6 | class TodosNew extends Component { 7 | createTodo(event) { 8 | event.preventDefault(); 9 | 10 | const body = this.refs.body.value; 11 | 12 | const todo = { 13 | body 14 | } 15 | 16 | this.props.createTodo(todo).then(this.refs.body.value = ''); 17 | } 18 | 19 | render() { 20 | return ( 21 |
22 |
23 |
24 | 25 | 26 |
27 |
28 |
29 | ); 30 | } 31 | } 32 | 33 | export default connect(null, { createTodo })(TodosNew); 34 | -------------------------------------------------------------------------------- /examples/todo-app/frontend/react-redux/app/js/reducers/error.js: -------------------------------------------------------------------------------- 1 | import { 2 | ERROR, 3 | RESET_ERROR 4 | } from '../actions/constants'; 5 | 6 | const INITIAL_STATE = { message: '' }; 7 | 8 | export default function(state = INITIAL_STATE, action) { 9 | switch(action.type) { 10 | case ERROR: 11 | return { ...state, message: action.payload }; 12 | case RESET_ERROR: 13 | return { ...state, message: '' }; 14 | default: 15 | return state; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/todo-app/frontend/react-redux/app/js/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import TodosReducer from './todos'; 4 | import ErrorReducer from './error'; 5 | 6 | export default combineReducers({ 7 | todos: TodosReducer, 8 | error: ErrorReducer 9 | }); 10 | -------------------------------------------------------------------------------- /examples/todo-app/frontend/react-redux/app/js/reducers/todos.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | import { 4 | CREATE_TODO, 5 | GET_TODOS, 6 | GET_TODO, 7 | UPDATE_TODO, 8 | DELETE_TODO 9 | } from '../actions/constants'; 10 | 11 | const INITIAL_STATE = { todos: [], todo: null }; 12 | 13 | export default function(state = INITIAL_STATE, action) { 14 | let todos; 15 | 16 | switch(action.type) { 17 | case CREATE_TODO: 18 | todos = state.todos.slice(); 19 | todos.unshift(action.payload); 20 | return { ...state, todos: todos }; 21 | case GET_TODOS: 22 | return { ...state, todos: action.payload }; 23 | case GET_TODO: 24 | return { ...state, todo: action.payload }; 25 | case UPDATE_TODO: 26 | todos = _.without(state.todos, 27 | _.find(state.todos, { id: action.previousID }) 28 | ); 29 | todos.unshift(action.payload); 30 | return { ...state, todos: todos }; 31 | case DELETE_TODO: 32 | todos = _.without(state.todos, 33 | _.find(state.todos, { id: action.payload.id }) 34 | ); 35 | return { ...state, todos: todos }; 36 | default: 37 | return state; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/todo-app/frontend/react-redux/app/js/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import createLogger from 'redux-logger'; 4 | 5 | import reducers from './reducers'; 6 | 7 | const devTools = window.devToolsExtension ? window.devToolsExtension() : f => f; 8 | 9 | const createStoreWithMiddleware = compose( 10 | applyMiddleware(thunk, createLogger()), 11 | devTools 12 | )(createStore); 13 | 14 | export default createStoreWithMiddleware(reducers); 15 | -------------------------------------------------------------------------------- /examples/todo-app/frontend/react-redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-book-todos-client", 3 | "version": "0.1.0", 4 | "description": "Serverless book todos client", 5 | "scripts": { 6 | "build": "webpack", 7 | "start": "webpack-dev-server --hot --inline" 8 | }, 9 | "keywords": [ 10 | "serverless", 11 | "serverless framework" 12 | ], 13 | "author": "", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "babel-core": "^6.14.0", 17 | "babel-loader": "^6.2.5", 18 | "babel-plugin-react-transform": "^2.0.2", 19 | "babel-plugin-syntax-class-properties": "^6.13.0", 20 | "babel-plugin-syntax-decorators": "^6.13.0", 21 | "babel-plugin-syntax-object-rest-spread": "^6.13.0", 22 | "babel-preset-es2015": "^6.14.0", 23 | "babel-preset-react": "^6.11.1", 24 | "babel-preset-stage-0": "^6.5.0", 25 | "file-loader": "^0.9.0", 26 | "react-hot-loader": "^3.0.0-beta.3", 27 | "webpack": "^1.13.2", 28 | "webpack-dev-server": "^1.15.1" 29 | }, 30 | "dependencies": { 31 | "babel-plugin-transform-class-properties": "^6.11.5", 32 | "babel-plugin-transform-object-rest-spread": "^6.8.0", 33 | "lodash": "^4.15.0", 34 | "react": "^15.3.1", 35 | "react-dom": "^15.3.1", 36 | "react-mixin": "^3.0.5", 37 | "react-redux": "^4.4.5", 38 | "redux": "^3.6.0", 39 | "redux-logger": "^2.6.1", 40 | "redux-thunk": "^2.1.0", 41 | "whatwg-fetch": "^1.0.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/todo-app/frontend/react-redux/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: { 3 | javascript: "./app/js/app.jsx", 4 | html: "./app/index.html" 5 | }, 6 | output: { 7 | path: __dirname + "/../dist", 8 | filename: "/js/app.js" 9 | }, 10 | resolve: { 11 | extensions: ["", ".webpack.js", ".web.js", ".js", ".jsx"] 12 | }, 13 | module: { 14 | loaders: [ 15 | { 16 | test: /\.html$/, 17 | loader: "file?name=[name].[ext]" 18 | }, 19 | { 20 | test: /\.jsx?$/, 21 | exclude: /node_modules/, 22 | loaders: ['babel?' + JSON.stringify( 23 | { 24 | presets: ['react', 'es2015'], 25 | "plugins": [ 26 | "syntax-class-properties", 27 | "syntax-decorators", 28 | "syntax-object-rest-spread", 29 | 30 | "transform-class-properties", 31 | "transform-object-rest-spread" 32 | ] 33 | } 34 | )] 35 | } 36 | ] 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /examples/todo-app/todos-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless/serverless-kubeless/336fb2ada1f324dc6782d411642ca1b43d90794e/examples/todo-app/todos-1.gif -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Bitnami. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | /* 20 | NOTE: this plugin is used to add all the different provider related plugins at once. 21 | This way only one plugin needs to be added to the service in order to get access to the 22 | whole provider implementation. 23 | */ 24 | 25 | const KubelessProvider = require('./provider/kubelessProvider'); 26 | const KubelessDeploy = require('./deploy/kubelessDeploy'); 27 | const KubelessDeployFunction = require('./deployFunction/kubelessDeployFunction'); 28 | const KubelessRemove = require('./remove/kubelessRemove'); 29 | const KubelessInvoke = require('./invoke/kubelessInvoke'); 30 | const KubelessInfo = require('./info/kubelessInfo'); 31 | const KubelessLogs = require('./logs/kubelessLogs'); 32 | 33 | class KubelessIndex { 34 | constructor(serverless, options) { 35 | this.serverless = serverless; 36 | this.options = options; 37 | 38 | this.serverless.pluginManager.addPlugin(KubelessProvider); 39 | this.serverless.pluginManager.addPlugin(KubelessDeploy); 40 | this.serverless.pluginManager.addPlugin(KubelessDeployFunction); 41 | this.serverless.pluginManager.addPlugin(KubelessRemove); 42 | this.serverless.pluginManager.addPlugin(KubelessInvoke); 43 | this.serverless.pluginManager.addPlugin(KubelessInfo); 44 | this.serverless.pluginManager.addPlugin(KubelessLogs); 45 | } 46 | } 47 | 48 | module.exports = KubelessIndex; 49 | -------------------------------------------------------------------------------- /info/kubelessInfo.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Bitnami. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const _ = require('lodash'); 20 | const BbPromise = require('bluebird'); 21 | const getInfo = require('../lib/get-info'); 22 | const helpers = require('../lib/helpers'); 23 | 24 | class KubelessInfo { 25 | constructor(serverless, options) { 26 | this.serverless = serverless; 27 | this.options = options || {}; 28 | this.provider = this.serverless.getProvider('kubeless'); 29 | this.commands = { 30 | info: { 31 | usage: 'Display information about the current functions', 32 | lifecycleEvents: [ 33 | 'info', 34 | ], 35 | options: { 36 | verbose: { 37 | usage: 'Display metadata', 38 | shortcut: 'v', 39 | }, 40 | }, 41 | }, 42 | }; 43 | this.hooks = { 44 | 'info:info': () => BbPromise.bind(this) 45 | .then(this.validate) 46 | .then(this.infoFunction), 47 | }; 48 | } 49 | 50 | validate() { 51 | const unsupportedOptions = ['stage', 'region']; 52 | helpers.warnUnsupportedOptions( 53 | unsupportedOptions, 54 | this.options, 55 | this.serverless.cli.log.bind(this.serverless.cli) 56 | ); 57 | return BbPromise.resolve(); 58 | } 59 | 60 | infoFunction(options) { 61 | return getInfo( 62 | this.serverless.service.functions, 63 | this.serverless.service.service, 64 | _.defaults({}, options, { 65 | namespace: this.serverless.service.provider.namespace, 66 | verbose: this.options.verbose, 67 | log: this.serverless.cli.consoleLog, 68 | }) 69 | ); 70 | } 71 | } 72 | 73 | module.exports = KubelessInfo; 74 | -------------------------------------------------------------------------------- /invoke/kubelessInvoke.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Bitnami. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const _ = require('lodash'); 20 | const BbPromise = require('bluebird'); 21 | const path = require('path'); 22 | const helpers = require('../lib/helpers'); 23 | const invoke = require('../lib/invoke'); 24 | 25 | class KubelessInvoke { 26 | constructor(serverless, options) { 27 | this.serverless = serverless; 28 | this.options = options || {}; 29 | this.provider = this.serverless.getProvider('kubeless'); 30 | 31 | this.hooks = { 32 | 'invoke:invoke': () => BbPromise.bind(this) 33 | .then(this.validate) 34 | .then(this.invokeFunction) 35 | .then(this.log), 36 | }; 37 | } 38 | 39 | validate() { 40 | const unsupportedOptions = ['stage', 'region', 'type']; 41 | helpers.warnUnsupportedOptions( 42 | unsupportedOptions, 43 | this.options, 44 | this.serverless.cli.log.bind(this.serverless.cli) 45 | ); 46 | if (_.isUndefined(this.serverless.service.functions[this.options.function])) { 47 | throw new Error( 48 | `The function ${this.options.function} is not present in the current description` 49 | ); 50 | } 51 | return BbPromise.resolve(); 52 | } 53 | 54 | invokeFunction(func, data) { 55 | const f = func || this.options.function; 56 | this.serverless.cli.log(`Calling function: ${f}...`); 57 | let dataPath = this.options.path; 58 | if (dataPath && !path.isAbsolute(dataPath)) { 59 | dataPath = path.join(this.serverless.config.servicePath, dataPath); 60 | } 61 | return invoke( 62 | f, 63 | data || this.options.data, 64 | _.map(this.serverless.service.functions, (desc, ff) => _.assign({}, desc, { id: ff })), 65 | { 66 | namespace: this.serverless.service.provider.namespace, 67 | path: dataPath, 68 | } 69 | ); 70 | } 71 | 72 | log(response) { 73 | if (this.options.log) { 74 | console.log('--------------------------------------------------------------------'); 75 | console.log(response.body); 76 | } 77 | return BbPromise.resolve(); 78 | } 79 | } 80 | 81 | module.exports = KubelessInvoke; 82 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Bitnami. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const _ = require('lodash'); 20 | const helpers = require('./helpers'); 21 | const request = require('request'); 22 | 23 | class Config { 24 | constructor(options) { 25 | const defaultNamespace = process.env.KUBELESS_NAMESPACE || 'kubeless'; 26 | const opts = _.defaults({}, options, { 27 | namespace: defaultNamespace, 28 | }); 29 | this.namespace = opts.namespace; 30 | const APIRootUrl = helpers.getKubernetesAPIURL(helpers.loadKubeConfig()); 31 | const url = `${APIRootUrl}/api/v1/namespaces/${opts.namespace}/configmaps/kubeless-config`; 32 | this.connectionOptions = Object.assign( 33 | helpers.getConnectionOptions(helpers.loadKubeConfig()), 34 | { url, json: true } 35 | ); 36 | this.configMag = {}; 37 | } 38 | init() { 39 | const data = []; 40 | return new Promise((resolve, reject) => { 41 | request.get(this.connectionOptions) 42 | .on('error', err => { 43 | reject(err); 44 | }) 45 | .on('data', (d) => { 46 | data.push(d); 47 | }) 48 | .on('end', () => { 49 | const res = JSON.parse(Buffer.concat(data).toString()); 50 | if (res.code && res.code !== 200) { 51 | reject(new Error( 52 | `Request returned: ${res.code} - ${res.message}` + 53 | `\n Response: ${JSON.stringify(res)}\n` + 54 | `${res.code === 401 && ' Check if your token has expired.'}` 55 | )); 56 | } else { 57 | this.configMag = res; 58 | resolve(); 59 | } 60 | }); 61 | }); 62 | } 63 | get(key, opt) { 64 | if (opt && opt.parse) { 65 | return JSON.parse(this.configMag.data[key]); 66 | } 67 | return this.configMag.data[key]; 68 | } 69 | } 70 | 71 | module.exports = Config; 72 | -------------------------------------------------------------------------------- /lib/crd.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Bitnami. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const _ = require('lodash'); 20 | const BbPromise = require('bluebird'); 21 | const helpers = require('./helpers'); 22 | const request = require('request'); 23 | 24 | class CRD { 25 | constructor(group, version, namespace, item) { 26 | this.namespace = namespace; 27 | const APIRootUrl = helpers.getKubernetesAPIURL(helpers.loadKubeConfig()); 28 | const fullUrl = `${APIRootUrl}/${group}/${version}/namespaces/${namespace}/${item}/`; 29 | this.connectionOptions = Object.assign( 30 | helpers.getConnectionOptions(helpers.loadKubeConfig()), 31 | { url: fullUrl, json: true } 32 | ); 33 | } 34 | getItem(id) { 35 | const data = []; 36 | return new BbPromise((resolve, reject) => { 37 | request.get(_.assign({}, this.connectionOptions, { 38 | url: `${this.connectionOptions.url}${id}`, 39 | })) 40 | .on('error', err => { 41 | reject(err); 42 | }) 43 | .on('data', (d) => { 44 | data.push(d); 45 | }) 46 | .on('end', () => { 47 | const res = Buffer.concat(data).toString(); 48 | resolve(JSON.parse(res)); 49 | }); 50 | }); 51 | } 52 | list() { 53 | const data = []; 54 | return new BbPromise((resolve, reject) => { 55 | request.get(this.connectionOptions) 56 | .on('error', err => { 57 | reject(err); 58 | }) 59 | .on('data', (d) => { 60 | data.push(d); 61 | }) 62 | .on('end', () => { 63 | const res = Buffer.concat(data).toString(); 64 | resolve(JSON.parse(res)); 65 | }); 66 | }); 67 | } 68 | post(body) { 69 | const data = []; 70 | return new Promise((resolve, reject) => { 71 | request.post(_.assign(body, this.connectionOptions)) 72 | .on('error', err => { 73 | reject(err); 74 | }) 75 | .on('data', (d) => { 76 | data.push(d); 77 | }) 78 | .on('end', () => { 79 | const res = Buffer.concat(data).toString(); 80 | resolve(JSON.parse(res)); 81 | }); 82 | }); 83 | } 84 | put(resourceID, body) { 85 | const data = []; 86 | return new BbPromise((resolve, reject) => { 87 | request.patch(_.assign({}, body, this.connectionOptions, { 88 | url: `${this.connectionOptions.url}${resourceID}`, 89 | headers: { 90 | 'Content-Type': 'application/merge-patch+json', 91 | }, 92 | })) 93 | .on('error', err => { 94 | reject(err); 95 | }) 96 | .on('data', (d) => { 97 | data.push(d); 98 | }) 99 | .on('end', () => { 100 | const res = Buffer.concat(data).toString(); 101 | resolve(JSON.parse(res)); 102 | }); 103 | }); 104 | } 105 | delete(resourceID) { 106 | const data = []; 107 | return new BbPromise((resolve, reject) => { 108 | request.delete(_.assign({}, this.connectionOptions, { 109 | url: `${this.connectionOptions.url}${resourceID}`, 110 | })) 111 | .on('error', err => { 112 | reject(err); 113 | }) 114 | .on('data', (d) => { 115 | data.push(d); 116 | }) 117 | .on('end', () => { 118 | const res = Buffer.concat(data).toString(); 119 | resolve(JSON.parse(res)); 120 | }); 121 | }); 122 | } 123 | } 124 | 125 | module.exports = CRD; 126 | -------------------------------------------------------------------------------- /lib/get-info.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Bitnami. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const _ = require('lodash'); 20 | const Api = require('kubernetes-client'); 21 | const BbPromise = require('bluebird'); 22 | const CRD = require('./crd'); 23 | const chalk = require('chalk'); 24 | const helpers = require('./helpers'); 25 | 26 | function toMultipleWords(word) { 27 | return word.replace(/([A-Z])/, ' $1').replace(/^./, (l) => l.toUpperCase()); 28 | } 29 | 30 | function formatMessage(service, f, options) { 31 | const opts = _.defaults({}, options, { 32 | color: false, 33 | verbose: false, 34 | }); 35 | if (!opts.color) chalk.enabled = false; 36 | let message = ''; 37 | message += `\n${chalk.yellow.underline(`Service Information "${service.name}"`)}\n`; 38 | message += `${chalk.yellow('Cluster IP: ')} ${service.ip}\n`; 39 | message += `${chalk.yellow('Type: ')} ${service.type}\n`; 40 | message += `${chalk.yellow('Ports: ')}\n`; 41 | _.each(service.ports, (port) => { 42 | // Ports can have variable properties 43 | _.each(port, (value, key) => { 44 | message += ` ${chalk.yellow(`${toMultipleWords(key)}: `)} ${value}\n`; 45 | }); 46 | }); 47 | if (opts.verbose) { 48 | message += `${chalk.yellow('Metadata')}\n`; 49 | message += ` ${chalk.yellow('Self Link: ')} ${service.selfLink}\n`; 50 | message += ` ${chalk.yellow('UID: ')} ${service.uid}\n`; 51 | message += ` ${chalk.yellow('Timestamp: ')} ${service.timestamp}\n`; 52 | } 53 | message += `${chalk.yellow.underline('Function Info')}\n`; 54 | if (f.url) { 55 | message += `${chalk.yellow('URL: ')} ${f.url}\n`; 56 | } 57 | if (f.annotations && f.annotations['kubeless.serverless.com/description']) { 58 | message += `${chalk.yellow('Description:')} ` + 59 | `${f.annotations['kubeless.serverless.com/description']}\n`; 60 | } 61 | if (f.labels) { 62 | message += `${chalk.yellow('Labels:\n')}`; 63 | _.each(f.labels, (v, k) => { 64 | message += `${chalk.yellow(` ${k}:`)} ${v}\n`; 65 | }); 66 | } 67 | message += `${chalk.yellow('Handler: ')} ${f.handler}\n`; 68 | message += `${chalk.yellow('Runtime: ')} ${f.runtime}\n`; 69 | message += `${chalk.yellow('Dependencies: ')} ${_.trim(f.deps)}\n`; 70 | if (opts.verbose) { 71 | message += `${chalk.yellow('Metadata:')}\n`; 72 | message += ` ${chalk.yellow('Self Link: ')} ${f.selfLink}\n`; 73 | message += ` ${chalk.yellow('UID: ')} ${f.uid}\n`; 74 | message += ` ${chalk.yellow('Timestamp: ')} ${f.timestamp}\n`; 75 | } 76 | return message; 77 | } 78 | 79 | function info(functions, service, options) { 80 | const opts = _.defaults({}, options, { 81 | namespace: 'default', 82 | verbose: false, 83 | log: console.log, 84 | color: true, 85 | }); 86 | let counter = 0; 87 | let message = ''; 88 | return new BbPromise((resolve, reject) => { 89 | _.each(functions, (desc, f) => { 90 | const namespace = desc.namespace || opts.namespace; 91 | const connectionOptions = helpers.getConnectionOptions(helpers.loadKubeConfig(), { 92 | namespace, 93 | }); 94 | const core = new Api.Core(connectionOptions); 95 | const functionsApi = new CRD('apis/kubeless.io', 'v1beta1', namespace, 'functions'); 96 | const httpTriggerApi = new CRD('apis/kubeless.io', 'v1beta1', namespace, 'httptriggers'); 97 | core.ns.services.get((err, servicesInfo) => { 98 | if (err) reject(new Error(err)); 99 | functionsApi.getItem(f).catch((ferr) => reject(ferr)).then(fDesc => { 100 | let tErr; 101 | let httpTriggerDesc; 102 | httpTriggerApi.getItem(f).catch((ex) => { 103 | tErr = ex; 104 | }).then(res => { 105 | if (res && res.kind === 'Status') { 106 | tErr = res; 107 | } else { 108 | httpTriggerDesc = res; 109 | } 110 | }).finally(() => { 111 | // eslint-disable-next-line max-len 112 | if (tErr && (tErr.code && tErr.code !== 404)) { 113 | reject(new Error(tErr)); 114 | } 115 | const functionService = _.find( 116 | servicesInfo.items, 117 | (s) => ( 118 | s.metadata.labels && 119 | s.metadata.labels.function === f 120 | ) 121 | ); 122 | if (_.isEmpty(functionService) || _.isEmpty(fDesc)) { 123 | opts.log(`Not found any information about the function "${f}"`); 124 | } else { 125 | let url = null; 126 | if (httpTriggerDesc) { 127 | // eslint-disable-next-line max-len 128 | url = `${httpTriggerDesc.spec['host-name'] || 'API_URL'}/${httpTriggerDesc.spec.path}`; 129 | } 130 | const fService = { 131 | name: functionService.metadata.name, 132 | ip: functionService.spec.clusterIP, 133 | type: functionService.spec.type, 134 | ports: functionService.spec.ports, 135 | selfLink: functionService.metadata.selfLink, 136 | uid: functionService.metadata.uid, 137 | timestamp: functionService.metadata.creationTimestamp, 138 | }; 139 | const func = { 140 | name: f, 141 | url, 142 | handler: fDesc.spec.handler, 143 | runtime: fDesc.spec.runtime, 144 | topic: fDesc.spec.topic, 145 | type: fDesc.spec.type, 146 | deps: fDesc.spec.deps, 147 | annotations: fDesc.metadata.annotations, 148 | labels: fDesc.metadata.labels, 149 | selfLink: fDesc.metadata.selfLink, 150 | uid: fDesc.metadata.uid, 151 | timestamp: fDesc.metadata.creationTimestamp, 152 | }; 153 | message += formatMessage( 154 | fService, 155 | func, 156 | _.defaults({}, opts, { color: opts.color }), 157 | { verbose: opts.verbose } 158 | ); 159 | } 160 | counter++; 161 | if (counter === _.keys(functions).length) { 162 | if (!_.isEmpty(message)) { 163 | opts.log(message); 164 | } 165 | resolve(message); 166 | } 167 | }); 168 | }); 169 | }); 170 | }); 171 | }); 172 | } 173 | 174 | module.exports = info; 175 | -------------------------------------------------------------------------------- /lib/get-logs.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Bitnami. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const _ = require('lodash'); 20 | const Api = require('kubernetes-client'); 21 | const BbPromise = require('bluebird'); 22 | const helpers = require('../lib/helpers'); 23 | const moment = require('moment'); 24 | const request = require('request'); 25 | 26 | function filterLogs(logs, options) { 27 | const opts = _.defaults({}, options, { 28 | startTime: null, 29 | count: null, 30 | filter: null, 31 | }); 32 | let logEntries = _.compact(logs.split('\n')); 33 | if (opts.count) { 34 | logEntries = logEntries.slice(logEntries.length - opts.count); 35 | } 36 | if (opts.filter) { 37 | logEntries = _.filter(logEntries, entry => !!entry.match(opts.filter)); 38 | } 39 | if (opts.startTime) { 40 | const since = !!opts.startTime.toString().match(/(?:m|h|d)/); 41 | let startMoment = null; 42 | if (since) { 43 | startMoment = moment().subtract( 44 | opts.startTime.replace(/\D/g, ''), 45 | opts.startTime.replace(/\d/g, '') 46 | ).valueOf(); 47 | } else { 48 | startMoment = moment(opts.startTime).valueOf(); 49 | } 50 | const logIndex = _.findIndex(logEntries, (entry) => { 51 | const entryDate = entry.match( 52 | /(\d{2}\/[a-zA-Z]{3}\/\d{4}:\d{2}:\d{2}:\d{2} \+\d{4}|-\d{4})/ 53 | ); 54 | if (entryDate) { 55 | const entryMoment = moment(entryDate[1], 'DD/MMM/YYYY:HH:mm:ss Z').valueOf(); 56 | return entryMoment >= startMoment; 57 | } 58 | return false; 59 | }); 60 | if (logIndex > -1) { 61 | logEntries = logEntries.slice(logIndex); 62 | } else { 63 | // There is no entry after the given startTime 64 | logEntries = []; 65 | } 66 | } 67 | return logEntries.join('\n'); 68 | } 69 | 70 | function printFilteredLogs(logs, opts) { 71 | const filteredLogs = filterLogs(logs, opts); 72 | if (!_.isEmpty(filteredLogs)) { 73 | if (!opts.silent) { 74 | console.log(filteredLogs); 75 | } 76 | } 77 | return filteredLogs; 78 | } 79 | 80 | function printLogs(func, options) { 81 | const config = helpers.loadKubeConfig(); 82 | const opts = _.defaults({}, options, { 83 | namespace: options.namespace || helpers.getDefaultNamespace(config), 84 | startTime: null, 85 | count: null, 86 | filter: null, 87 | silent: false, 88 | tail: false, 89 | onData: (d) => { 90 | const logs = d.toString().trim() || ''; 91 | return printFilteredLogs(logs, opts); 92 | }, 93 | }); 94 | const core = new Api.Core(helpers.getConnectionOptions(config, { namespace: opts.namespace })); 95 | return new BbPromise((resolve, reject) => { 96 | core.ns.pods.get((err, podsInfo) => { 97 | if (err) reject(new Error(err)); 98 | const functionPods = _.filter( 99 | podsInfo.items, 100 | (podInfo) => ( 101 | !_.isEmpty(podInfo.metadata.labels) && 102 | podInfo.metadata.labels.function === func 103 | ) 104 | ); 105 | if (_.isEmpty(functionPods)) { 106 | reject( 107 | new Error(`Unable to find the pod for the function ${func}. ` + 108 | 'Please ensure that there is a function deployed with that ID') 109 | ); 110 | } else { 111 | _.each(functionPods, functionPod => { 112 | if (opts.tail) { 113 | const APIRootUrl = helpers.getKubernetesAPIURL(helpers.loadKubeConfig()); 114 | const url = `${APIRootUrl}/api/v1/namespaces/${opts.namespace}/pods/` + 115 | `${functionPod.metadata.name}/log?follow=true`; 116 | const connectionOptions = Object.assign( 117 | helpers.getConnectionOptions(helpers.loadKubeConfig()), 118 | { url } 119 | ); 120 | request.get( 121 | connectionOptions 122 | ).on('data', opts.onData); 123 | } else { 124 | core.ns.pods(functionPod.metadata.name).log.get((errLog, logs) => { 125 | if (errLog) reject(new Error(errLog)); 126 | const filteredLogs = printFilteredLogs(logs || '', opts); 127 | resolve(filteredLogs); 128 | }); 129 | } 130 | }); 131 | } 132 | }); 133 | }); 134 | } 135 | 136 | module.exports = printLogs; 137 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Bitnami. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const _ = require('lodash'); 20 | const fs = require('fs'); 21 | const moment = require('moment'); 22 | const path = require('path'); 23 | const yaml = require('js-yaml'); 24 | const proc = require('child_process'); 25 | 26 | function loadKubeConfig() { 27 | const kubeCfgPath = path.join(process.env.HOME, '.kube/config'); 28 | let config = {}; 29 | if (process.env.KUBECONFIG) { 30 | // KUBECONFIG paths list is semicolon delimited for Windows 31 | // and colon delimited for Mac and Linux 32 | let kubeConfigDelimiter; 33 | if (process.platform === 'win32') { 34 | kubeConfigDelimiter = ';'; 35 | } else { 36 | kubeConfigDelimiter = ':'; 37 | } 38 | const configFiles = process.env.KUBECONFIG.split(kubeConfigDelimiter); 39 | _.each(configFiles, configFile => { 40 | _.defaults(config, yaml.safeLoad(fs.readFileSync(configFile))); 41 | }); 42 | } else if (!fs.existsSync(kubeCfgPath)) { 43 | throw new Error( 44 | 'Unable to locate the configuration file for your cluster. ' + 45 | 'Make sure you have your cluster configured locally' 46 | ); 47 | } else { 48 | config = yaml.safeLoad(fs.readFileSync(kubeCfgPath)); 49 | } 50 | return config; 51 | } 52 | 53 | function getContextName(config) { 54 | return process.env.KUBECONTEXT || config['current-context']; 55 | } 56 | 57 | function getContextInfo(config, context) { 58 | const contextInfo = _.find(config.contexts, c => c.name === context); 59 | if (!contextInfo) { 60 | throw new Error(`Unable to find configuration of context ${context}`); 61 | } 62 | return contextInfo.context; 63 | } 64 | 65 | function getClusterInfo(config, context) { 66 | const clusterName = getContextInfo(config, context).cluster; 67 | const clusterInfo = _.find(config.clusters, c => c.name === clusterName); 68 | if (!clusterInfo) { 69 | throw new Error(`Unable to find cluster information for context ${context}`); 70 | } 71 | return clusterInfo; 72 | } 73 | 74 | function getUserInfo(config, context) { 75 | const userName = getContextInfo(config, context).user; 76 | const userInfo = _.find(config.users, u => u.name === userName); 77 | if (!userInfo) { 78 | throw new Error(`Unable to find user information for context ${context}`); 79 | } 80 | return userInfo; 81 | } 82 | 83 | function getKubernetesAPIURL(config) { 84 | const currentContext = getContextName(config); 85 | const clusterInfo = getClusterInfo(config, currentContext); 86 | // Remove trailing '/' of the URL in case it exists 87 | let clusterURL = clusterInfo.cluster.server.replace(/\/$/, ''); 88 | // Add protocol if missing 89 | clusterURL = _.startsWith(clusterURL, 'http') ? clusterURL : `http://${clusterURL}`; 90 | return clusterURL; 91 | } 92 | 93 | function getPropertyText(property, info) { 94 | // Data could be pointing to a file or be base64 encoded 95 | let result = null; 96 | if (!_.isEmpty(info[property])) { 97 | result = fs.readFileSync(info[property]); 98 | } else if (!_.isEmpty(info[`${property}-data`])) { 99 | result = Buffer.from(info[`${property}-data`], 'base64'); 100 | } 101 | return result; 102 | } 103 | 104 | function getToken(userInfo) { 105 | const token = _.get(userInfo, 'user.token') || 106 | _.get(userInfo, 'user.auth-provider.config.id-token'); 107 | const accessToken = _.get(userInfo, 'user.auth-provider.config.access-token'); 108 | let cmd = _.get(userInfo, 'user.exec.command'); 109 | const awsClis = ['aws', 'aws-iam-authenticator']; 110 | if (token) { 111 | return token; 112 | } else if (accessToken) { 113 | // Access tokens may expire so we better check the expire date 114 | if (userInfo.user['auth-provider'].config.expiry) { 115 | const expiry = moment(userInfo.user['auth-provider'].config.expiry); 116 | if (expiry < moment()) { 117 | throw new Error( 118 | 'The access token has expired. Make sure you can access your cluster and try again' 119 | ); 120 | } 121 | } 122 | return accessToken; 123 | } else if (cmd && awsClis.includes(cmd) !== -1) { 124 | const args = _.get(userInfo, 'user.exec.args'); 125 | if (args) { 126 | cmd = `${cmd} ${args.join(' ')}`; 127 | } 128 | const env = _.get(userInfo, 'user.exec.env', []); 129 | const envvars = Object.assign({}, process.env); 130 | if (env) { 131 | for (const envvar of env) { 132 | envvars[envvar.name] = envvar.value || ''; 133 | } 134 | } 135 | let output = {}; 136 | try { 137 | output = proc.execSync(cmd, { env: envvars }); 138 | } catch (err) { 139 | throw new Error(`Failed to refresh token: ${err.message}`); 140 | } 141 | const resultObj = JSON.parse(output); 142 | const execToken = _.get(resultObj, 'status.token'); 143 | if (execToken) { 144 | return execToken; 145 | } 146 | } 147 | return null; 148 | } 149 | 150 | function getDefaultNamespace(config) { 151 | const currentContext = getContextName(config); 152 | return getContextInfo(config, currentContext).namespace || 'default'; 153 | } 154 | 155 | function getConnectionOptions(config, modif) { 156 | const currentContext = getContextName(config); 157 | const userInfo = getUserInfo(config, currentContext); 158 | const clusterInfo = getClusterInfo(config, currentContext); 159 | 160 | const connectionOptions = { 161 | group: 'k8s.io', 162 | url: getKubernetesAPIURL(config), 163 | namespace: getDefaultNamespace(config), 164 | }; 165 | // Config certificate-authority 166 | const ca = getPropertyText('certificate-authority', clusterInfo.cluster); 167 | if (ca) { 168 | connectionOptions.ca = ca; 169 | } else { 170 | // No certificate-authority found 171 | connectionOptions.insecureSkipTlsVerify = true; 172 | connectionOptions.strictSSL = false; 173 | } 174 | // Config authentication 175 | const token = getToken(userInfo); 176 | if (token) { 177 | connectionOptions.auth = { 178 | bearer: token, 179 | }; 180 | } else { 181 | // If there is not a valid token we can authenticate either using 182 | // username and password or a certificate and a key 183 | const user = _.get(userInfo, 'user.username'); 184 | const password = _.get(userInfo, 'user.password'); 185 | if (!_.isEmpty(user) && !_.isEmpty(password)) { 186 | connectionOptions.auth = { user, password }; 187 | } else { 188 | const properties = { 189 | cert: 'client-certificate', 190 | key: 'client-key', 191 | }; 192 | _.each(properties, (property, key) => { 193 | connectionOptions[key] = getPropertyText(property, userInfo.user); 194 | if (!connectionOptions[key]) { 195 | console.log( 196 | 'Unable to find required information for authenticating against the cluster' 197 | ); 198 | } 199 | }); 200 | } 201 | } 202 | return _.defaults({}, modif, connectionOptions); 203 | } 204 | 205 | function warnUnsupportedOptions(unsupportedOptions, definedOptions, logFunction) { 206 | unsupportedOptions.forEach((opt) => { 207 | if (!_.isUndefined(definedOptions[opt])) { 208 | logFunction(`Warning: Option ${opt} is not supported for the kubeless plugin`); 209 | } 210 | }); 211 | } 212 | 213 | function getRuntimeDepfile(runtime, configMap) { 214 | const runtimesInfo = configMap.get('runtime-images', { parse: true }); 215 | let depFile = null; 216 | _.each(runtimesInfo, r => { 217 | if (runtime.match(r.ID)) { 218 | depFile = r.depName; 219 | } 220 | }); 221 | return depFile; 222 | } 223 | 224 | function checkFinished(counter, max, errors, resolve, reject, options) { 225 | const opts = _.defaults({}, options, { 226 | onSuccess: () => new Promise(r => r()), 227 | }); 228 | if (counter === max) { 229 | if (_.isEmpty(errors)) { 230 | opts.onSuccess().then(resolve); 231 | } else { 232 | reject(new Error( 233 | 'Found errors while processing the given functions:\n' + 234 | `${errors.join('\n')}` 235 | )); 236 | } 237 | } 238 | } 239 | 240 | function getDeployableItemsNumber(functions) { 241 | return _.sum([_.keys(functions).length].concat(_.map(functions, f => _.size(f.events)))); 242 | } 243 | 244 | function setExponentialInterval(targetFunction, initialDelay, maxDelay) { 245 | let delay = initialDelay; 246 | let timer; 247 | const timerWrapper = () => { 248 | try { 249 | targetFunction(); 250 | } catch (ex) { 251 | console.error(ex); 252 | } 253 | if (timer) { 254 | delay = Math.round(delay * 1.2); 255 | if (delay > maxDelay) { 256 | delay = maxDelay; 257 | } 258 | timer = setTimeout(timerWrapper, delay); 259 | } 260 | }; 261 | timer = setTimeout(timerWrapper, delay); 262 | return { 263 | clearInterval: () => { 264 | clearTimeout(timer); 265 | timer = null; 266 | }, 267 | }; 268 | } 269 | 270 | module.exports = { 271 | warnUnsupportedOptions, 272 | loadKubeConfig, 273 | getKubernetesAPIURL, 274 | getDefaultNamespace, 275 | getConnectionOptions, 276 | getRuntimeDepfile, 277 | checkFinished, 278 | getDeployableItemsNumber, 279 | setExponentialInterval, 280 | }; 281 | -------------------------------------------------------------------------------- /lib/invoke.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Bitnami. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const _ = require('lodash'); 20 | const Api = require('kubernetes-client'); 21 | const BbPromise = require('bluebird'); 22 | const crypto = require('crypto'); 23 | const fs = require('fs'); 24 | const path = require('path'); 25 | const request = require('request'); 26 | const helpers = require('../lib/helpers'); 27 | 28 | function getData(data, options) { 29 | const opts = _.defaults({}, options, { 30 | path: null, 31 | }); 32 | let result = null; 33 | try { 34 | if (!_.isEmpty(data)) { 35 | if (_.isPlainObject(data)) { 36 | result = data; 37 | } else { 38 | try { 39 | // Try to parse data as JSON 40 | JSON.parse(data); 41 | result = { 42 | body: data, 43 | json: true, 44 | }; 45 | } catch (e) { 46 | // Assume data is a string 47 | result = { 48 | body: data, 49 | }; 50 | } 51 | } 52 | } else if (opts.path) { 53 | if (!path.isAbsolute(opts.path)) { 54 | throw new Error('Data path should be absolute'); 55 | } 56 | if (!fs.existsSync(opts.path)) { 57 | throw new Error('The file you provided does not exist.'); 58 | } 59 | result = { 60 | body: fs.readFileSync(opts.path, 'utf-8'), 61 | json: true, 62 | }; 63 | } 64 | } catch (e) { 65 | throw new Error( 66 | `Unable to parse data given in the arguments: \n${e.message}` 67 | ); 68 | } 69 | return result; 70 | } 71 | 72 | function invoke(func, data, funcsDesc, options) { 73 | const opts = _.defaults({}, options, { 74 | namespace: null, 75 | path: null, 76 | }); 77 | const config = helpers.loadKubeConfig(); 78 | const APIRootUrl = helpers.getKubernetesAPIURL(config); 79 | const desc = _.find(funcsDesc, d => d.id === func); 80 | const namespace = desc.namespace || 81 | opts.namespace || 82 | helpers.getDefaultNamespace(config); 83 | const connectionOptions = helpers.getConnectionOptions(helpers.loadKubeConfig(), { namespace }); 84 | const core = new Api.Core(connectionOptions); 85 | const requestData = getData(data, { 86 | path: opts.path, 87 | }); 88 | if (desc.sequence) { 89 | let promise = null; 90 | _.each(desc.sequence.slice(), sequenceFunction => { 91 | if (promise) { 92 | promise = promise.then( 93 | result => invoke(sequenceFunction, result.body, funcsDesc, opts) 94 | ); 95 | } else { 96 | promise = invoke(sequenceFunction, requestData, funcsDesc, opts); 97 | } 98 | }); 99 | return new BbPromise((resolve, reject) => promise.then( 100 | response => resolve(response), 101 | err => reject(err) 102 | )); 103 | } 104 | return new BbPromise((resolve, reject) => { 105 | const parseReponse = (err, response) => { 106 | if (err) { 107 | reject(new Error(err.message)); 108 | } else { 109 | if (response.statusCode !== 200) { 110 | reject(new Error(response.statusMessage)); 111 | } 112 | resolve(response); 113 | } 114 | }; 115 | core.ns.services.get((err, servicesInfo) => { 116 | if (err) { 117 | reject(err); 118 | } else { 119 | const functionService = _.find( 120 | servicesInfo.items, 121 | (service) => ( 122 | service.metadata.labels && 123 | service.metadata.labels.function === func 124 | ) 125 | ); 126 | if (_.isEmpty(functionService)) { 127 | opts.log(`Not found any information about the function "${func}"`); 128 | } 129 | const port = functionService.spec.ports[0].name || functionService.spec.ports[0].port; 130 | const url = `${APIRootUrl}/api/v1/namespaces/${namespace}/services/${func}:${port}/proxy/`; 131 | const invokeConnectionOptions = Object.assign( 132 | connectionOptions, { 133 | url, 134 | headers: { 135 | 'Content-Type': 'application/x-www-form-urlencoded', 136 | 'event-id': `sls-cli-${crypto.randomBytes(6).toString('hex')}`, 137 | 'event-time': new Date().toISOString(), 138 | 'event-type': 'application/x-www-form-urlencoded', 139 | 'event-namespace': 'serverless.kubeless.io', 140 | }, 141 | } 142 | ); 143 | if (_.isEmpty(requestData)) { 144 | // There is no data to send, sending a GET request 145 | request.get(invokeConnectionOptions, parseReponse); 146 | } else { 147 | // Sending request data with a POST 148 | request.post( 149 | Object.assign( 150 | invokeConnectionOptions, 151 | requestData 152 | ), 153 | parseReponse 154 | ); 155 | } 156 | } 157 | }); 158 | }); 159 | } 160 | 161 | module.exports = invoke; 162 | -------------------------------------------------------------------------------- /lib/remove.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Bitnami. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const _ = require('lodash'); 20 | const BbPromise = require('bluebird'); 21 | const CRD = require('./crd'); 22 | const helpers = require('./helpers'); 23 | 24 | function apiDeleteTrigger(triggerName, namespace, triggerType) { 25 | const triggerApi = new CRD('apis/kubeless.io', 'v1beta1', namespace, triggerType); 26 | return triggerApi.delete(triggerName); 27 | } 28 | 29 | function removeTrigger(event, funcName, namespace, service, options) { 30 | let triggerPromise; 31 | switch (_.keys(event)[0]) { 32 | case 'http': 33 | if (_.isEmpty(event.http)) { 34 | throw new Error('You should specify a path for the trigger event'); 35 | } 36 | options.log(`Deleting http trigger for ${funcName}`); 37 | triggerPromise = apiDeleteTrigger( 38 | funcName, 39 | namespace, 40 | 'httptriggers' 41 | ); 42 | break; 43 | case 'trigger': { 44 | if (_.isEmpty(event.trigger)) { 45 | throw new Error('You should specify a topic for the trigger event'); 46 | } 47 | options.log(`Deleting PubSub trigger ${funcName}-${event.trigger}`); 48 | 49 | // Defaults to Kafka 50 | let mqType = 'kafka'; 51 | if (typeof event.trigger !== 'string') { 52 | if (_.isEmpty(event.trigger.queue)) { 53 | throw new Error( 54 | 'You should specify a message queue type for the trigger event (i.e. kafka, nats)' 55 | ); 56 | } 57 | mqType = event.trigger.queue; 58 | } 59 | 60 | triggerPromise = apiDeleteTrigger( 61 | _.kebabCase(`${funcName}-${event.trigger}`), 62 | namespace, 63 | `${mqType}triggers` 64 | ); 65 | break; 66 | } 67 | case 'schedule': 68 | if (_.isEmpty(event.schedule)) { 69 | throw new Error('You should specify a schedule for the trigger event'); 70 | } 71 | options.log(`Deleting scheduled trigger for ${funcName}`); 72 | triggerPromise = apiDeleteTrigger( 73 | funcName, 74 | namespace, 75 | 'cronjobtriggers' 76 | ); 77 | break; 78 | default: 79 | throw new Error(`Event type ${event.type} is not supported`); 80 | } 81 | return triggerPromise; 82 | } 83 | 84 | 85 | function checkResult(res, name, errors, options) { 86 | if (res && res.code && res.code !== 200) { 87 | if (res.code === 404) { 88 | if (options.verbose) { 89 | options.log(`The element ${name} doesn't exist. Skipping removal.`); 90 | } 91 | } else { 92 | errors.push( 93 | `Unable to remove ${name}. Received:\n` + 94 | ` Code: ${res.code}\n` + 95 | ` Message: ${res.message}`); 96 | } 97 | } 98 | } 99 | 100 | function removeFunction(functions, service, options) { 101 | const opts = _.defaults({}, options, { 102 | namespace: 'default', 103 | verbose: false, 104 | log: console.log, 105 | apiExtensions: null, 106 | }); 107 | const errors = []; 108 | let counter = 0; 109 | // Total number of elements to delete 110 | const elements = helpers.getDeployableItemsNumber(functions); 111 | return new BbPromise((resolve, reject) => { 112 | _.each(functions, (desc) => { 113 | opts.log(`Removing function: ${desc.id}...`); 114 | const namespace = desc.namespace || opts.namespace; 115 | const functionsApi = new CRD('apis/kubeless.io', 'v1beta1', namespace, 'functions'); 116 | // Delete function 117 | functionsApi.delete(desc.id).catch((err) => { 118 | errors.push( 119 | `Unable to remove the function ${desc.id}. Received:\n` + 120 | ` Code: ${err.code}\n` + 121 | ` Message: ${err.message}` 122 | ); 123 | }).then(res => { 124 | checkResult(res, desc.id, errors, { log: opts.log, verbose: opts.verbose }); 125 | counter++; 126 | helpers.checkFinished(counter, elements, errors, resolve, reject); 127 | }); 128 | _.each(desc.events, event => { 129 | removeTrigger(event, desc.id, namespace, service, opts).catch(err => { 130 | checkResult(err, desc.id, errors, { log: opts.log, verbose: opts.verbose }); 131 | }).then((res) => { 132 | checkResult(res, desc.id, errors, { log: opts.log, verbose: opts.verbose }); 133 | counter++; 134 | helpers.checkFinished(counter, elements, errors, resolve, reject); 135 | }); 136 | }); 137 | }); 138 | }); 139 | } 140 | 141 | module.exports = removeFunction; 142 | -------------------------------------------------------------------------------- /lib/strategy.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Bitnami. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const _ = require('lodash'); 20 | 21 | const Base64ZipContent = require('./strategy/base64_zip_content'); 22 | const S3ZipContent = require('./strategy/s3_zip_content'); 23 | 24 | const strategies = { 25 | Base64ZipContent, 26 | S3ZipContent, 27 | }; 28 | 29 | class KubelessDeployStrategy { 30 | constructor(serverless) { 31 | this.serverless = serverless; 32 | } 33 | 34 | factory() { 35 | const deploy = _.defaults({}, this.serverless.service.provider.deploy, { 36 | strategy: 'Base64ZipContent', 37 | options: {}, 38 | }); 39 | 40 | if (deploy.strategy in strategies) { 41 | return new strategies[deploy.strategy](this, deploy.options); 42 | } 43 | 44 | throw new Error(`Unknown deploy strategy "${deploy.strategy}"`); 45 | } 46 | } 47 | 48 | module.exports = KubelessDeployStrategy; 49 | -------------------------------------------------------------------------------- /lib/strategy/base64_zip_content.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Bitnami. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const crypto = require('crypto'); 20 | const fs = require('fs'); 21 | const BbPromise = require('bluebird'); 22 | 23 | class Base64ZipContent { 24 | constructor(strategy, options) { 25 | this.strategy = strategy; 26 | this.options = options; 27 | } 28 | 29 | deploy(description, artifact) { 30 | return new BbPromise((resolve) => { 31 | const shasum = crypto.createHash('sha256'); 32 | const content = fs.readFileSync(artifact); 33 | 34 | shasum.update(content); 35 | 36 | resolve({ 37 | content: content.toString('base64'), 38 | checksum: `sha256:${shasum.digest('hex')}`, 39 | contentType: 'base64+zip', 40 | }); 41 | }); 42 | } 43 | } 44 | 45 | module.exports = Base64ZipContent; 46 | -------------------------------------------------------------------------------- /lib/strategy/s3_zip_content.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Bitnami. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const _ = require('lodash'); 20 | const crypto = require('crypto'); 21 | const fs = require('fs'); 22 | const AWS = require('aws-sdk'); 23 | const BbPromise = require('bluebird'); 24 | 25 | class S3ZipContent { 26 | constructor(strategy, options) { 27 | this.strategy = strategy; 28 | this.options = options; 29 | } 30 | 31 | deploy(description, artifact) { 32 | const options = _.defaults({}, this.options, { 33 | expires: 60 * 60 * 24 * 365, 34 | }); 35 | return new BbPromise((resolve, reject) => { 36 | const shasum = crypto.createHash('sha256'); 37 | const content = fs.readFileSync(artifact); 38 | shasum.update(content); 39 | 40 | AWS.config.update(options, true); 41 | const s3 = new AWS.S3(); 42 | const Key = `${description.name}-${+(new Date())}.zip`; 43 | 44 | this.strategy.serverless.cli.log(`Uploading function ${description.name} as ${Key}`); 45 | 46 | return s3.putObject({ 47 | Key, 48 | Bucket: options.bucket, 49 | Body: fs.readFileSync(artifact), 50 | }).promise() 51 | .then(() => { 52 | const url = s3.getSignedUrl('getObject', { 53 | Key, 54 | Bucket: options.bucket, 55 | Expires: options.expires, 56 | }); 57 | 58 | resolve({ 59 | content: url, 60 | checksum: `sha256:${shasum.digest('hex')}`, 61 | contentType: 'url+zip', 62 | }); 63 | }, reject); 64 | }); 65 | } 66 | } 67 | 68 | module.exports = S3ZipContent; 69 | -------------------------------------------------------------------------------- /logs/kubelessLogs.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Bitnami. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const _ = require('lodash'); 20 | const BbPromise = require('bluebird'); 21 | const getLogs = require('../lib/get-logs'); 22 | const helpers = require('../lib/helpers'); 23 | 24 | class KubelessLogs { 25 | constructor(serverless, options) { 26 | this.serverless = serverless; 27 | this.options = options || {}; 28 | this.provider = this.serverless.getProvider('kubeless'); 29 | this.commands = { 30 | logs: { 31 | usage: 'Output the logs of a deployed function', 32 | lifecycleEvents: [ 33 | 'logs', 34 | ], 35 | options: { 36 | count: { 37 | usage: 'Number of lines to print', 38 | shortcut: 'n', 39 | }, 40 | }, 41 | }, 42 | }; 43 | this.hooks = { 44 | 'logs:logs': () => BbPromise.bind(this) 45 | .then(this.validate) 46 | .then(this.printLogs), 47 | }; 48 | } 49 | 50 | validate() { 51 | const unsupportedOptions = ['stage', 'region', 'interval']; 52 | helpers.warnUnsupportedOptions( 53 | unsupportedOptions, 54 | this.options, 55 | this.serverless.cli.log.bind(this.serverless.cli) 56 | ); 57 | if (_.isUndefined(this.serverless.service.functions[this.options.function])) { 58 | throw new Error( 59 | `The function ${this.options.function} is not present in the current description` 60 | ); 61 | } 62 | return BbPromise.resolve(); 63 | } 64 | 65 | printLogs(options) { 66 | const opts = _.defaults({}, options, { 67 | startTime: this.options.startTime, 68 | count: this.options.count, 69 | filter: this.options.filter, 70 | silent: false, 71 | tail: this.options.tail, 72 | namespace: this.serverless.service.functions[this.options.function].namespace || 73 | this.serverless.service.provider.namespace, 74 | }); 75 | return getLogs(this.options.function, opts); 76 | } 77 | } 78 | 79 | module.exports = KubelessLogs; 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-kubeless", 3 | "version": "0.11.2", 4 | "description": "This plugin enables support for Kubeless within the [Serverless Framework](https://github.com/serverless).", 5 | "main": "index.js", 6 | "directories": { 7 | "example": "examples" 8 | }, 9 | "scripts": { 10 | "lint": "eslint . --cache", 11 | "test": "mocha test/*.test.js --exit", 12 | "examples": "mocha test/examples-test.js --exit" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/serverless/serverless-kubeless.git" 17 | }, 18 | "author": "containers@bitnami.com", 19 | "license": "Apache-2.0", 20 | "bugs": { 21 | "url": "https://github.com/serverless/serverless-kubeless/issues" 22 | }, 23 | "homepage": "https://github.com/serverless/serverless-kubeless#readme", 24 | "dependencies": { 25 | "aws-sdk": "^2.527.0", 26 | "bluebird": "^3.5.0", 27 | "chalk": "^2.0.1", 28 | "js-yaml": "^3.9.0", 29 | "jszip": "^3.1.3", 30 | "kubernetes-client": "^3.12.0", 31 | "lodash": "^4.17.4", 32 | "moment": "^2.18.1" 33 | }, 34 | "devDependencies": { 35 | "chai": "^4.0.2", 36 | "chai-as-promised": "^7.1.0", 37 | "eslint": "^3.3.1", 38 | "eslint-config-airbnb": "^10.0.1", 39 | "eslint-config-airbnb-base": "^5.0.2", 40 | "eslint-plugin-import": "^1.16.0", 41 | "eslint-plugin-jsx-a11y": "^2.1.0", 42 | "eslint-plugin-react": "^6.1.1", 43 | "fs-extra": "^4.0.1", 44 | "mocha": "^4.0.0", 45 | "nock": "^9.2.3", 46 | "request": "^2.85.0", 47 | "sinon": "^4.0.0" 48 | } 49 | } -------------------------------------------------------------------------------- /provider/kubelessProvider.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Bitnami. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const providerName = 'kubeless'; 20 | 21 | class KubelessProvider { 22 | static getProviderName() { 23 | return providerName; 24 | } 25 | 26 | constructor(serverless) { 27 | this.serverless = serverless; 28 | this.provider = this; 29 | this.serverless.setProvider(providerName, this); 30 | } 31 | 32 | } 33 | 34 | module.exports = KubelessProvider; 35 | -------------------------------------------------------------------------------- /remove/kubelessRemove.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Bitnami. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const _ = require('lodash'); 20 | const BbPromise = require('bluebird'); 21 | const helpers = require('../lib/helpers'); 22 | const remove = require('../lib/remove'); 23 | 24 | class KubelessRemove { 25 | constructor(serverless, options) { 26 | this.serverless = serverless; 27 | this.options = options || {}; 28 | this.provider = this.serverless.getProvider('kubeless'); 29 | 30 | this.hooks = { 31 | 'remove:remove': () => BbPromise.bind(this) 32 | .then(this.validate) 33 | .then(this.removeFunction), 34 | }; 35 | } 36 | 37 | validate() { 38 | const unsupportedOptions = ['stage', 'region']; 39 | helpers.warnUnsupportedOptions( 40 | unsupportedOptions, 41 | this.options, 42 | this.serverless.cli.log.bind(this.serverless.cli) 43 | ); 44 | return BbPromise.resolve(); 45 | } 46 | 47 | removeFunction() { 48 | const parsedFunctions = _.map( 49 | this.serverless.service.functions, 50 | (f, id) => _.assign({ id }, f) 51 | ); 52 | return remove(parsedFunctions, this.serverless.service.service, { 53 | namespace: this.serverless.service.provider.namespace, 54 | verbose: this.options.verbose, 55 | log: this.serverless.cli.log.bind(this.serverless.cli), 56 | }); 57 | } 58 | } 59 | 60 | module.exports = KubelessRemove; 61 | -------------------------------------------------------------------------------- /scripts/install-minikube.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright (c) 2016-2017 Bitnami 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # From minikube howto 18 | export MINIKUBE_WANTUPDATENOTIFICATION=false 19 | export MINIKUBE_WANTREPORTERRORPROMPT=false 20 | export MINIKUBE_HOME=$HOME 21 | export CHANGE_MINIKUBE_NONE_USER=true 22 | mkdir -p ~/.kube 23 | touch ~/.kube/config 24 | 25 | export KUBECONFIG=$HOME/.kube/config 26 | 27 | MINIKUBE_VERSION=$MINIKUBE_VERSION 28 | 29 | install_bin() { 30 | local exe=${1:?} 31 | test -n "${TRAVIS}" && sudo install -v ${exe} /usr/local/bin || install ${exe} /usr/local/bin 32 | } 33 | 34 | # Travis ubuntu trusty env doesn't have nsenter, needed for VM-less minikube 35 | # (--vm-driver=none, runs dockerized) 36 | check_or_build_nsenter() { 37 | which nsenter >/dev/null && return 0 38 | echo "INFO: Getting 'nsenter' ..." 39 | curl -LO http://mirrors.kernel.org/ubuntu/pool/main/u/util-linux/util-linux_2.30.1-0ubuntu4_amd64.deb 40 | dpkg -x ./util-linux_2.30.1-0ubuntu4_amd64.deb /tmp/out 41 | install_bin /tmp/out/usr/bin/nsenter 42 | } 43 | check_or_install_minikube() { 44 | which minikube || { 45 | wget -q --no-clobber -O minikube \ 46 | https://storage.googleapis.com/minikube/releases/${MINIKUBE_VERSION}/minikube-linux-amd64 47 | install_bin ./minikube 48 | } 49 | } 50 | 51 | # Install nsenter if missing 52 | check_or_build_nsenter 53 | # Install minikube if missing 54 | check_or_install_minikube 55 | MINIKUBE_BIN=$(which minikube) 56 | 57 | # Start minikube 58 | sudo -E ${MINIKUBE_BIN} start --vm-driver=none \ 59 | --extra-config=apiserver.Authorization.Mode=RBAC \ 60 | --memory 4096 61 | 62 | # Wait til settles 63 | echo "INFO: Waiting for minikube cluster to be ready ..." 64 | typeset -i cnt=120 65 | until kubectl --context=minikube get pods >& /dev/null; do 66 | ((cnt=cnt-1)) || exit 1 67 | sleep 1 68 | done 69 | 70 | kubectl --context=minikube get clusterrolebinding kube-dns-admin >& /dev/null || \ 71 | kubectl --context=minikube create clusterrolebinding kube-dns-admin --serviceaccount=kube-system:default --clusterrole=cluster-admin 72 | kubectl create clusterrolebinding cluster-admin:kube-system --clusterrole=cluster-admin --serviceaccount=kube-system:default 73 | 74 | # Enable Nginx Ingress 75 | echo "INFO: Enabling ingress addon to minikube..." 76 | sudo -E ${MINIKUBE_BIN} addons enable ingress 77 | sudo -E ${MINIKUBE_BIN} config set WantUpdateNotification false 78 | 79 | # Give some time for the cluster to become healthy 80 | echo "Waiting until Nginx pod is ready ..." 81 | typeset -i cnt=300 82 | until kubectl get pods -l name=nginx-ingress-controller -n kube-system | grep -q Running; do 83 | ((cnt=cnt-1)) || exit 1 84 | sleep 1 85 | done 86 | 87 | exit 0 88 | # vim: sw=4 ts=4 et si 89 | -------------------------------------------------------------------------------- /scripts/integration-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | install_kubectl() { 5 | curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.7.8/bin/linux/amd64/kubectl 6 | chmod +x ./kubectl 7 | sudo mv ./kubectl /usr/local/bin/kubectl 8 | } 9 | 10 | install_minikube() { 11 | `dirname $0`/install-minikube.sh 12 | } 13 | 14 | install_kubecfg() { 15 | curl -LO https://github.com/ksonnet/kubecfg/releases/download/v0.5.0/kubecfg-linux-amd64 16 | chmod +x ./kubecfg-linux-amd64 17 | sudo mv ./kubecfg-linux-amd64 /usr/local/bin/kubecfg 18 | chmod +x /usr/local/bin/kubecfg 19 | if [ ! -d "ksonnet-lib" ]; then 20 | git clone --depth=1 https://github.com/ksonnet/ksonnet-lib.git ksonnet-lib 21 | fi 22 | export KUBECFG_JPATH=$PWD/ksonnet-lib 23 | } 24 | 25 | install_kubeless() { 26 | kubectl create ns kubeless 27 | kubectl create -f https://github.com/kubeless/kubeless/releases/download/$KUBELESS_VERSION/kubeless-$KUBELESS_VERSION.yaml 28 | kubectl create -f https://github.com/kubeless/kafka-trigger/releases/download/$KUBELESS_KAFKA_VERSION/kafka-zookeeper-$KUBELESS_KAFKA_VERSION.yaml 29 | curl -LO https://github.com/kubeless/kubeless/releases/download/$KUBELESS_VERSION/kubeless_linux-amd64.zip 30 | unzip kubeless_linux-amd64.zip 31 | sudo mv ./bundles/kubeless_linux-amd64/kubeless /usr/local/bin/kubeless 32 | # Wait for Kafka pod to be running 33 | until kubectl get all --all-namespaces | sed -n 's/po\/kafka-0//p' | grep Running; do kubectl -n kubeless describe pod kafka-0; sleep 10; done 34 | } 35 | 36 | install_minio() { 37 | kubectl create -f `dirname $0`/../test/minio.yml 38 | until kubectl get -n kubeless deployment minio -o jsonpath="{.status.readyReplicas}" | grep 1; do sleep 5; done 39 | } 40 | 41 | # Install dependencies 42 | echo "Installing kubectl" 43 | install_kubectl 44 | echo "Installing Minikube" 45 | install_minikube 46 | echo "Installing kubecfg" 47 | install_kubecfg 48 | echo "Installing Kubeless" 49 | install_kubeless 50 | echo "Installing Minio" 51 | install_minio 52 | kubectl get all --all-namespaces 53 | 54 | # Run tests 55 | set +e 56 | npm run examples 57 | result=$? 58 | set -e 59 | 60 | # Clean up 61 | minikube delete 62 | 63 | exit $result 64 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | function get_version { 5 | echo $(jq -r .version ./package.json) 6 | } 7 | 8 | function check_tag { 9 | local tag=$1 10 | published_tags=`curl -H "Authorization: token $ACCESS_TOKEN" -s https://api.github.com/repos/$REPO_DOMAIN/$REPO_NAME/tags` 11 | already_published=`echo $published_tags | jq ".[] | select(.name == \"$tag\")"` 12 | echo $already_published 13 | } 14 | 15 | function release_tag { 16 | local tag=$1 17 | git fetch --tags 18 | local last_tag=`curl -H "Authorization: token $ACCESS_TOKEN" -s https://api.github.com/repos/$REPO_DOMAIN/$REPO_NAME/tags | jq --raw-output '.[0].name'` 19 | local release_notes=`git log $last_tag..HEAD --oneline` 20 | local parsed_release_notes=$(echo "$release_notes" | sed -n -e 'H;${x;s/\n/\\n - /g;s/^\\n//;p;}') 21 | parsed_release_notes=`echo "$parsed_release_notes" | sed -e '${s/ \( - [^ ]* Merge pull request\)/\1/g;}'` 22 | release=`curl -H "Authorization: token $ACCESS_TOKEN" -s --data "{ 23 | \"tag_name\": \"$tag\", 24 | \"target_commitish\": \"master\", 25 | \"name\": \"$REPO_NAME-$tag\", 26 | \"body\": \"Release $tag includes the following commits: \n$parsed_release_notes\", 27 | \"draft\": false, 28 | \"prerelease\": false 29 | }" https://api.github.com/repos/$REPO_DOMAIN/$REPO_NAME/releases` 30 | echo $release | jq ".id" 31 | } 32 | 33 | version=`get_version` 34 | 35 | if [[ -z "$REPO_NAME" || -z "$REPO_DOMAIN" ]]; then 36 | echo "Github repository not specified" > /dev/stderr 37 | exit 1 38 | fi 39 | 40 | if [[ -z "$ACCESS_TOKEN" ]]; then 41 | echo "Unable to release: Github Token not specified" > /dev/stderr 42 | exit 1 43 | fi 44 | 45 | repo_check=`curl -H "Authorization: token $ACCESS_TOKEN" -s https://api.github.com/repos/$REPO_DOMAIN/$REPO_NAME` 46 | if [[ $repo_check == *"Not Found"* ]]; then 47 | echo "Not found a Github repository for $REPO_DOMAIN/$REPO_NAME, it is not possible to publish it" > /dev/stderr 48 | exit 1 49 | else 50 | tag=v$version 51 | already_published=`check_tag $tag` 52 | if [[ -z $already_published ]]; then 53 | echo "Releasing $tag in Github" 54 | release_id=`release_tag $tag` 55 | if [ "$release_id" == "null" ]; then 56 | echo "There was an error trying to release $tag" > /dev/stderr 57 | exit 1 58 | else 59 | echo "Released $tag with ID $release_id" 60 | fi 61 | else 62 | echo "Skipping Github release since $tag was already released" 63 | fi 64 | fi 65 | -------------------------------------------------------------------------------- /test/config.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Bitnami. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const _ = require('lodash'); 20 | const expect = require('chai').expect; 21 | const Config = require('../lib/config'); 22 | const helpers = require('../lib/helpers'); 23 | const sinon = require('sinon'); 24 | const loadKubeConfig = require('./lib/load-kube-config'); 25 | 26 | describe('Config', () => { 27 | describe('#constructor', () => { 28 | const previousEnv = _.cloneDeep(process.env); 29 | beforeEach(() => { 30 | sinon.stub(helpers, 'loadKubeConfig').callsFake(loadKubeConfig); 31 | }); 32 | afterEach(() => { 33 | helpers.loadKubeConfig.restore(); 34 | process.env = _.cloneDeep(previousEnv); 35 | }); 36 | it('should set the given namespace', () => { 37 | const config = new Config({ namespace: 'figjam' }); 38 | expect(config.namespace).to.be.eql('figjam'); 39 | expect(config.connectionOptions.url).to.be.eql( 40 | 'http://1.2.3.4:4433/api/v1/namespaces/figjam/configmaps/kubeless-config' 41 | ); 42 | }); 43 | it('should set the given namespace even if env var is set', () => { 44 | process.env.KUBELESS_NAMESPACE = 'foobar'; 45 | const config = new Config({ namespace: 'figjam' }); 46 | expect(config.namespace).to.be.eql('figjam'); 47 | expect(config.connectionOptions.url).to.be.eql( 48 | 'http://1.2.3.4:4433/api/v1/namespaces/figjam/configmaps/kubeless-config' 49 | ); 50 | }); 51 | it('should set the namespace given via an env var if none is given in options', () => { 52 | process.env.KUBELESS_NAMESPACE = 'foobar'; 53 | const config = new Config(); 54 | expect(config.namespace).to.be.eql('foobar'); 55 | expect(config.connectionOptions.url).to.be.eql( 56 | 'http://1.2.3.4:4433/api/v1/namespaces/foobar/configmaps/kubeless-config' 57 | ); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/helpers.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Bitnami. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const _ = require('lodash'); 20 | const expect = require('chai').expect; 21 | const fs = require('fs'); 22 | const helpers = require('../lib/helpers'); 23 | const loadKubeConfig = require('./lib/load-kube-config'); 24 | const moment = require('moment'); 25 | const os = require('os'); 26 | const path = require('path'); 27 | const rm = require('./lib/rm'); 28 | const yaml = require('js-yaml'); 29 | 30 | describe('Helper functions', () => { 31 | describe('#loadKubeConfig', () => { 32 | const configSample = loadKubeConfig(); 33 | let cwd = null; 34 | const previousEnv = _.cloneDeep(process.env); 35 | beforeEach(() => { 36 | cwd = path.join(os.tmpdir(), moment().valueOf().toString()); 37 | fs.mkdirSync(cwd); 38 | }); 39 | afterEach(() => { 40 | process.env = _.cloneDeep(previousEnv); 41 | rm(cwd); 42 | }); 43 | it('should find kubernetes config on its default path', () => { 44 | process.env.HOME = cwd; 45 | fs.mkdirSync(path.join(cwd, '.kube')); 46 | fs.writeFileSync(path.join(cwd, '.kube/config'), yaml.safeDump(configSample)); 47 | expect(helpers.loadKubeConfig()).to.be.eql(configSample); 48 | }); 49 | it('should find kubernetes config specified at KUBECONFIG', () => { 50 | process.env.KUBECONFIG = path.join(cwd, 'config'); 51 | fs.writeFileSync(path.join(cwd, 'config'), yaml.safeDump(configSample)); 52 | expect(helpers.loadKubeConfig()).to.be.eql(configSample); 53 | }); 54 | it('should merge kubernetes config specified at KUBECONFIG', () => { 55 | process.env.KUBECONFIG = `${path.join(cwd, 'config-1')}:${path.join(cwd, 'config-2')}`; 56 | fs.writeFileSync( 57 | path.join(cwd, 'config-1'), 58 | yaml.safeDump( 59 | _.assign({}, configSample, { 'current-context': 'cluster-id-1' }) 60 | ) 61 | ); 62 | fs.writeFileSync( 63 | path.join(cwd, 'config-2'), 64 | yaml.safeDump( 65 | _.assign({}, configSample, { test: 'test-value' }) 66 | ) 67 | ); 68 | expect(helpers.loadKubeConfig()).to.be.eql(_.defaults( 69 | { 'current-context': 'cluster-id-1' }, 70 | { test: 'test-value' }, 71 | configSample 72 | )); 73 | }); 74 | }); 75 | describe('#getKubernetesAPIURL', () => { 76 | it('retrieves the server URL', () => { 77 | const config = loadKubeConfig(); 78 | const expectedURL = config.clusters[0].cluster.server; 79 | expect(helpers.getKubernetesAPIURL(config)).to.be.eql(expectedURL); 80 | }); 81 | it('retrieves the server URL without the trailing /', () => { 82 | const config = loadKubeConfig({ 83 | clusters: [ 84 | { 85 | cluster: { 86 | 'certificate-authority-data': 'LS0tLS1', 87 | server: 'http://1.2.3.4:4433/', 88 | }, 89 | name: 'cluster-name', 90 | }, 91 | ], 92 | }); 93 | expect(helpers.getKubernetesAPIURL(config)).to.be.eql('http://1.2.3.4:4433'); 94 | }); 95 | }); 96 | describe('#getConnectionOptions', () => { 97 | it('should return the correct options based on the current context', () => { 98 | const config = { 99 | 'current-context': 'cluster-id-2', 100 | clusters: [{ 101 | cluster: { 'certificate-authority-data': 'LS0tLS1', server: 'http://1.2.3.4:4433' }, 102 | name: 'cluster-name-1', 103 | }, { 104 | cluster: { 'certificate-authority-data': 'LS0tLS1', server: 'http://4.3.2.1:4433' }, 105 | name: 'cluster-name-2', 106 | }], 107 | contexts: [{ 108 | context: { cluster: 'cluster-name-1', user: 'cluster-user-1' }, 109 | name: 'cluster-id-1', 110 | }, { 111 | context: { cluster: 'cluster-name-2', user: 'cluster-user-2' }, 112 | name: 'cluster-id-2', 113 | }], 114 | users: [ 115 | { name: 'cluster-user-1', user: { username: 'admin-1', password: 'password1234' } }, 116 | { name: 'cluster-user-2', user: { username: 'admin-2', password: 'password4321' } }, 117 | ], 118 | }; 119 | expect(helpers.getConnectionOptions(config)).to.be.eql({ 120 | group: 'k8s.io', 121 | namespace: 'default', 122 | url: 'http://4.3.2.1:4433', 123 | ca: Buffer.from('LS0tLS1', 'base64'), 124 | auth: { 125 | user: 'admin-2', 126 | password: 'password4321', 127 | }, 128 | }); 129 | }); 130 | it('should return the correct namespace based on the current context', () => { 131 | const config = loadKubeConfig({ 132 | 'current-context': 'cluster-id', 133 | contexts: [{ 134 | context: { cluster: 'cluster-name', user: 'cluster-user', namespace: 'custom' }, 135 | name: 'cluster-id', 136 | }], 137 | }); 138 | expect(helpers.getConnectionOptions(config).namespace).to.be.eql('custom'); 139 | }); 140 | it('should return connection options with a certificate-authority (file)', () => { 141 | const ca = path.join(os.tmpdir(), moment().valueOf().toString()); 142 | fs.writeFileSync(ca, 'abcdef1234'); 143 | const config = loadKubeConfig({ 144 | clusters: [ 145 | { 146 | cluster: { 147 | 'certificate-authority': ca, 148 | server: 'http://1.2.3.4:4433', 149 | }, 150 | name: 'cluster-name', 151 | }, 152 | ], 153 | }); 154 | try { 155 | expect(helpers.getConnectionOptions(config).ca.toString()).to.be.eql('abcdef1234'); 156 | } finally { 157 | rm(ca); 158 | } 159 | }); 160 | it('should return connection options with a certificate-authority (data)', () => { 161 | const config = loadKubeConfig({ 162 | clusters: [ 163 | { 164 | cluster: { 165 | 'certificate-authority-data': 'LS0tLS1', 166 | server: 'http://1.2.3.4:4433', 167 | }, 168 | name: 'cluster-name', 169 | }, 170 | ], 171 | }); 172 | expect(helpers.getConnectionOptions(config).ca).to.be.eql(Buffer.from('LS0tLS1', 'base64')); 173 | }); 174 | it('should return connection options with a token', () => { 175 | const config = loadKubeConfig({ 176 | users: [ 177 | { 178 | name: 'cluster-user', 179 | user: { 180 | token: 'token1234', 181 | }, 182 | }, 183 | ], 184 | }); 185 | expect(helpers.getConnectionOptions(config).auth).to.be.eql({ 186 | bearer: 'token1234', 187 | }); 188 | }); 189 | it('should return connection options with an id token', () => { 190 | const config = loadKubeConfig({ 191 | users: [ 192 | { 193 | name: 'cluster-user', 194 | user: { 195 | 'auth-provider': { 196 | config: { 197 | 'id-token': 'token1234', 198 | }, 199 | }, 200 | }, 201 | }, 202 | ], 203 | }); 204 | expect(helpers.getConnectionOptions(config).auth).to.be.eql({ 205 | bearer: 'token1234', 206 | }); 207 | }); 208 | it('should return connection options with an access token', () => { 209 | const config = loadKubeConfig({ 210 | users: [ 211 | { 212 | name: 'cluster-user', 213 | user: { 214 | 'auth-provider': { 215 | config: { 216 | 'access-token': 'token1234', 217 | expiry: moment().add('1', 'm'), 218 | }, 219 | }, 220 | }, 221 | }, 222 | ], 223 | }); 224 | expect(helpers.getConnectionOptions(config).auth).to.be.eql({ 225 | bearer: 'token1234', 226 | }); 227 | }); 228 | it('should throw an error if the access-token has expired', () => { 229 | const config = loadKubeConfig({ 230 | users: [ 231 | { 232 | name: 'cluster-user', 233 | user: { 234 | 'auth-provider': { 235 | config: { 236 | 'access-token': 'token1234', 237 | expiry: moment().subtract('1', 'm'), 238 | }, 239 | }, 240 | }, 241 | }, 242 | ], 243 | }); 244 | expect(() => helpers.getConnectionOptions(config)).to.throw('The access token has expired'); 245 | }); 246 | it('should return connection options with user and password', () => { 247 | const config = loadKubeConfig({ 248 | users: [ 249 | { 250 | name: 'cluster-user', 251 | user: { 252 | username: 'cluster-admin', 253 | password: 'admin-password', 254 | }, 255 | }, 256 | ], 257 | }); 258 | expect(helpers.getConnectionOptions(config).auth).to.be.eql({ 259 | user: 'cluster-admin', 260 | password: 'admin-password', 261 | }); 262 | }); 263 | it('should return connection options with cert and key (files)', () => { 264 | const cwd = path.join(os.tmpdir(), moment().valueOf().toString()); 265 | fs.mkdirSync(cwd); 266 | fs.writeFileSync(path.join(cwd, 'server.key'), 'abcdef1234'); 267 | fs.writeFileSync(path.join(cwd, 'cert.crt'), 'cert1234'); 268 | const config = loadKubeConfig({ 269 | users: [ 270 | { 271 | name: 'cluster-user', 272 | user: { 273 | 'client-certificate': path.join(cwd, 'cert.crt'), 274 | 'client-key': path.join(cwd, 'server.key'), 275 | }, 276 | }, 277 | ], 278 | }); 279 | const result = helpers.getConnectionOptions(config); 280 | try { 281 | expect(result.cert.toString()).to.be.eql('cert1234'); 282 | expect(result.key.toString()).to.be.eql('abcdef1234'); 283 | } finally { 284 | rm(cwd); 285 | } 286 | }); 287 | it('should return connection options with cert and key (data)', () => { 288 | const config = loadKubeConfig({ 289 | users: [ 290 | { 291 | name: 'cluster-user', 292 | user: { 293 | 'client-certificate-data': new Buffer('cert1234').toString('base64'), 294 | 'client-key-data': new Buffer('abcdef1234').toString('base64'), 295 | }, 296 | }, 297 | ], 298 | }); 299 | const result = helpers.getConnectionOptions(config); 300 | expect(result.cert.toString()).to.be.eql('cert1234'); 301 | expect(result.key.toString()).to.be.eql('abcdef1234'); 302 | }); 303 | }); 304 | }); 305 | -------------------------------------------------------------------------------- /test/kafka-novols.jsonnet: -------------------------------------------------------------------------------- 1 | # Remove volumeClaimTemplates from kafkaSts to enable testing kubeless 2 | # on simple clusters deploys like kubeadm-dind-cluster 3 | local kakfaZookeeper = import "kafka-zookeeper.jsonnet"; 4 | kakfaZookeeper + { 5 | controller+: 6 | { spec+: {template+: {spec+: {containers: [{imagePullPolicy: "IfNotPresent", name: "kafka-trigger-controller", image: std.extVar("controller_image")}] }}}}, 7 | kafkaSts+: 8 | {spec+: {template+: {spec+: {volumes: [{name: "datadir", emptyDir: {}}]}}}}, 9 | zookeeperSts+: 10 | {spec+: {template+: {spec+: {volumes: [{name: "datadir", emptyDir: {}}]}}}} 11 | } -------------------------------------------------------------------------------- /test/kubelessDeployFunction.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Bitnami. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const _ = require('lodash'); 20 | const chaiAsPromised = require('chai-as-promised'); 21 | const expect = require('chai').expect; 22 | const fs = require('fs'); 23 | const mocks = require('./lib/mocks'); 24 | const moment = require('moment'); 25 | const nock = require('nock'); 26 | const os = require('os'); 27 | const path = require('path'); 28 | const sinon = require('sinon'); 29 | 30 | const KubelessDeployFunction = require('../deployFunction/kubelessDeployFunction'); 31 | const serverlessFact = require('./lib/serverless'); 32 | 33 | let serverless = serverlessFact(); 34 | 35 | const functionName = 'myFunction'; 36 | 37 | require('chai').use(chaiAsPromised); 38 | 39 | function instantiateKubelessDeploy(zipFile, depsFile, serverlessWithFunction, options) { 40 | const kubelessDeployFunction = new KubelessDeployFunction( 41 | serverlessWithFunction, 42 | _.defaults({ function: functionName, options }) 43 | ); 44 | // Mock call to getFunctionContent when retrieving the function code 45 | sinon.stub(kubelessDeployFunction, 'getFileContent'); 46 | // Mock call to getFunctionContent when retrieving the requirements text 47 | kubelessDeployFunction.getFileContent 48 | .withArgs(zipFile, path.basename(depsFile)) 49 | .callsFake(() => ({ catch: () => ({ then: (f) => { 50 | if (fs.existsSync(depsFile)) { 51 | return f(fs.readFileSync(depsFile).toString()); 52 | } 53 | return f(null); 54 | } }) }) 55 | ); 56 | return kubelessDeployFunction; 57 | } 58 | 59 | describe('KubelessDeployFunction', () => { 60 | describe('#deploy', () => { 61 | let cwd = null; 62 | let clock = null; 63 | let config = null; 64 | let pkgFile = null; 65 | let depsFile = null; 66 | let serverlessWithFunction = null; 67 | const functionRawText = 'function code'; 68 | const functionChecksum = 69 | 'sha256:ce182d715b42b27f1babf8b4196cd4f8c900ca6593a4293d455d1e5e2296ebee'; 70 | const functionText = new Buffer(functionRawText).toString('base64'); 71 | 72 | let kubelessDeployFunction = null; 73 | let defaultFuncSpec = null; 74 | 75 | beforeEach(() => { 76 | serverless = serverlessFact(); 77 | cwd = path.join(os.tmpdir(), moment().valueOf().toString()); 78 | fs.mkdirSync(cwd); 79 | config = mocks.kubeConfig(cwd); 80 | pkgFile = path.join(cwd, 'function.zip'); 81 | fs.writeFileSync(pkgFile, functionRawText); 82 | depsFile = path.join(cwd, 'requirements.txt'); 83 | setInterval(() => { 84 | clock.tick(2001); 85 | }, 100); 86 | clock = sinon.useFakeTimers(); 87 | serverlessWithFunction = _.defaultsDeep({}, serverless, { 88 | config: { 89 | servicePath: cwd, 90 | serverless: { 91 | service: { 92 | artifact: pkgFile, 93 | }, 94 | }, 95 | }, 96 | service: { 97 | functions: {}, 98 | }, 99 | }); 100 | serverlessWithFunction.service.functions[functionName] = { 101 | handler: 'function.hello', 102 | package: {}, 103 | }; 104 | serverlessWithFunction.service.functions.otherFunction = { 105 | handler: 'function.hello', 106 | package: {}, 107 | }; 108 | kubelessDeployFunction = instantiateKubelessDeploy( 109 | pkgFile, 110 | depsFile, 111 | serverlessWithFunction 112 | ); 113 | defaultFuncSpec = (modif) => _.assign({ 114 | deps: '', 115 | function: functionText, 116 | checksum: functionChecksum, 117 | 'function-content-type': 'base64+zip', 118 | handler: serverlessWithFunction.service.functions[functionName].handler, 119 | runtime: serverlessWithFunction.service.provider.runtime, 120 | timeout: '180', 121 | service: { 122 | ports: [{ name: 'http-function-port', port: 8080, protocol: 'TCP', targetPort: 8080 }], 123 | selector: { function: functionName }, 124 | type: 'ClusterIP', 125 | }, 126 | }, modif); 127 | mocks.createDeploymentNocks( 128 | config.clusters[0].cluster.server, functionName, defaultFuncSpec(), { 129 | functionExists: true, 130 | }); 131 | nock(config.clusters[0].cluster.server) 132 | .patch(`/apis/kubeless.io/v1beta1/namespaces/default/functions/${functionName}`, { 133 | apiVersion: 'kubeless.io/v1beta1', 134 | kind: 'Function', 135 | metadata: { 136 | name: functionName, 137 | namespace: 'default', 138 | labels: { 'created-by': 'kubeless', function: functionName }, 139 | annotations: {}, 140 | }, 141 | spec: defaultFuncSpec(), 142 | }) 143 | .reply(200, '{"message": "OK"}'); 144 | }); 145 | afterEach(() => { 146 | mocks.restoreKubeConfig(cwd); 147 | nock.cleanAll(); 148 | clock.restore(); 149 | }); 150 | it('should deploy the chosen function', () => expect( 151 | kubelessDeployFunction.deployFunction() 152 | ).to.be.fulfilled); 153 | it('should redeploy only the chosen function', () => expect( 154 | kubelessDeployFunction.deployFunction().then(() => { 155 | expect(kubelessDeployFunction.serverless.service.functions).to.be.eql( 156 | { myFunction: { handler: 'function.hello', package: {} } } 157 | ); 158 | }) 159 | ).to.be.fulfilled); 160 | }); 161 | }); 162 | -------------------------------------------------------------------------------- /test/kubelessDeployStrategy.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Bitnami. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const fs = require('fs'); 20 | const os = require('os'); 21 | const path = require('path'); 22 | const moment = require('moment'); 23 | const expect = require('chai').expect; 24 | const Strategy = require('../lib/strategy'); 25 | const Base64ZipContent = require('../lib/strategy/base64_zip_content'); 26 | const serverlessFact = require('./lib/serverless'); 27 | 28 | describe('KubelessDeployStrategy', () => { 29 | it('default strategy is Base64ZipContent', () => { 30 | const serverless = serverlessFact(); 31 | const strategy = new Strategy(serverless); 32 | const product = strategy.factory(); 33 | 34 | expect(Object.getPrototypeOf(product).constructor).to.equal(Base64ZipContent); 35 | }); 36 | 37 | describe('Base64ZipContent', () => { 38 | describe('#deploy', () => { 39 | const functionRawText = 'function code'; 40 | const functionChecksum = 41 | 'sha256:ce182d715b42b27f1babf8b4196cd4f8c900ca6593a4293d455d1e5e2296ebee'; 42 | 43 | let pkgPath; 44 | 45 | beforeEach(() => { 46 | pkgPath = `${path.join(os.tmpdir(), moment().valueOf().toString())}.zip`; 47 | fs.writeFileSync(pkgPath, functionRawText); 48 | }); 49 | 50 | afterEach(() => { 51 | fs.unlinkSync(pkgPath); 52 | }); 53 | 54 | it('produces valid deploy options', () => { 55 | const serverless = serverlessFact(); 56 | const strategy = new Strategy(serverless); 57 | const fixture = new Base64ZipContent(strategy); 58 | 59 | return fixture.deploy({}, pkgPath).then(result => { 60 | expect(result.contentType).to.equal('base64+zip'); 61 | expect(result.content).to.equal(Buffer.from(functionRawText).toString('base64')); 62 | expect(result.checksum).to.equal(functionChecksum); 63 | }); 64 | }); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /test/kubelessInfo.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Bitnami. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const _ = require('lodash'); 20 | const BbPromise = require('bluebird'); 21 | const chaiAsPromised = require('chai-as-promised'); 22 | const expect = require('chai').expect; 23 | const fs = require('fs'); 24 | const moment = require('moment'); 25 | const mocks = require('./lib/mocks'); 26 | const nock = require('nock'); 27 | const os = require('os'); 28 | const path = require('path'); 29 | const rm = require('./lib/rm'); 30 | const sinon = require('sinon'); 31 | const getServerlessObj = require('./lib/serverless'); 32 | const KubelessInfo = require('../info/kubelessInfo'); 33 | 34 | const func = 'my-function'; 35 | const serviceName = 'test'; 36 | const serverless = getServerlessObj( 37 | { service: { service: serviceName, functions: { 'my-function': {} } } } 38 | ); 39 | 40 | require('chai').use(chaiAsPromised); 41 | 42 | describe('KubelessInfo', () => { 43 | describe('#constructor', () => { 44 | const options = { test: 1 }; 45 | const kubelessInfo = new KubelessInfo(serverless, options); 46 | let validateStub = null; 47 | let infoStub = null; 48 | const stubHooks = (kbInfo) => { 49 | validateStub = sinon.stub(kbInfo, 'validate').returns(BbPromise.resolve()); 50 | infoStub = sinon.stub(kbInfo, 'infoFunction').returns(BbPromise.resolve()); 51 | }; 52 | const restoreHooks = (kbInfo) => { 53 | kbInfo.validate.restore(); 54 | kbInfo.infoFunction.restore(); 55 | }; 56 | beforeEach(() => { 57 | stubHooks(kubelessInfo); 58 | }); 59 | afterEach(() => { 60 | restoreHooks(kubelessInfo); 61 | }); 62 | it('should set the serverless instance', () => { 63 | expect(kubelessInfo.serverless).to.be.eql(serverless); 64 | }); 65 | it('should set options if provided', () => { 66 | expect(kubelessInfo.options).to.be.eql(options); 67 | }); 68 | it('should set a provider ', () => { 69 | expect(kubelessInfo.provider).to.not.be.eql(undefined); 70 | }); 71 | it('should have hooks', () => expect(kubelessInfo.hooks).to.be.not.empty); 72 | it('should run promise chain in order', () => kubelessInfo.hooks['info:info']().then(() => { 73 | expect(validateStub.calledOnce).to.be.equal(true); 74 | expect(infoStub.calledAfter(validateStub)).to.be.equal(true); 75 | })); 76 | }); 77 | describe('#validate', () => { 78 | it('prints a message if an unsupported option is given', () => { 79 | const kubelessInfo = new KubelessInfo(serverless, { region: 'us-east1' }); 80 | expect(() => kubelessInfo.validate()).to.not.throw(); 81 | expect(serverless.cli.log.firstCall.args).to.be.eql( 82 | ['Warning: Option region is not supported for the kubeless plugin'] 83 | ); 84 | }); 85 | }); 86 | function mockGetCalls(config, functions, functionModif) { 87 | const namespaces = _.map(functions, f => f.namespace); 88 | _.each(namespaces, ns => { 89 | nock(config.clusters[0].cluster.server) 90 | .get(`/api/v1/namespaces/${ns}/services`) 91 | .reply(200, { 92 | items: _.map(_.filter(functions, (f) => f.namespace === ns), (f) => ({ 93 | metadata: 94 | { 95 | name: f.id, 96 | namespace: f.namespace, 97 | selfLink: `/api/v1/namespaces/${f.namespace}/services/${f.id}`, 98 | uid: '010a169d-618c-11e7-9939-080027abf356', 99 | resourceVersion: '248', 100 | creationTimestamp: '2017-07-05T14:12:39Z', 101 | labels: { function: f.id }, 102 | }, 103 | spec: 104 | { 105 | ports: [{ protocol: 'TCP', port: 8080, targetPort: 8080, nodePort: 30817 }], 106 | selector: { function: f.id }, 107 | clusterIP: '10.0.0.177', 108 | type: 'NodePort', 109 | sessionAffinity: 'None', 110 | }, 111 | status: { loadBalancer: {} }, 112 | })), 113 | }) 114 | .persist(); 115 | }); 116 | 117 | // Mock call to get.functions per namespace 118 | const allFunctions = _.map(functions, (f) => (_.defaultsDeep({}, functionModif, { 119 | apiVersion: 'kubeless.io/v1beta1', 120 | kind: 'Function', 121 | metadata: 122 | { 123 | name: f.id, 124 | namespace: f.namespace, 125 | selfLink: `/apis/kubeless.io/v1beta1/namespaces/${f.namespace}/functions/${f.id}`, 126 | uid: '0105ba84-618c-11e7-9939-080027abf356', 127 | resourceVersion: '244', 128 | creationTimestamp: '2017-07-05T14:12:39Z', 129 | }, 130 | spec: { 131 | deps: '', 132 | function: '', 133 | handler: `${f.id}.hello`, 134 | runtime: 'python2.7', 135 | }, 136 | }))); 137 | _.each(functions, f => { 138 | nock(config.clusters[0].cluster.server) 139 | .get(`/apis/kubeless.io/v1beta1/namespaces/${f.namespace}/functions/${f.id}`) 140 | .reply(200, _.find(allFunctions, (ff) => ff.metadata.name === f.id)); 141 | }); 142 | 143 | // Mock call to get.httptrigger per namespace 144 | _.each(functions, f => { 145 | if (f.path) { 146 | nock(config.clusters[0].cluster.server) 147 | .get(`/apis/kubeless.io/v1beta1/namespaces/${f.namespace}/httptriggers/${f.id}`) 148 | .reply(200, { 149 | spec: { 150 | 'host-name': '1.2.3.4.nip.io', 151 | path: f.path.replace(/^\//, ''), 152 | }, 153 | }); 154 | } 155 | }); 156 | } 157 | function infoMock(f) { 158 | return `\nService Information "${f}"\n` + 159 | 'Cluster IP: 10.0.0.177\n' + 160 | 'Type: NodePort\n' + 161 | 'Ports: \n' + 162 | ' Protocol: TCP\n' + 163 | ' Port: 8080\n' + 164 | ' Target Port: 8080\n' + 165 | ' Node Port: 30817\n' + 166 | 'Function Info\n' + 167 | `Handler: ${f}.hello\n` + 168 | 'Runtime: python2.7\n' + 169 | 'Dependencies: \n'; 170 | } 171 | 172 | 173 | describe('#printInfo', () => { 174 | let config = null; 175 | let cwd = null; 176 | 177 | beforeEach(() => { 178 | cwd = path.join(os.tmpdir(), moment().valueOf().toString()); 179 | fs.mkdirSync(cwd); 180 | config = mocks.kubeConfig(cwd); 181 | }); 182 | afterEach(() => { 183 | rm(cwd); 184 | nock.cleanAll(); 185 | }); 186 | it('should return logs with the correct formating', () => { 187 | mockGetCalls(config, [ 188 | { id: func, namespace: 'default' }, 189 | { id: 'my-function-1', namespace: 'custom-1' }, 190 | ]); 191 | const kubelessInfo = new KubelessInfo(serverless, { function: func }); 192 | return expect(kubelessInfo.infoFunction({ color: false })).to.become( 193 | infoMock(func) 194 | ); 195 | }); 196 | it('should return info for functions in different namespaces', (done) => { 197 | mockGetCalls(config, [ 198 | { id: 'my-function-1', namespace: 'custom-1' }, 199 | { id: 'my-function-2', namespace: 'custom-2' }, 200 | ]); 201 | const serverlessWithNS = getServerlessObj({ service: { 202 | service: serviceName, 203 | provider: { 204 | namespace: 'custom-1', 205 | }, 206 | functions: { 207 | 'my-function-1': {}, 208 | 'my-function-2': { namespace: 'custom-2' }, 209 | }, 210 | } }); 211 | const kubelessInfo = new KubelessInfo(serverlessWithNS); 212 | kubelessInfo.infoFunction({ color: false }).then((message) => { 213 | expect(message).to.be.eql(`${infoMock('my-function-1')}${infoMock('my-function-2')}`); 214 | done(); 215 | }); 216 | }); 217 | it('should return info only for the functions defined in the current scope', (done) => { 218 | mockGetCalls(config, [ 219 | { id: 'my-function-1', namespace: 'custom-1' }, 220 | { id: 'my-function-2', namespace: 'custom-2' }, 221 | ]); 222 | const serverlessWithNS = getServerlessObj({ 223 | service: { 224 | service: serviceName, 225 | provider: { 226 | namespace: 'custom-1', 227 | }, 228 | functions: { 229 | 'my-function-1': {}, 230 | }, 231 | }, 232 | }); 233 | const kubelessInfo = new KubelessInfo(serverlessWithNS); 234 | kubelessInfo.infoFunction({ color: false }).then((message) => { 235 | expect(message).to.be.eql(`${infoMock('my-function-1')}`); 236 | done(); 237 | }); 238 | }); 239 | it('should return an error message if no function is found', (done) => { 240 | mockGetCalls(config, [{ id: 'other-function', namespace: 'custom-1' }]); 241 | nock(config.clusters[0].cluster.server) 242 | .get('/apis/kubeless.io/v1beta1/namespaces/custom-1/functions/my-function-1') 243 | .reply(404, { code: 404 }); 244 | const serverlessWithNS = getServerlessObj({ 245 | service: { 246 | service: serviceName, 247 | provider: { 248 | namespace: 'custom-1', 249 | }, 250 | functions: { 251 | 'my-function-1': {}, 252 | }, 253 | }, 254 | }); 255 | sinon.stub(serverlessWithNS.cli, 'consoleLog'); 256 | const kubelessInfo = new KubelessInfo(serverlessWithNS); 257 | kubelessInfo.infoFunction().then(() => { 258 | expect(serverlessWithNS.cli.consoleLog.callCount).to.be.eql(1); 259 | expect(serverlessWithNS.cli.consoleLog.firstCall.args[0]).to.be.eql( 260 | 'Not found any information about the function "my-function-1"' 261 | ); 262 | done(); 263 | }); 264 | }); 265 | it('should return the description in case it exists', (done) => { 266 | mockGetCalls( 267 | config, 268 | [{ id: func, namespace: 'default' }], 269 | { metadata: { annotations: { 'kubeless.serverless.com/description': 'Test Description' } } } 270 | ); 271 | const kubelessInfo = new KubelessInfo(serverless, { function: func }); 272 | kubelessInfo.infoFunction({ color: false }).then((message) => { 273 | expect(message).to.match(/Description: Test Description/); 274 | done(); 275 | }); 276 | }); 277 | it('should return the labels in case they exist', (done) => { 278 | mockGetCalls( 279 | config, 280 | [{ id: func, namespace: 'default' }], 281 | { metadata: { labels: { label1: 'text1', label2: 'text2' } } } 282 | ); 283 | const kubelessInfo = new KubelessInfo(serverless, { function: func }); 284 | kubelessInfo.infoFunction({ color: false }).then((message) => { 285 | expect(message).to.match(/Labels:\n {2}label1: text1\n {2}label2: text2\n/); 286 | done(); 287 | }); 288 | }); 289 | it('should return the URL in case a path is specified', (done) => { 290 | mockGetCalls(config, [{ id: func, namespace: 'default', path: '/hello' }]); 291 | const kubelessInfo = new KubelessInfo(serverless, { function: func }); 292 | kubelessInfo.infoFunction({ color: false }).then((message) => { 293 | expect(message).to.match(/URL: {2}1.2.3.4.nip.io\/hello\n/); 294 | done(); 295 | }); 296 | }); 297 | }); 298 | }); 299 | -------------------------------------------------------------------------------- /test/kubelessLogs.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Bitnami. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const _ = require('lodash'); 20 | const Api = require('kubernetes-client'); 21 | const BbPromise = require('bluebird'); 22 | const chaiAsPromised = require('chai-as-promised'); 23 | const expect = require('chai').expect; 24 | const helpers = require('../lib/helpers'); 25 | const loadKubeConfig = require('./lib/load-kube-config'); 26 | const moment = require('moment'); 27 | const request = require('request'); 28 | const sinon = require('sinon'); 29 | 30 | const KubelessLogs = require('../logs/kubelessLogs'); 31 | 32 | const f = 'my-function'; 33 | const serverless = require('./lib/serverless')({ service: { functions: { 'my-function': {} } } }); 34 | 35 | require('chai').use(chaiAsPromised); 36 | 37 | describe('KubelessLogs', () => { 38 | describe('#constructor', () => { 39 | const options = { test: 1 }; 40 | const kubelessLogs = new KubelessLogs(serverless, options); 41 | let validateStub = null; 42 | let logsStub = null; 43 | const stubHooks = (kbLogs) => { 44 | validateStub = sinon.stub(kbLogs, 'validate').returns(BbPromise.resolve()); 45 | logsStub = sinon.stub(kbLogs, 'printLogs').returns(BbPromise.resolve()); 46 | }; 47 | const restoreHooks = (kbLogs) => { 48 | kbLogs.validate.restore(); 49 | kbLogs.printLogs.restore(); 50 | }; 51 | beforeEach(() => { 52 | stubHooks(kubelessLogs); 53 | }); 54 | afterEach(() => { 55 | restoreHooks(kubelessLogs); 56 | }); 57 | it('should set the serverless instance', () => { 58 | expect(kubelessLogs.serverless).to.be.eql(serverless); 59 | }); 60 | it('should set options if provided', () => { 61 | expect(kubelessLogs.options).to.be.eql(options); 62 | }); 63 | it('should set a provider ', () => { 64 | expect(kubelessLogs.provider).to.not.be.eql(undefined); 65 | }); 66 | it('should have hooks', () => expect(kubelessLogs.hooks).to.not.be.empty); 67 | it('should run promise chain in order', () => kubelessLogs.hooks['logs:logs']().then(() => { 68 | expect(validateStub.calledOnce).to.be.equal(true); 69 | expect(logsStub.calledAfter(validateStub)).to.be.equal(true); 70 | })); 71 | }); 72 | describe('#validate', () => { 73 | it('prints a message if an unsupported option is given', () => { 74 | const kubelessLogs = new KubelessLogs(serverless, { region: 'us-east1', function: f }); 75 | expect(() => kubelessLogs.validate()).to.not.throw(); 76 | expect(serverless.cli.log.firstCall.args).to.be.eql( 77 | ['Warning: Option region is not supported for the kubeless plugin'] 78 | ); 79 | }); 80 | it('throws an error if the function provider is not present in the description', () => { 81 | const kubelessInvoke = new KubelessLogs(serverless, { 82 | function: 'foo', 83 | }); 84 | expect(() => kubelessInvoke.validate()).to.throw( 85 | 'The function foo is not present in the current description' 86 | ); 87 | }); 88 | }); 89 | describe('#printLogs', () => { 90 | const pod = 'my-pod'; 91 | /* eslint-disable max-len */ 92 | const logsSample = 93 | // Yesterday 94 | `172.17.0.1 - - [${moment().utc().subtract('1', 'd').format('DD/MMM/YYYY:HH:mm:ss')} +0000] "GET /healthz HTTP/1.1" 200 2 "" "Go-http-client/1.1" 0/95\n` + 95 | // One hour before 96 | `172.17.0.1 - - [${moment().utc().subtract('1', 'h').format('DD/MMM/YYYY:HH:mm:ss')} +0000] "POST / HTTP/1.1" 500 742 "" "" 0/484\n` + 97 | // One minute before 98 | `172.17.0.1 - - [${moment().utc().subtract('1', 'm').format('DD/MMM/YYYY:HH:mm:ss')} +0000] "GET /healthz HTTP/1.1" 200 2 "" "Go-http-client/1.1" 0/84`; 99 | /* eslint-enable max-len */ 100 | 101 | beforeEach(() => { 102 | sinon.stub(Api.Core.prototype, 'get').callsFake(function (p, ff) { 103 | if (p.path[0] === `/api/v1/namespaces/${this.ns.namespace}/pods`) { 104 | // Mock call to get.pods 105 | ff(null, { 106 | statusCode: 200, 107 | body: { 108 | items: [ 109 | // Check that pods that are not functions are ignored 110 | { metadata: { name: 'unrelated' } }, 111 | { metadata: { name: pod, labels: { function: f } } }, 112 | ], 113 | }, 114 | }); 115 | } 116 | if (p.path[0] === `/api/v1/namespaces/${this.ns.namespace}/pods/${pod}/log`) { 117 | // Mock call to pods('my-function').log.get 118 | ff(null, 119 | { 120 | statusCode: 200, 121 | body: logsSample, 122 | } 123 | ); 124 | } 125 | }); 126 | sinon.stub(helpers, 'loadKubeConfig').callsFake(loadKubeConfig); 127 | }); 128 | afterEach(() => { 129 | Api.Core.prototype.get.restore(); 130 | helpers.loadKubeConfig.restore(); 131 | }); 132 | it('should print the function logs', () => { 133 | const kubelessLogs = new KubelessLogs(serverless, { function: f }); 134 | return expect(kubelessLogs.printLogs({ silent: true })).to.become(logsSample); 135 | }); 136 | it('should throw an error if the the function has not been deployed', () => { 137 | const serverlessTest = _.cloneDeep(serverless); 138 | serverlessTest.service.functions.test = {}; 139 | const kubelessLogs = new KubelessLogs(serverlessTest, { function: 'test' }); 140 | return expect(kubelessLogs.printLogs()).to.be.eventually.rejectedWith( 141 | 'Unable to find the pod for the function test' 142 | ); 143 | }); 144 | it('should filter a specific number of log lines', () => { 145 | const kubelessLogs = new KubelessLogs(serverless, { count: 1, function: f }); 146 | return expect(kubelessLogs.printLogs({ silent: true })).to.become(logsSample.split('\n')[2]); 147 | }); 148 | it('should filter a lines with a pattern', () => { 149 | const kubelessLogs = new KubelessLogs(serverless, { filter: 'POST', function: f }); 150 | return expect(kubelessLogs.printLogs({ silent: true })).to.become(logsSample.split('\n')[1]); 151 | }); 152 | it('should filter a lines with a start time as a date string', () => { 153 | const kubelessLogs = new KubelessLogs(serverless, { 154 | // In the last two minutes 155 | startTime: moment().subtract('2', 'm').format(), 156 | function: f, 157 | }); 158 | // Should return last entry 159 | return expect(kubelessLogs.printLogs({ silent: true })).to.become(logsSample.split('\n')[2]); 160 | }); 161 | it('should filter a lines with a start time as a number', () => { 162 | const kubelessLogs = new KubelessLogs(serverless, { 163 | // In the last two minutes 164 | startTime: moment().subtract('2', 'm').valueOf(), 165 | function: f, 166 | }); 167 | // Should return last entry 168 | return expect(kubelessLogs.printLogs({ silent: true })).to.become(logsSample.split('\n')[2]); 169 | }); 170 | it('should filter a lines from a period of time', () => { 171 | const kubelessLogs = new KubelessLogs(serverless, { 172 | // In the last two hours 173 | startTime: '2h', 174 | function: f, 175 | }); 176 | // Should return last two entries 177 | return expect(kubelessLogs.printLogs({ silent: true })).to.become( 178 | logsSample.split('\n').slice(1).join('\n') 179 | ); 180 | }); 181 | it('should not print anything if there are no entries that pass the filter', () => { 182 | sinon.stub(console, 'log'); 183 | try { 184 | const kubelessLogs = new KubelessLogs(serverless, { 185 | // Right now 186 | startTime: moment().format(), 187 | function: f, 188 | }); 189 | const promise = kubelessLogs.printLogs({ silent: true }); 190 | expect(console.log.callCount).to.be.eql(0); 191 | return expect(promise).to.become(''); 192 | } finally { 193 | console.log.restore(); 194 | } 195 | }); 196 | it('calls Kubernetes API following the logs in case it is required', () => { 197 | sinon.stub(request, 'get').returns({ 198 | on: () => {}, 199 | }); 200 | try { 201 | const kubelessLogs = new KubelessLogs(serverless, { function: f, tail: true }); 202 | kubelessLogs.printLogs(); 203 | expect(request.get.calledOnce).to.be.eql(true); 204 | expect(request.get.firstCall.args[0].url).to.be.eql( 205 | `${loadKubeConfig().clusters[0].cluster.server}` + 206 | `/api/v1/namespaces/default/pods/${pod}/log?follow=true` 207 | ); 208 | } finally { 209 | request.get.restore(); 210 | } 211 | }); 212 | it('calls Kubernetes API with the correct namespace', () => { 213 | const serverlessWithNS = _.cloneDeep(serverless); 214 | serverlessWithNS.service.functions[f].namespace = 'test'; 215 | const kubelessLogs = new KubelessLogs(serverlessWithNS, { function: f, tail: true }); 216 | kubelessLogs.printLogs(); 217 | expect(Api.Core.prototype.get.callCount).to.be.eql(1); 218 | expect(Api.Core.prototype.get.firstCall.args[0].path[0]).to.be.eql( 219 | '/api/v1/namespaces/test/pods' 220 | ); 221 | }); 222 | it('calls Kubernetes API with the correct namespace (tailing)', () => { 223 | sinon.stub(request, 'get').returns({ 224 | on: () => {}, 225 | }); 226 | try { 227 | const serverlessWithNS = _.cloneDeep(serverless); 228 | serverlessWithNS.service.functions[f].namespace = 'test'; 229 | const kubelessLogs = new KubelessLogs(serverlessWithNS, { function: f, tail: true }); 230 | kubelessLogs.printLogs(); 231 | expect(request.get.calledOnce).to.be.eql(true); 232 | expect(request.get.firstCall.args[0].url).to.be.eql( 233 | `${loadKubeConfig().clusters[0].cluster.server}` + 234 | `/api/v1/namespaces/test/pods/${pod}/log?follow=true` 235 | ); 236 | } finally { 237 | request.get.restore(); 238 | } 239 | }); 240 | }); 241 | }); 242 | -------------------------------------------------------------------------------- /test/kubelessRemove.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Bitnami. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const _ = require('lodash'); 20 | const Api = require('kubernetes-client'); 21 | const BbPromise = require('bluebird'); 22 | const chaiAsPromised = require('chai-as-promised'); 23 | const expect = require('chai').expect; 24 | const fs = require('fs'); 25 | const mocks = require('./lib/mocks'); 26 | const moment = require('moment'); 27 | const nock = require('nock'); 28 | const os = require('os'); 29 | const path = require('path'); 30 | const sinon = require('sinon'); 31 | const rm = require('./lib/rm'); 32 | 33 | const KubelessRemove = require('../remove/kubelessRemove'); 34 | const serverless = require('./lib/serverless')(); 35 | 36 | require('chai').use(chaiAsPromised); 37 | 38 | describe('KubelessRemove', () => { 39 | describe('#constructor', () => { 40 | const options = { test: 1 }; 41 | const kubelessRemove = new KubelessRemove(serverless, options); 42 | let validateStub = null; 43 | let removeStub = null; 44 | const stubHooks = (kbRemove) => { 45 | validateStub = sinon.stub(kbRemove, 'validate').returns(BbPromise.resolve()); 46 | removeStub = sinon.stub(kbRemove, 'removeFunction').returns(BbPromise.resolve()); 47 | }; 48 | const restoreHooks = (kbRemove) => { 49 | kbRemove.validate.restore(); 50 | kbRemove.removeFunction.restore(); 51 | }; 52 | beforeEach(() => { 53 | stubHooks(kubelessRemove); 54 | }); 55 | afterEach(() => { 56 | restoreHooks(kubelessRemove); 57 | }); 58 | it('should set the serverless instance', () => { 59 | expect(kubelessRemove.serverless).to.be.eql(serverless); 60 | }); 61 | it('should set options if provided', () => { 62 | expect(kubelessRemove.options).to.be.eql(options); 63 | }); 64 | it('should set a provider ', () => { 65 | expect(kubelessRemove.provider).to.not.be.eql(undefined); 66 | }); 67 | it('should have hooks', () => expect(kubelessRemove.hooks).to.be.not.empty); 68 | it( 69 | 'should run promise chain in order', 70 | () => kubelessRemove.hooks['remove:remove']().then(() => { 71 | expect(validateStub.calledOnce).to.be.equal(true); 72 | expect(removeStub.calledAfter(validateStub)).to.be.equal(true); 73 | }) 74 | ); 75 | }); 76 | describe('#validate', () => { 77 | it('prints a message if an unsupported option is given', () => { 78 | const kubelessRemove = new KubelessRemove(serverless, { region: 'us-east1' }); 79 | expect(() => kubelessRemove.validate()).to.not.throw(); 80 | expect(serverless.cli.log.firstCall.args).to.be.eql( 81 | ['Warning: Option region is not supported for the kubeless plugin'] 82 | ); 83 | }); 84 | }); 85 | describe('#remove', () => { 86 | let config = null; 87 | let cwd = null; 88 | let serverlessWithFunction = null; 89 | let kubelessRemove = null; 90 | 91 | beforeEach(() => { 92 | serverlessWithFunction = _.defaultsDeep({}, serverless, { 93 | service: { 94 | functions: { 95 | myFunction: { 96 | handler: 'function.hello', 97 | }, 98 | }, 99 | }, 100 | }); 101 | kubelessRemove = new KubelessRemove(serverlessWithFunction); 102 | cwd = path.join(os.tmpdir(), moment().valueOf().toString()); 103 | fs.mkdirSync(cwd); 104 | config = mocks.kubeConfig(cwd); 105 | fs.writeFileSync(path.join(cwd, 'function.py'), 'function code'); 106 | sinon.stub(Api.Extensions.prototype, 'delete'); 107 | sinon.stub(Api.Extensions.prototype, 'get'); 108 | Api.Extensions.prototype.get.callsFake((data, ff) => { 109 | ff(null, { statusCode: 200, body: { items: [] } }); 110 | }); 111 | Api.Extensions.prototype.delete.callsFake((data, ff) => { 112 | ff(null, { statusCode: 200 }); 113 | }); 114 | }); 115 | afterEach(() => { 116 | nock.cleanAll(); 117 | Api.Extensions.prototype.delete.restore(); 118 | Api.Extensions.prototype.get.restore(); 119 | rm(cwd); 120 | }); 121 | it('should remove a function', () => { 122 | nock(config.clusters[0].cluster.server) 123 | .delete('/apis/kubeless.io/v1beta1/namespaces/default/functions/myFunction') 124 | .reply(200, {}); 125 | const result = expect( // eslint-disable-line no-unused-expressions 126 | kubelessRemove.removeFunction(cwd).then(() => { 127 | expect(nock.pendingMocks()).to.not.contain('DELETE http://1.2.3.4:4433/apis/kubeless.io/v1beta1/namespaces/default/functions/myFunction'); 128 | }) 129 | ).to.be.fulfilled; 130 | return result; 131 | }); 132 | it('should skip a removal if an error 404 is returned', () => { 133 | nock(config.clusters[0].cluster.server) 134 | .delete('/apis/kubeless.io/v1beta1/namespaces/default/functions/myFunction') 135 | .reply(404, { code: 404 }); 136 | return expect( // eslint-disable-line no-unused-expressions 137 | kubelessRemove.removeFunction(cwd).then(() => { 138 | expect(nock.pendingMocks()).to.not.contain('DELETE http://1.2.3.4:4433/apis/kubeless.io/v1beta1/namespaces/default/functions/myFunction'); 139 | }) 140 | ).to.be.fulfilled; 141 | }); 142 | it('should fail if a removal returns an error code', () => { 143 | nock(config.clusters[0].cluster.server) 144 | .delete('/apis/kubeless.io/v1beta1/namespaces/default/functions/myFunction') 145 | .reply(500, { code: 500, message: 'Internal server error' }); 146 | return expect( // eslint-disable-line no-unused-expressions 147 | kubelessRemove.removeFunction(cwd) 148 | ).to.be.eventually.rejectedWith('Internal server error'); 149 | }); 150 | it('should remove the possible functions even if one of them fails', (done) => { 151 | const serverlessWithFunctions = _.defaultsDeep({}, serverless, { 152 | service: { 153 | functions: { 154 | myFunction1: { 155 | handler: 'function.hello', 156 | }, 157 | myFunction2: { 158 | handler: 'function.hello', 159 | }, 160 | myFunction3: { 161 | handler: 'function.hello', 162 | }, 163 | }, 164 | }, 165 | }); 166 | nock(config.clusters[0].cluster.server) 167 | .delete('/apis/kubeless.io/v1beta1/namespaces/default/functions/myFunction1') 168 | .reply(200, {}); 169 | nock(config.clusters[0].cluster.server) 170 | .delete('/apis/kubeless.io/v1beta1/namespaces/default/functions/myFunction2') 171 | .reply(500, { code: 500, message: 'Internal server error' }); 172 | nock(config.clusters[0].cluster.server) 173 | .delete('/apis/kubeless.io/v1beta1/namespaces/default/functions/myFunction3') 174 | .reply(200, {}); 175 | kubelessRemove = new KubelessRemove(serverlessWithFunctions); 176 | kubelessRemove.removeFunction(cwd).catch(e => { 177 | expect(e.message).to.contain('Message: Internal server error'); 178 | expect(nock.pendingMocks()).to.be.eql([]); 179 | done(); 180 | }); 181 | }); 182 | it('calls Kubernetes API with the correct namespace (in provider)', () => { 183 | const serverlessWithNS = _.cloneDeep(serverlessWithFunction); 184 | serverlessWithNS.service.provider.namespace = 'test'; 185 | nock(config.clusters[0].cluster.server) 186 | .delete('/apis/kubeless.io/v1beta1/namespaces/test/functions/myFunction') 187 | .reply(200, {}); 188 | kubelessRemove = new KubelessRemove(serverlessWithNS); 189 | return expect( // eslint-disable-line no-unused-expressions 190 | kubelessRemove.removeFunction(cwd).then(() => { 191 | expect(nock.pendingMocks()).to.be.eql([]); 192 | }) 193 | ).to.be.fulfilled; 194 | }); 195 | it('calls Kubernetes API with the correct namespace (in function)', () => { 196 | const serverlessWithNS = _.cloneDeep(serverlessWithFunction); 197 | serverlessWithNS.service.functions.myFunction.namespace = 'test'; 198 | nock(config.clusters[0].cluster.server) 199 | .delete('/apis/kubeless.io/v1beta1/namespaces/test/functions/myFunction') 200 | .reply(200, {}); 201 | kubelessRemove = new KubelessRemove(serverlessWithNS); 202 | return expect( // eslint-disable-line no-unused-expressions 203 | kubelessRemove.removeFunction(cwd).then(() => { 204 | expect(nock.pendingMocks()).to.be.eql([]); 205 | }) 206 | ).to.be.fulfilled; 207 | }); 208 | }); 209 | }); 210 | -------------------------------------------------------------------------------- /test/lib/load-kube-config.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Bitnami. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const _ = require('lodash'); 20 | 21 | module.exports = function (config) { 22 | return _.defaults({}, config, { 23 | apiVersion: 'v1', 24 | 'current-context': 'cluster-id', 25 | clusters: [ 26 | { 27 | cluster: { 28 | 'certificate-authority-data': 'LS0tLS1', 29 | server: 'http://1.2.3.4:4433', 30 | }, 31 | name: 'cluster-name', 32 | }, 33 | ], 34 | contexts: [ 35 | { 36 | context: { 37 | cluster: 'cluster-name', 38 | user: 'cluster-user', 39 | }, 40 | name: 'cluster-id', 41 | }, 42 | ], 43 | users: [ 44 | { 45 | name: 'cluster-user', 46 | user: { 47 | username: 'admin', 48 | password: 'password1234', 49 | }, 50 | }, 51 | ], 52 | }); 53 | }; 54 | -------------------------------------------------------------------------------- /test/lib/mocks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const fs = require('fs'); 5 | const moment = require('moment'); 6 | const nock = require('nock'); 7 | const path = require('path'); 8 | const rm = require('./rm'); 9 | const sinon = require('sinon'); 10 | const yaml = require('js-yaml'); 11 | 12 | function thirdPartyResources(kubelessDeploy, namespace) { 13 | const put = sinon.stub().callsFake((body, callback) => { 14 | callback(null, { statusCode: 200 }); 15 | }); 16 | const result = { 17 | namespaces: { 18 | namespace: namespace || 'default', 19 | }, 20 | ns: { 21 | functions: () => ({ 22 | put, 23 | }), 24 | }, 25 | addResource: sinon.stub(), 26 | }; 27 | result.ns.functions.post = sinon.stub().callsFake((body, callback) => { 28 | callback(null, { statusCode: 200 }); 29 | }); 30 | result.ns.functions.get = sinon.stub().callsFake((callback) => { 31 | callback(null, { statusCode: 200, body: { items: [] } }); 32 | }); 33 | if (kubelessDeploy.getThirdPartyResources.isSinonProxy) { 34 | kubelessDeploy.getThirdPartyResources.returns(result); 35 | } else { 36 | sinon.stub(kubelessDeploy, 'getThirdPartyResources').returns(result); 37 | } 38 | return result; 39 | } 40 | 41 | function kubeConfig(cwd) { 42 | fs.mkdirSync(path.join(cwd, '.kube')); 43 | fs.writeFileSync( 44 | path.join(cwd, '.kube/config'), 45 | 'apiVersion: v1\n' + 46 | 'current-context: cluster-id\n' + 47 | 'clusters:\n' + 48 | '- cluster:\n' + 49 | ' certificate-authority-data: LS0tLS1\n' + 50 | ' server: http://1.2.3.4:4433\n' + 51 | ' name: cluster-name\n' + 52 | 'contexts:\n' + 53 | '- context:\n' + 54 | ' cluster: cluster-name\n' + 55 | ' namespace: custom\n' + 56 | ' user: cluster-user\n' + 57 | ' name: cluster-id\n' + 58 | 'users:\n' + 59 | '- name: cluster-user\n' + 60 | ' user:\n' + 61 | ' username: admin\n' + 62 | ' password: password1234\n' 63 | ); 64 | process.env.HOME = cwd; 65 | return yaml.safeLoad(fs.readFileSync(path.join(cwd, '.kube/config'))); 66 | } 67 | 68 | const previousEnv = _.cloneDeep(process.env); 69 | 70 | function restoreKubeConfig(cwd) { 71 | rm(cwd); 72 | process.env = _.cloneDeep(previousEnv); 73 | } 74 | 75 | 76 | function createDeploymentNocks(endpoint, func, funcSpec, options) { 77 | const opts = _.defaults({}, options, { 78 | namespace: 'default', 79 | functionExists: false, 80 | description: null, 81 | labels: null, 82 | postReply: { message: 'OK' }, 83 | }); 84 | const postBody = { 85 | apiVersion: 'kubeless.io/v1beta1', 86 | kind: 'Function', 87 | metadata: { 88 | name: func, 89 | namespace: opts.namespace, 90 | labels: _.assign({ 91 | 'created-by': 'kubeless', 92 | function: func, 93 | }, opts.labels), 94 | annotations: {}, 95 | }, 96 | spec: funcSpec, 97 | }; 98 | if (opts.description) { 99 | postBody.metadata.annotations['kubeless.serverless.com/description'] = opts.description; 100 | } 101 | if (opts.labels) { 102 | postBody.spec.service.selector = _.assign(postBody.spec.service.selector); 103 | } 104 | nock(endpoint) 105 | .persist() 106 | .get('/api/v1/namespaces/kubeless/configmaps/kubeless-config') 107 | .reply(200, JSON.stringify({ data: { 'runtime-images': JSON.stringify([ 108 | { ID: 'python', depName: 'requirements.txt' }, 109 | { ID: 'nodejs', depName: 'package.json' }, 110 | { ID: 'ruby', depName: 'Gemfile' }, 111 | ]) } })); 112 | if (opts.functionExists) { 113 | nock(endpoint) 114 | .get(`/apis/kubeless.io/v1beta1/namespaces/${opts.namespace}/functions/${func}`) 115 | .reply(200, JSON.stringify(postBody)); 116 | } else { 117 | nock(endpoint) 118 | .get(`/apis/kubeless.io/v1beta1/namespaces/${opts.namespace}/functions/${func}`) 119 | .reply(404, JSON.stringify({ code: 404 })); 120 | } 121 | nock(endpoint) 122 | .post(`/apis/kubeless.io/v1beta1/namespaces/${opts.namespace}/functions/`, postBody) 123 | .reply(200, opts.postReply); 124 | nock(endpoint) 125 | .persist() 126 | .get(`/api/v1/namespaces/${opts.namespace}/pods`) 127 | .reply(200, JSON.stringify({ 128 | items: [{ 129 | metadata: { 130 | name: func, 131 | labels: { function: func }, 132 | annotations: {}, 133 | creationTimestamp: moment().add('60', 's'), 134 | }, 135 | spec: funcSpec, 136 | status: { 137 | containerStatuses: [{ ready: true, restartCount: 0 }], 138 | }, 139 | }], 140 | })); 141 | nock(endpoint) 142 | .persist() 143 | .get(`/api/v1/namespaces/${opts.namespace}/services`) 144 | .reply(200, JSON.stringify({ 145 | items: [{ 146 | metadata: { 147 | name: func, 148 | labels: { function: func }, 149 | annotations: {}, 150 | creationTimestamp: moment().add('60', 's'), 151 | }, 152 | }], 153 | })); 154 | } 155 | 156 | function createTriggerNocks(endpoint, func, hostname, p, options) { 157 | const opts = _.defaults({}, options, { 158 | namespace: 'default', 159 | }); 160 | nock(endpoint) 161 | .post(`/apis/kubeless.io/v1beta1/namespaces/${opts.namespace}/httptriggers/`, { 162 | apiVersion: 'kubeless.io/v1beta1', 163 | kind: 'HTTPTrigger', 164 | metadata: { 165 | name: func, 166 | namespace: opts.namespace, 167 | annotations: { 168 | 'kubernetes.io/ingress.class': 'nginx', 169 | }, 170 | labels: { 171 | 'created-by': 'kubeless', 172 | }, 173 | }, 174 | spec: { 175 | 'host-name': hostname, 176 | 'basic-auth-secret': '', 177 | 'cors-enable': false, 178 | 'function-name': func, 179 | gateway: 'nginx', 180 | path: p.replace(/^\//, ''), 181 | tls: false, 182 | 'tls-secret': '', 183 | }, 184 | }) 185 | .reply(200, { message: 'OK' }); 186 | } 187 | 188 | module.exports = { 189 | thirdPartyResources, 190 | kubeConfig, 191 | restoreKubeConfig, 192 | createDeploymentNocks, 193 | createTriggerNocks, 194 | }; 195 | -------------------------------------------------------------------------------- /test/lib/rm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | 5 | function rm(p) { 6 | if (fs.existsSync(p)) { 7 | if (fs.lstatSync(p).isFile()) { 8 | fs.unlinkSync(p); 9 | } else { 10 | fs.readdirSync(p).forEach((file) => { 11 | const curPath = `${p}/${file}`; 12 | if (fs.lstatSync(curPath).isDirectory()) { // recurse 13 | rm(curPath); 14 | } else { // delete file 15 | fs.unlinkSync(curPath); 16 | } 17 | }); 18 | fs.rmdirSync(p); 19 | } 20 | } 21 | } 22 | 23 | module.exports = rm; 24 | -------------------------------------------------------------------------------- /test/lib/serverless.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 Bitnami. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const _ = require('lodash'); 20 | const fs = require('fs'); 21 | const sinon = require('sinon'); 22 | 23 | class CLI { 24 | constructor() { 25 | this.log = sinon.stub(); 26 | this.consoleLog = () => {}; 27 | } 28 | } 29 | 30 | module.exports = (modif) => _.defaultsDeep({}, modif, { 31 | config: () => {}, 32 | pluginManager: { getPlugins: () => [] }, 33 | classes: { Error, CLI }, 34 | service: { 35 | getFunction: () => {}, 36 | package: {}, 37 | provider: { 38 | runtime: 'python2.7', 39 | }, 40 | resources: {}, 41 | functions: {}, 42 | getAllFunctions: () => [], 43 | }, 44 | cli: new CLI(), 45 | getProvider: () => ({}), 46 | utils: { 47 | fileExistsSync: (p) => fs.existsSync(p), 48 | readFileSync: (p) => { 49 | const content = fs.readFileSync(p); 50 | if (p.endsWith('.json')) { 51 | return JSON.parse(content); 52 | } 53 | return content; 54 | }, 55 | }, 56 | }); 57 | -------------------------------------------------------------------------------- /test/minio.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 3 | kind: Deployment 4 | metadata: 5 | name: minio 6 | namespace: kubeless 7 | spec: 8 | template: 9 | metadata: 10 | labels: 11 | app: minio 12 | spec: 13 | initContainers: 14 | - name: kubeless-bucket-init 15 | image: minio/minio:RELEASE.2019-09-05T23-24-38Z 16 | volumeMounts: 17 | - name: data 18 | mountPath: "/data" 19 | command: ['/bin/sh', '-c', 'mkdir -p /data/kubeless'] 20 | containers: 21 | - name: minio 22 | volumeMounts: 23 | - name: data 24 | mountPath: "/data" 25 | image: minio/minio:RELEASE.2019-09-05T23-24-38Z 26 | args: 27 | - server 28 | - /data 29 | env: 30 | - name: MINIO_ACCESS_KEY 31 | value: "minio" 32 | - name: MINIO_SECRET_KEY 33 | value: "minio123" 34 | ports: 35 | - containerPort: 9000 36 | readinessProbe: 37 | httpGet: 38 | path: /minio/health/ready 39 | port: 9000 40 | initialDelaySeconds: 5 41 | periodSeconds: 5 42 | livenessProbe: 43 | httpGet: 44 | path: /minio/health/live 45 | port: 9000 46 | initialDelaySeconds: 10 47 | periodSeconds: 10 48 | volumes: 49 | - name: data 50 | emptyDir: {} 51 | --- 52 | apiVersion: v1 53 | kind: Service 54 | metadata: 55 | name: minio 56 | namespace: kubeless 57 | spec: 58 | type: ClusterIP 59 | clusterIP: 10.98.211.80 60 | ports: 61 | - port: 9000 62 | targetPort: 9000 63 | protocol: TCP 64 | selector: 65 | app: minio 66 | --------------------------------------------------------------------------------