├── .circleci └── config.yml ├── .dockerignore ├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .npmignore ├── Dockerfile ├── LICENSE ├── README.md ├── bin ├── clean.sh ├── lint.sh └── test.sh ├── docs └── kubesail-logo.png ├── package.json ├── src ├── deployNodeApp.js ├── index.js ├── languages │ ├── nextjs.js │ ├── nginx.js │ ├── nodejs.js │ ├── plain-dockerfile.js │ ├── python.js │ └── ruby.js ├── modules │ ├── mongodb.js │ ├── postgres.js │ └── redis.js └── util.js ├── test ├── create-react-app │ ├── .dockerignore │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── App.css │ │ ├── App.js │ │ ├── App.test.js │ │ ├── index.css │ │ ├── index.js │ │ ├── logo.svg │ │ ├── serviceWorker.js │ │ └── setupTests.js │ └── yarn.lock ├── index.js ├── nginx-simple │ ├── README.md │ └── public │ │ └── index.html ├── nodejs-hotreload │ ├── README.md │ ├── package.json │ └── src │ │ └── index.js ├── nodejs-mongodb │ ├── package.json │ └── src │ │ └── index.js ├── nodejs-postgres │ ├── package.json │ └── src │ │ └── index.js ├── nodejs-redis │ ├── index.js │ └── package.json ├── nodejs-simple │ ├── index.js │ └── package.json ├── python-redis │ ├── init.sh │ ├── package.json │ ├── requirements.txt │ └── server.py ├── python-simple │ ├── requirements.txt │ └── server.py ├── ruby-redis │ ├── Gemfile │ └── app │ │ └── index.rb └── ruby-simple │ ├── Gemfile │ └── index.rb └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: kubesail/dna-test:14 6 | working_directory: ~/repo 7 | steps: 8 | - checkout 9 | 10 | - run: 11 | name: Installing 12 | command: yarn --no-progress --no-emoji --prefer-offline 13 | 14 | - run: 15 | name: Linting 16 | command: ./bin/lint.sh 17 | 18 | - run: 19 | name: set Kubeconfig 20 | command: echo "$DNA_KUBECONFIG" | base64 -d > kubeconfig.yaml 21 | 22 | - run: 23 | name: Testing 24 | command: ./bin/test.sh 25 | 26 | workflows: 27 | version: 2 28 | build-and-deploy: 29 | jobs: 30 | - build 31 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | test/node_modules 4 | yarn-error.log 5 | .env 6 | tmp 7 | test/*/*/k8s 8 | test/*/*/Dockerfile 9 | test/*/*/skaffold.yaml 10 | test/*/*/.* 11 | test/nginx/simple/package.json 12 | kubeconfig.yaml 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root=true 2 | [*] 3 | charset=utf-8 4 | end_of_line=lf 5 | insert_final_newline=true 6 | indent_style=space 7 | indent_size=2 8 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignorePatterns": ["out/", ".next", "dev-server.js", "https-server.js"], 3 | "extends": ["standard", "plugin:security/recommended", "prettier"], 4 | "plugins": ["standard", "security", "prettier"], 5 | "rules": { 6 | "react/no-unescaped-entities": 0, 7 | "@next/next/no-document-import-in-page": 0, 8 | "react-hooks/exhaustive-deps": 0, 9 | "@next/next/no-img-element": 0, 10 | "prettier/prettier": "warn", 11 | "no-console": ["warn", { "allow": ["warn", "error"] }], 12 | // "no-return-assign": 0, 13 | "require-await": 0, 14 | "prefer-regex-literals": 0, 15 | // "quotes": ["error", "single"], 16 | // "no-return-await": 0, 17 | "array-callback-return": 0, 18 | "no-unreachable": ["warn"], 19 | "no-use-before-define": "error", 20 | "no-unused-vars": [1, { "vars": "all", "args": "none", "varsIgnorePattern": "^(logger$|React$|_)" }], 21 | "standard/computed-property-even-spacing": 0, 22 | "camelcase": 0, 23 | "object-curly-spacing": ["error", "always"], 24 | "security/detect-object-injection": 0, 25 | "security/detect-non-literal-fs-filename": 0, 26 | "security/detect-unsafe-regex": 0, 27 | "import/order": ["warn", { "groups": ["builtin", "external", "internal"] }], 28 | "no-unused-expressions": 0 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | yarn.lock binary 2 | *.secret binary 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | test/node_modules 4 | yarn-error.log 5 | .env 6 | tmp 7 | test/*/*/k8s 8 | test/*/*/Dockerfile 9 | test/*/*/skaffold.yaml 10 | test/*/*/.* 11 | test/nginx/simple/package.json 12 | kubeconfig.yaml 13 | test/*/.dna.json 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test 3 | docs 4 | Dockerfile -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Published as kubesail/dna-test:14 2 | 3 | FROM node:21-bullseye-slim 4 | 5 | ARG TARGETARCH 6 | ENV TARGETARCH=${TARGETARCH:-amd64} 7 | 8 | RUN apt-get update -yqq && \ 9 | apt-get install -yqq bash curl git && \ 10 | curl -Ls https://storage.googleapis.com/kubernetes-release/release/v1.18.1/bin/linux/$TARGETARCH/kubectl -o /usr/local/bin/kubectl && \ 11 | chmod +x /usr/local/bin/kubectl && \ 12 | curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash && \ 13 | mv kustomize /usr/local/bin/kustomize && \ 14 | curl -s -Lo skaffold https://storage.googleapis.com/skaffold/releases/latest/skaffold-linux-$TARGETARCH && \ 15 | chmod +x skaffold && \ 16 | mv skaffold /usr/local/bin/skaffold 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Seandon Mooy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **deploy-node-app** 2 | 3 | [![npm version](https://img.shields.io/npm/v/deploy-node-app.svg?style=flat-square)](https://www.npmjs.com/package/deploy-node-app) 4 | 5 | ### Deploy apps to Kubernetes, with zero config! 6 | 7 | `deploy-node-app` will prompt you with a minimal set of questions required to deploy your app to any Kubernetes cluster. If zero-config with no lock-in sounds too good to be true - remember this project is in **beta** :wink:. However, it mostly works, and `deploy-node-app` also supports more than just Node.js projects! Try it on a Python or Ruby project or a static site project! 8 | 9 | Once you've run `deploy-node-app` in your project, you can commit your `.dna.json` file and use `deploy-node-app` with no prompts in the future (works great for CI too!). 10 | 11 | ## Instructions 12 | 13 | Just run `npx deploy-node-app` in your node project. 14 | 15 | ![Example](https://github.com/kubesail/deploy-node-app/raw/master/docs/terminal-example-1.svg?sanitize=true) 16 | 17 | ## What does this tool do? 18 | 19 | `deploy-node-app` is a project bootstrapper, powered by [Skaffold](https://github.com/GoogleContainerTools/skaffold). After answering a few questions about your app, this tool can: 20 | 21 | 1. Create a Dockerfile, skaffold.yaml and all the Kubernetes YAML you need! 22 | 2. Automatically provision common dependencies (like redis and postgres)! 23 | 3. Develop and deploy your app on any Kubernetes cluster 24 | 25 | Essentially, `deploy-node-app` supercharges any web applications with awesome tools and best practices. 26 | 27 | ## Usage and examples 28 | 29 | ``` 30 | Usage: deploy-node-app [env] [action] 31 | 32 | Options: 33 | -V, --version output the version number 34 | -w, --write Write files to project (writes out Dockerfile, skaffold.yaml, etc) 35 | -u, --update Update existing files (default: false) 36 | -f, --force Dont prompt if possible (default: false) 37 | -l, --label [foo=bar,tier=service] Add labels to created Kubernetes resources 38 | -t, --target Target project directory (default: ".") 39 | -c, --config Kubernetes configuration file (default: "~/.kube/config") 40 | -m, --modules Explicitly add modules 41 | ``` 42 | 43 | By default, `deploy-node-app` will write a few files to your directory, and by default files won't be touched if they've been modified. `deploy-node-app` by itself is the same as `deploy-node-app production deploy` 44 | 45 | Simply run `npx deploy-node-app` in your repository. The tool will attempt to prompt you when it needs answers to questions, and do it's best to bootstrap your application. Take a look at [supported languages](https://github.com/kubesail/deploy-node-app/tree/master/src/languages) - we're always looking to add more! 46 | 47 | ## Tests-as-examples 48 | 49 | Take a look at [/test](https://github.com/kubesail/deploy-node-app/tree/master/test) for a growing list of examples! 50 | 51 | ## Dependencies 52 | 53 | `deploy-node-app` knows about dependencies! For example, if you install a redis or postgres driver for Node.js, Python, Ruby [and more](https://github.com/kubesail/deploy-node-app/tree/master/src/languages), `deploy-node-app` will automatically create Redis or Postgres deployments that work with your app! 54 | 55 | ## Suggested tools: 56 | 57 | - [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) - required for creating your deployment, and recommended for managing your deployment after created 58 | - [Skaffold](https://skaffold.dev/docs/install/) - Kubernetes workflow utility 59 | 60 | --- 61 | 62 | deploy-node-app is created and maintained by 63 | 64 | [Kubesail 65 |
66 | KubeSail - Kubernetes for Human Beings](https://kubesail.com) 67 | 68 | --- 69 | 70 | ### Contributing 71 | 72 | If you feel that this tool can be improved in any way, feel free to open an issue or pull request! 73 | -------------------------------------------------------------------------------- /bin/clean.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | git clean -xdf test/ > /dev/null 4 | git checkout -- test/*/package.json 5 | -------------------------------------------------------------------------------- /bin/lint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | ./node_modules/.bin/eslint src --no-eslintrc -c .eslintrc.json 6 | -------------------------------------------------------------------------------- /bin/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eE 4 | function finish { 5 | set +x 6 | echo "Tests Failed!" 7 | } 8 | trap finish ERR 9 | 10 | bash ./bin/clean.sh 11 | SKAFFOLD_PATH=$(which skaffold) ./node_modules/.bin/mocha ./test/index.js --bail 12 | bash ./bin/clean.sh 13 | -------------------------------------------------------------------------------- /docs/kubesail-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kubesail/deploy-node-app/d5011157f15c08b8d7fd5e2d9dc0a5569ba82925/docs/kubesail-logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deploy-node-app", 3 | "version": "3.1.0", 4 | "description": "Develop and deploy Node.js apps with Kubernetes, with zero config!", 5 | "main": "src/index.js", 6 | "repository": "https://github.com/kubesail/deploy-node-app", 7 | "license": "MIT", 8 | "engines": { 9 | "node": ">=8" 10 | }, 11 | "prettier": { 12 | "semi": false, 13 | "singleQuote": true, 14 | "printWidth": 100, 15 | "arrowParens": "avoid", 16 | "trailingComma": "none" 17 | }, 18 | "contributors": [ 19 | { 20 | "name": "Seandon Mooy", 21 | "email": "seandon@kubesail.com" 22 | }, 23 | { 24 | "name": "Dan Pastusek", 25 | "email": "dan@kubesail.com" 26 | } 27 | ], 28 | "scripts": { 29 | "test": "./bin/test.sh" 30 | }, 31 | "bin": { 32 | "deploy-node-app": "src/index.js", 33 | "deploy-to-kube": "src/index.js" 34 | }, 35 | "authors": [ 36 | "Seandon Mooy ", 37 | "Dan Pastusek " 38 | ], 39 | "devDependencies": { 40 | "babel-eslint": "^10.1.0", 41 | "chai": "^4.3.6", 42 | "eslint": "^8.22.0", 43 | "eslint-config-prettier": "^8.5.0", 44 | "eslint-config-standard": "^17.0.0", 45 | "eslint-plugin-import": "^2.26.0", 46 | "eslint-plugin-n": "^15.2.4", 47 | "eslint-plugin-node": "^11.1.0", 48 | "eslint-plugin-prettier": "^4.2.1", 49 | "eslint-plugin-promise": "^6.0.0", 50 | "eslint-plugin-security": "^1.5.0", 51 | "eslint-plugin-standard": "^5.0.0", 52 | "mocha": "^10.0.0", 53 | "prettier": "^2.7.1", 54 | "prettier-eslint": "^15.0.1", 55 | "prettier-eslint-cli": "^7.0.0" 56 | }, 57 | "dependencies": { 58 | "ansi-styles": "5.2.0", 59 | "chalk": "^4", 60 | "commander": "^9.4.0", 61 | "diff": "^5.1.0", 62 | "get-kubesail-config": "^1.0.4", 63 | "got": "^11", 64 | "inquirer": "8.2.3", 65 | "inquirer-fuzzy-path": "^2.3.0", 66 | "js-yaml": "^4.1.0", 67 | "lodash": "^4.17.21", 68 | "mkdirp": "^1.0.4", 69 | "validator": "^13.7.0" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/deployNodeApp.js: -------------------------------------------------------------------------------- 1 | // Deploy Node App (deploy-node-app) - Develop and deploy Node.js apps with Kubernetes, with zero config! 2 | // developed by KubeSail.com! 3 | 4 | const fs = require('fs') 5 | const os = require('os') 6 | const path = require('path') 7 | const chalk = require('chalk') 8 | const inquirer = require('inquirer') 9 | const { isFQDN } = require('validator') 10 | const yaml = require('js-yaml') 11 | const merge = require('lodash/merge') 12 | const style = require('ansi-styles') 13 | const getKubesailConfig = require('get-kubesail-config') 14 | inquirer.registerPrompt('fuzzypath', require('inquirer-fuzzy-path')) 15 | const { 16 | fatal, 17 | log, 18 | debug, 19 | mkdir, 20 | prompt, 21 | cleanupWrittenFiles, 22 | readDNAConfig, 23 | ensureBinaries, 24 | writeTextLine, 25 | execSyncWithEnv, 26 | confirmWriteFile 27 | } = require('./util') 28 | const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) 29 | const WARNING = `${style.yellow.open}!!${style.yellow.close}` 30 | 31 | // Load meta-modules! These match dependency packages to files in ./modules - these files in turn build out Kubernetes resources! 32 | const metaModules = [ 33 | require('./modules/mongodb'), 34 | require('./modules/postgres'), 35 | require('./modules/redis') 36 | ] 37 | 38 | // Should be sorted in order of specificity 39 | // IE: All Next.js apps are Node.js apps, but not all Node.js apps are Next.js apps: therefore, Next.js should come before Node.js 40 | const languages = [ 41 | require('./languages/nextjs'), 42 | require('./languages/nginx'), 43 | require('./languages/nodejs'), 44 | require('./languages/python'), 45 | require('./languages/ruby'), 46 | require('./languages/plain-dockerfile') 47 | ] 48 | 49 | // Only allow projects that are valid dns components - we will prompt the user for a different name if this is name matched 50 | const validProjectNameRegex = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/i 51 | 52 | // Read local .kube configuration 53 | let kubeConfig = {} 54 | function readLocalKubeConfig(configPathOption) { 55 | const configPath = configPathOption || path.join(os.homedir(), '.kube', 'config') 56 | debug(`Using kube config ${configPath}`) 57 | if (!fs.existsSync(configPath)) return {} 58 | try { 59 | kubeConfig = yaml.load(fs.readFileSync(configPath)) 60 | } catch (err) { 61 | fatal( 62 | `It seems you have a Kubernetes config file at ${configPath}, but it is not valid yaml, or unreadable! Error: ${err.message}` 63 | ) 64 | } 65 | } 66 | 67 | // promptForPackageName tries to get a URI-able name out of a project using validProjectNameRegex 68 | // This ensures a DNS-valid name for Kuberentes as well as for container registries, etc. 69 | async function promptForPackageName( 70 | packageName = '', 71 | force = false, 72 | message = 'What should we name this app? (internal name, not shown to users, must be URL-safe)\n' 73 | ) { 74 | const sanitizedName = packageName.split('.')[0] 75 | if (force && validProjectNameRegex.test(sanitizedName)) { 76 | process.stdout.write(`${WARNING} Using project name ${chalk.green.bold(sanitizedName)}...\n\n`) 77 | return sanitizedName 78 | } else { 79 | const newName = packageName.replace(/[^a-z0-9-]/gi, '') 80 | if (force) return newName 81 | else { 82 | process.stdout.write('\n') 83 | const { name } = await prompt([ 84 | { 85 | name: 'name', 86 | type: 'input', 87 | message, 88 | default: newName, 89 | validate: input => (validProjectNameRegex.test(input) ? true : 'Invalid name!') 90 | } 91 | ]) 92 | return name 93 | } 94 | } 95 | } 96 | 97 | async function promptForEntrypoint(language, options) { 98 | if (fs.existsSync('./package.json')) { 99 | const packageJson = JSON.parse(fs.readFileSync('./package.json')) 100 | const useYarn = fs.existsSync('./yarn.lock') 101 | if (typeof language.skipEntrypointPrompt) return language.entrypoint() 102 | if (packageJson.scripts) { 103 | const choices = Object.keys(packageJson.scripts).map(k => 104 | useYarn ? `yarn ${k}` : `npm run ${k}` 105 | ) 106 | const chooseFile = 'Enter a command' 107 | choices.push(chooseFile) 108 | let defaultScript = packageJson.scripts.start ? 'start' : Object.keys(packageJson.scripts)[0] 109 | if (!defaultScript) defaultScript = chooseFile 110 | debug('promptForEntrypoint:', { choices, useYarn, defaultScript }) 111 | const { entrypoint } = await prompt([ 112 | { 113 | name: 'entrypoint', 114 | type: 'rawlist', 115 | message: 'Which command starts your application? (From package.json)', 116 | default: useYarn ? `yarn ${defaultScript}` : `npm run ${defaultScript}`, 117 | choices 118 | } 119 | ]) 120 | if (entrypoint && entrypoint !== chooseFile) return entrypoint 121 | } 122 | } 123 | const suggestedDefaultPaths = language.suggestedEntrypoints || [ 124 | 'src/index.js', 125 | 'index.js', 126 | 'index.py', 127 | 'src/index.py', 128 | 'public/index.html', 129 | 'main.py', 130 | 'server.py', 131 | 'index.html' 132 | ] 133 | const invalidPaths = [ 134 | 'LICENSE', 135 | 'README', 136 | 'package-lock.json', 137 | 'node_modules', 138 | 'yarn.lock', 139 | 'yarn-error.log', 140 | 'package.json', 141 | 'Dockerfile', 142 | '.log', 143 | '.json', 144 | '.lock', 145 | '.css', 146 | '.svg', 147 | '.md', 148 | '.png', 149 | '.disabled', 150 | '.ico', 151 | '.txt' 152 | ] 153 | const entrypointFromDockerRaw = (options.dockerfileContents || '').match(/^ENTRYPOINT (.*)$/m) 154 | 155 | const defaultValue = suggestedDefaultPaths.find(p => fs.existsSync(path.join(options.target, p))) 156 | if (entrypointFromDockerRaw) { 157 | try { 158 | const entrypointFromDocker = JSON.parse(entrypointFromDockerRaw[1]) 159 | return entrypointFromDocker.join(' ') 160 | } catch (err) {} 161 | return entrypointFromDockerRaw[1].replace(/"/g, '') 162 | } 163 | if (options.dockerfileContents) return '' 164 | if (language.skipEntrypointPrompt) return language.entrypoint() 165 | 166 | process.stdout.write('\n') 167 | const { entrypoint } = await prompt([ 168 | { 169 | name: 'entrypoint', 170 | type: 'fuzzypath', 171 | message: 'What command starts your application? (eg: "npm start", "server.py", "index.html")', 172 | default: defaultValue, 173 | excludePath: nodePath => nodePath.startsWith('node_modules'), 174 | excludeFilter: filepath => { 175 | return filepath.startsWith('.') || invalidPaths.find(p => filepath.endsWith(p)) 176 | }, 177 | itemType: 'file', 178 | rootPath: options.target, 179 | suggestOnly: true 180 | } 181 | ]) 182 | const response = entrypoint.replace(/\\/g, '/').replace(options.target, '.') 183 | if (!response) return defaultValue 184 | else return response 185 | } 186 | 187 | async function promptForImageName(projectName, existingName) { 188 | process.stdout.write('\n') 189 | 190 | let assumedUsername = os.userInfo().username 191 | try { 192 | if (fs.statSync('.git/config')) { 193 | const gitConfig = fs 194 | .readFileSync('.git/config') 195 | .toString('ascii') 196 | .replace(/\t/g, '') 197 | .replace(/\r\n/g, '\n') 198 | .trim() 199 | .split('\n') 200 | .find(s => s.startsWith('url =')) 201 | if (gitConfig) { 202 | const [_prefix, repo] = gitConfig.split(':') 203 | if (repo) { 204 | const [username, _project] = repo.split('/') 205 | if (username) assumedUsername = username 206 | } 207 | } 208 | } 209 | } catch (err) { 210 | debug(`Failed to parse .git/config file to obtain username`, { errMsg: err.message }) 211 | } 212 | 213 | const { imageName } = await prompt([ 214 | { 215 | name: 'imageName', 216 | type: 'input', 217 | message: 218 | 'What is the container image name for our project? To use docker hub, try username/project-name. Note: Make sure this is marked private, or it may be automatically created as a public image!', 219 | default: existingName || `${assumedUsername}/${projectName.replace(/[^a-z0-9-]/gi, '')}` 220 | } 221 | ]) 222 | process.stdout.write('\n') 223 | return imageName 224 | } 225 | 226 | // Prompts user for project ports and attempts to suggest best practices 227 | async function promptForPorts(existingPorts = [], language, options = {}) { 228 | let defaultValue = 229 | existingPorts.length > 0 230 | ? existingPorts.join(', ') 231 | : (language.suggestedPorts || [8000]).join(', ') 232 | 233 | if (options.dockerfileContents) { 234 | const portsFromDockerfileRaw = (options.dockerfileContents || '').match(/^EXPOSE (.*)$/m) 235 | if (portsFromDockerfileRaw) { 236 | defaultValue = portsFromDockerfileRaw[1].split(' ')[0].replace(/\/.*$/, '') 237 | return [defaultValue] 238 | } 239 | } 240 | process.stdout.write('\n') 241 | const { newPorts } = await prompt([ 242 | { 243 | name: 'newPorts', 244 | type: 'input', 245 | message: 'Does your app listen on any ports? If so, please enter them comma separated', 246 | default: defaultValue, 247 | 248 | validate: input => { 249 | if (!input) return true 250 | const ports = input.replace(/ /g, '').split(',') 251 | for (let i = 0; i < ports.length; i++) { 252 | const port = parseInt(ports[i], 10) 253 | if (isNaN(port)) return 'Ports must be numbers!' 254 | else if (port <= 1024) { 255 | log( 256 | `${WARNING} We strongly suggest not using a "low port" - please choose a port above 1024 in your Dockerfile!` 257 | ) 258 | return true 259 | } else if (port >= 65535) { 260 | return 'Ports higher than 65535 will typically not work, please choose a port between 1024 and 65535!' 261 | } 262 | } 263 | return true 264 | } 265 | } 266 | ]) 267 | return newPorts 268 | .replace(/ /g, '') 269 | .split(',') 270 | .map(port => parseInt(port, 10)) 271 | .filter(Boolean) 272 | } 273 | 274 | async function promptForIngress(language, _options) { 275 | process.stdout.write('\n') 276 | 277 | if (!language.skipHttpPrompt) { 278 | const { isHttp } = await prompt([ 279 | { name: 'isHttp', type: 'confirm', default: true, message: 'Is this an HTTP service?' } 280 | ]) 281 | if (!isHttp) return false 282 | } 283 | 284 | const baseDirName = path.basename(process.cwd()) 285 | if (isFQDN(baseDirName)) { 286 | debug('Project directory is an FQDN - assuming this is the domain the user wants') 287 | return baseDirName 288 | } 289 | 290 | let supportsAutoGeneratingIngress = false 291 | if (kubeConfig && kubeConfig['current-context'] && kubeConfig.contexts) { 292 | const { context } = kubeConfig.contexts.find(c => c.name === kubeConfig['current-context']) 293 | const { cluster } = kubeConfig.clusters.find(c => c.name === context.cluster) 294 | if (cluster && cluster.server && cluster.server.indexOf('kubesail.com')) { 295 | supportsAutoGeneratingIngress = true 296 | } 297 | } 298 | 299 | process.stdout.write('\n') 300 | const { ingressUri } = await prompt([ 301 | { 302 | name: 'ingressUri', 303 | type: 'input', 304 | message: 305 | 'What domain will this use?' + 306 | (supportsAutoGeneratingIngress ? ' (leave blank for auto-generate)' : ''), 307 | validate: input => { 308 | if (input && (input.startsWith('http://') || input.startsWith('https://'))) { 309 | input = input.replace(/^https?:\/\//, '') 310 | } 311 | const fqdn = isFQDN(input) 312 | if (input && fqdn) return true 313 | else if (input && !fqdn) return 'Please input a valid DNS name (ie: my.example.com)' 314 | else return supportsAutoGeneratingIngress 315 | } 316 | } 317 | ]) 318 | if (!ingressUri && supportsAutoGeneratingIngress) return true 319 | else return ingressUri 320 | } 321 | 322 | async function promptForLanguage(options) { 323 | if (options.language) { 324 | const found = languages.find(l => l.name === options.language) 325 | if (found) { 326 | found.detectedOptions = options.languageOptions 327 | return found 328 | } else { 329 | log(`No such language ${options.language}`) 330 | } 331 | } 332 | for (let i = 0; i < languages.length; i++) { 333 | const detected = await languages[i].detect(options) 334 | if (detected) { 335 | languages[i].detectedOptions = detected 336 | return languages[i] 337 | } 338 | } 339 | return fatal( 340 | "Unable to determine what sort of project this is. Please let us know what langauge this project is written in at https://github.com/kubesail/deploy-node-app/issues and we'll add support!" 341 | ) 342 | } 343 | 344 | // Asks the user if they'd like to create a KubeSail.com context, if they have none. 345 | async function promptForCreateKubeContext() { 346 | if (!kubeConfig || !kubeConfig.clusters || !kubeConfig.clusters.length) { 347 | process.stdout.write('\n') 348 | const { createKubeSailContext } = await prompt([ 349 | { 350 | name: 'createKubeSailContext', 351 | type: 'confirm', 352 | message: 353 | 'It looks like you have no Kubernetes cluster configured. Would you like to create a free Kubernetes namespace on KubeSail.com?\n' 354 | } 355 | ]) 356 | // getKubesailConfig will pop the users browser and return with a new valid context if the user signs up. 357 | if (createKubeSailContext) kubeConfig = await getKubesailConfig() 358 | fatal("You'll need a Kubernetes config before continuing!") 359 | } 360 | } 361 | 362 | // Asks the user if there are additional artifacts they'd like to add to their configuration 363 | async function promptForAdditionalArtifacts(options) { 364 | if (!options.prompts) return false 365 | process.stdout.write('\n') 366 | const { additionalArtifacts } = await prompt([ 367 | { 368 | name: 'additionalArtifacts', 369 | type: 'confirm', 370 | default: false, 371 | message: 'Would you like to add an additional entrypoint? (ie: Is this a mono-repo?)\n' 372 | } 373 | ]) 374 | return additionalArtifacts 375 | } 376 | 377 | // Write out the Kustomization files for a meta-module 378 | async function writeModuleConfiguration( 379 | env = 'production', 380 | mod, 381 | options = { force: false, update: false } 382 | ) { 383 | if (typeof mod !== 'object' || typeof mod.name !== 'string') throw new Error('Invalid module!') 384 | const modPath = `k8s/dependencies/${mod.name}` 385 | const deploymentFile = `${mod.kind || 'deployment'}.yaml` 386 | const resources = [deploymentFile] 387 | const secrets = [] 388 | await mkdir(modPath, options) 389 | if (mod.ports && mod.ports.length > 0) { 390 | await writeService(`${modPath}/service.yaml`, mod.name, mod.ports, options) 391 | resources.push('service.yaml') 392 | } 393 | await writeKustomization(`${modPath}/kustomization.yaml`, { ...options, resources, secrets: [] }) 394 | if (mod.envs) { 395 | await mkdir(`k8s/overlays/${env}/secrets`, options) 396 | await writeSecret(`k8s/overlays/${env}/secrets/${mod.name}.env`, { ...options, ...mod, env }) 397 | secrets.push({ name: mod.name, path: `secrets/${mod.name}.env` }) 398 | } 399 | await writeDeployment(`${modPath}/${deploymentFile}`, null, { ...options, ...mod, secrets }) 400 | return { base: `../../../${modPath}`, secrets } 401 | } 402 | 403 | function loadAndMergeYAML(path, newData) { 404 | if (!newData) throw new Error('loadAndMergeYAML handed null newData') 405 | let yamlStr = '' 406 | if (fs.existsSync(path)) { 407 | const existing = yaml.load(fs.readFileSync(path)) 408 | merge(existing, newData) 409 | if (typeof existing !== 'object') throw new Error('loadAndMergeYAML null existing') 410 | yamlStr = yaml.dump(existing) 411 | } else yamlStr = yaml.dump(newData) 412 | return yamlStr + '\n' 413 | } 414 | 415 | // Writes a simple Kubernetes Deployment object 416 | async function writeDeployment(path, language, options = { force: false, update: false }) { 417 | const { name, entrypoint, image, ports = [], secrets = [] } = options 418 | const resources = { requests: { cpu: '50m', memory: '100Mi' } } 419 | const containerPorts = ports.map(port => { 420 | return { containerPort: port } 421 | }) 422 | 423 | const container = { name, image, ports: containerPorts, resources } 424 | if (entrypoint) container.command = entrypoint 425 | if (language && language.entrypoint) container.command = language.entrypoint(container.command) 426 | if (!container.command) delete container.command 427 | 428 | if (typeof container.command === 'string') { 429 | container.command = container.command.split(' ').filter(Boolean) 430 | } 431 | 432 | if (secrets.length > 0) { 433 | container.envFrom = secrets.map(secret => { 434 | return { secretRef: { name: secret.name } } 435 | }) 436 | } 437 | 438 | await confirmWriteFile( 439 | path, 440 | loadAndMergeYAML(path, { 441 | apiVersion: 'apps/v1', 442 | kind: 'Deployment', 443 | metadata: { name }, 444 | spec: { 445 | replicas: 1, 446 | selector: { matchLabels: { app: name } }, 447 | template: { 448 | metadata: { labels: { app: name } }, 449 | spec: { containers: [container] } 450 | } 451 | } 452 | }), 453 | options 454 | ) 455 | } 456 | 457 | // Writes a simple Kubernetes Service object 458 | async function writeService(path, name, ports, options = { force: false, update: false }) { 459 | await confirmWriteFile( 460 | path, 461 | loadAndMergeYAML(path, { 462 | apiVersion: 'v1', 463 | kind: 'Service', 464 | metadata: { name }, 465 | spec: { 466 | selector: { app: name }, 467 | ports: ports.map(port => { 468 | return { port, targetPort: port, protocol: 'TCP' } 469 | }) 470 | } 471 | }), 472 | options 473 | ) 474 | } 475 | 476 | // Writes a simple Kubernetes Ingress object 477 | async function writeIngress(path, name, uri, port, options = { force: false, update: false }) { 478 | const portObj = {} 479 | if (isNaN(parseInt(port, 10))) portObj.name = port 480 | else portObj.number = port 481 | const rule = { 482 | http: { 483 | paths: [{ pathType: 'ImplementationSpecific', backend: { service: { name, port: portObj } } }] 484 | } 485 | } 486 | const spec = { rules: [rule] } 487 | if (typeof uri === 'string') { 488 | spec.tls = [{ hosts: [uri], secretName: name }] 489 | rule.host = uri 490 | } 491 | await confirmWriteFile( 492 | path, 493 | loadAndMergeYAML(path, { 494 | apiVersion: 'networking.k8s.io/v1', 495 | kind: 'Ingress', 496 | metadata: { name }, 497 | spec 498 | }), 499 | options 500 | ) 501 | } 502 | 503 | async function writeKustomization(path, options = { force: false, update: false }) { 504 | const { resources = [], bases = [], secrets = {} } = options 505 | const kustomization = { resources, bases } 506 | if (secrets.length > 0) { 507 | kustomization.secretGenerator = [] 508 | for (let i = 0; i < secrets.length; i++) { 509 | kustomization.secretGenerator.push({ name: secrets[i].name, envs: [secrets[i].path] }) 510 | } 511 | } 512 | await confirmWriteFile(path, loadAndMergeYAML(path, kustomization), options) 513 | } 514 | 515 | async function writeSecret(path, options = { force: false, update: false }) { 516 | const { envs } = options 517 | const lines = [] 518 | const existingSecrets = {} 519 | await mkdir(`k8s/overlays/${options.env}/secrets`, { ...options, dontPrune: true }) 520 | if (fs.existsSync(path)) { 521 | const lines = fs.readFileSync(path).toString().split('\n').filter(Boolean) 522 | lines.forEach((line, i) => { 523 | try { 524 | existingSecrets[line.slice(0, line.indexOf('='))] = line.slice( 525 | line.indexOf('=') + 1, 526 | line.length 527 | ) 528 | } catch (err) { 529 | log(`${WARNING} Failed to parse secret from "${path}", line ${i + 1}`) 530 | } 531 | }) 532 | } 533 | for (const key in envs) { 534 | let value = 535 | process.env[key] || typeof envs[key] === 'function' 536 | ? envs[key](existingSecrets[key], options) 537 | : envs[key] 538 | if (value instanceof Promise) value = await value 539 | lines.push(`${key}=${value}`) 540 | } 541 | await confirmWriteFile(path, lines.join('\n') + '\n', { ...options, dontPrune: true }) 542 | } 543 | 544 | async function writeSkaffold(path, envs, options = { force: false, update: false }) { 545 | const envNames = Object.keys(envs) 546 | const profiles = envNames.map(envName => { 547 | const env = envs[envName] 548 | return { 549 | name: envName, 550 | deploy: { kustomize: { paths: [`k8s/overlays/${envName}`] } }, 551 | build: { 552 | artifacts: env.map(a => { 553 | const language = languages.find(l => l.name === a.language) 554 | if (!language) throw new Error('Unable to detect language in writeSkaffold!') 555 | return language.artifact 556 | ? language.artifact(envName, a.image) 557 | : { 558 | image: a.image, 559 | sync: { manual: [{ src: 'src/**/*.js', dest: '.' }] }, 560 | docker: { buildArgs: { ENV: envName } } 561 | } 562 | }) 563 | } 564 | } 565 | }) 566 | const artifacts = envNames 567 | .map(e => 568 | envs[e].map(a => { 569 | return { image: a.image } 570 | }) 571 | ) 572 | .flat() 573 | await confirmWriteFile( 574 | path, 575 | loadAndMergeYAML(path, { 576 | apiVersion: 'skaffold/v3', 577 | kind: 'Config', 578 | portForward: [], 579 | build: { artifacts }, 580 | profiles 581 | }), 582 | options 583 | ) 584 | } 585 | 586 | async function generateArtifact( 587 | env = 'production', 588 | envConfig, 589 | options = { update: false, force: false } 590 | ) { 591 | // Ask some questions if we have missing info in our 'deploy-node-app' configuration: 592 | let name = options.name || envConfig.name 593 | const baseDirName = path.basename(process.cwd()) 594 | if (!name && validProjectNameRegex.test(baseDirName)) name = baseDirName 595 | if (options.forceNew || !name || !validProjectNameRegex.test(name)) 596 | name = await promptForPackageName( 597 | envConfig.find(e => e.name === name) ? `${baseDirName}-new` : baseDirName, 598 | options.force 599 | ) 600 | // If there is another artifact in this env with the same name but a different entrypoint, let's ask the user for a different name 601 | if (options.forceNew) { 602 | while (envConfig.find(e => e.name === name)) { 603 | name = await promptForPackageName( 604 | `${baseDirName}-new`, 605 | options.force, 606 | 'It looks like that name is already used! Pick a different name for this artifact:\n' 607 | ) 608 | } 609 | } 610 | 611 | if (fs.existsSync('Dockerfile')) { 612 | options.dockerfileContents = fs.readFileSync('Dockerfile').toString() 613 | } 614 | 615 | const language = await promptForLanguage(options) 616 | debug('promptForLanguage', { language, languageFromOptions: options.language }) 617 | 618 | // Entrypoint: 619 | let entrypoint = 620 | options.entrypoint || 621 | (envConfig[0] && envConfig[0].entrypoint) || 622 | (language.skipEntrypointPrompt ? undefined : await promptForEntrypoint(language, options)) 623 | if (options.forceNew) entrypoint = await promptForEntrypoint(language, options) 624 | let artifact = envConfig.find(e => e.entrypoint === entrypoint) || {} 625 | 626 | // Container ports: 627 | let ports = options.ports || artifact.ports 628 | if (!ports || ports === 'none') ports = [] 629 | if (language.skipPortPrompt && ports.length === 0 && language.suggestedPorts) { 630 | ports = language.suggestedPorts 631 | } 632 | if (ports.length === 0 && !artifact.ports && !language.skipPortPrompt) { 633 | ports = await promptForPorts(artifact.ports, language, options) 634 | } 635 | 636 | // If this process listens on a port, write a Kubernetes Service and potentially an Ingress 637 | let uri = options.address || artifact.uri 638 | if (ports.length > 0 && uri === undefined) uri = await promptForIngress(language, options) 639 | 640 | // Secrets will track secrets created by our dependencies which need to be written out to Kubernetes Secrets 641 | const secrets = [] 642 | 643 | // Bases is an array of Kustomization directories - this always includes our base structure and also any supported dependencies 644 | const bases = [] 645 | 646 | // Resources track resources added by this project, which will go into our base kustomization.yaml file 647 | const resources = [] 648 | 649 | // Create directory structure 650 | await mkdir(`k8s/overlays/${env}`, options) 651 | for (let i = 0; i < envConfig.length; i++) { 652 | mkdir(`k8s/base/${envConfig[i].name}`, options) 653 | bases.push(`../../base/${envConfig[i].name}`) 654 | } 655 | 656 | // Write Dockerfile based on our language 657 | if (language.dockerfile) { 658 | await confirmWriteFile( 659 | 'Dockerfile', 660 | language.dockerfile({ 661 | ...options, 662 | ...language, 663 | entrypoint, 664 | name, 665 | env 666 | }), 667 | options 668 | ) 669 | } 670 | 671 | // Write a Kubernetes Deployment object 672 | await mkdir(`k8s/base/${name}`, options) 673 | await writeDeployment(`k8s/base/${name}/deployment.yaml`, language, { 674 | ...options, 675 | name, 676 | entrypoint, 677 | ports, 678 | secrets 679 | }) 680 | resources.push('./deployment.yaml') 681 | 682 | if (ports.length > 0) { 683 | await writeService(`k8s/base/${name}/service.yaml`, name, ports, { 684 | ...options, 685 | name, 686 | env, 687 | ports 688 | }) 689 | resources.push('./service.yaml') 690 | if (uri) { 691 | await writeIngress(`k8s/base/${name}/ingress.yaml`, name, uri, ports[0], { 692 | ...options, 693 | name, 694 | env, 695 | ports 696 | }) 697 | resources.push('./ingress.yaml') 698 | } 699 | } 700 | 701 | // Write Kustomization configuration 702 | await writeKustomization(`k8s/base/${name}/kustomization.yaml`, { 703 | ...options, 704 | env, 705 | ports, 706 | resources, 707 | secrets: [] 708 | }) 709 | 710 | // Return the new, full configuration for this environment 711 | artifact = Object.assign({}, artifact, { 712 | name, 713 | uri, 714 | image: options.image, 715 | entrypoint, 716 | ports, 717 | language: language.name, 718 | languageOptions: language.detectedOptions 719 | }) 720 | if (!envConfig.find(a => a.entrypoint === artifact.entrypoint)) envConfig.push(artifact) 721 | return envConfig.map(a => { 722 | if (a.entrypoint === artifact.entrypoint) return Object.assign({}, a, artifact) 723 | else return a 724 | }) 725 | } 726 | 727 | async function init(action, env = 'production', config, options = { update: false, force: false }) { 728 | if (!config.envs) config.envs = {} 729 | if (!config.envs[env]) config.envs[env] = [] 730 | if (!validProjectNameRegex.test(env)) return fatal(`Invalid env "${env}" provided!`) 731 | let artifacts = config.envs[env] 732 | const bases = [] 733 | let secrets = [] 734 | 735 | // Container image (Note that we assume one Docker image per project, even if there are multiple entrypoints / artifacts) 736 | // Users with multi-language mono-repos probably should eject and design their own Skaffold configuration :) 737 | const image = options.image 738 | ? options.image 739 | : artifacts[0] && artifacts[0].image 740 | ? artifacts[0].image 741 | : await promptForImageName(path.basename(process.cwd())) 742 | 743 | // If create a kube config if none already exists 744 | if (options.prompts && action === 'deploy') { 745 | readLocalKubeConfig(options.config) 746 | await promptForCreateKubeContext() 747 | } 748 | 749 | // Re-generate our artifacts 750 | const numberOfArtifactsAtStart = parseInt(artifacts.length, 10) // De-reference 751 | for (let i = 0; i < artifacts.length; i++) { 752 | artifacts = await generateArtifact(env, artifacts, { ...options, ...artifacts[i], image }) 753 | } 754 | // Always generateArtifact if there are no artifacts 755 | if (numberOfArtifactsAtStart === 0) { 756 | artifacts = await generateArtifact(env, artifacts, { ...options, image }) 757 | } 758 | // If we're writing our very first artifact, or if we've explicitly called --add 759 | if (options.add) { 760 | while (await promptForAdditionalArtifacts(options)) { 761 | const newConfig = await generateArtifact(env, artifacts, { 762 | ...options, 763 | image, 764 | forceNew: true 765 | }) 766 | artifacts = artifacts.map(e => { 767 | if (newConfig.name === e.name) return newConfig 768 | return e 769 | }) 770 | } 771 | } 772 | 773 | const matchedModules = [] 774 | artifacts.forEach(async artifact => { 775 | bases.push(`../../base/${artifact.name}`) 776 | // Find service modules we support 777 | if (artifact.language.matchModules) { 778 | await artifact.language.matchModules(metaModules, options).map(mod => { 779 | if (!matchedModules.find(n => n === mod.name)) matchedModules.push(mod) 780 | }) 781 | } 782 | }) 783 | 784 | // Add explicitly chosen modules as well 785 | const chosenModules = [] 786 | .concat(config.modules || [], options.modules) 787 | .filter((v, i, s) => s.indexOf(v) === i) 788 | if (chosenModules.length) { 789 | chosenModules.forEach(mod => { 790 | const metaModule = metaModules.find(m => m.name === mod.name) 791 | if (metaModule) matchedModules.push(metaModule) 792 | }) 793 | } 794 | if (matchedModules.length > 0) 795 | debug(`Adding configuration for submodules: "${matchedModules.join(', ')}"`) 796 | 797 | // Add matched modules to our Kustomization file 798 | for (let i = 0; i < matchedModules.length; i++) { 799 | const matched = matchedModules[i] 800 | const { base, secrets: moduleSecrets } = await writeModuleConfiguration(env, matched, options) 801 | secrets = secrets.concat(moduleSecrets) 802 | bases.push(base) 803 | } 804 | 805 | config.envs[env] = artifacts 806 | 807 | // Write supporting files - note that it's very important that users ignore secrets!!! 808 | // TODO: We don't really offer any sort of solution for secrets management (git-crypt probably fits best) 809 | await writeTextLine('.gitignore', 'k8s/overlays/*/secrets/*', { 810 | ...options, 811 | append: true, 812 | dontPrune: true 813 | }) 814 | await writeTextLine('.dockerignore', `k8s\nnode_modules\n.git\ndist\nbuild`, { 815 | ...options, 816 | append: true, 817 | dontPrune: true 818 | }) 819 | await writeKustomization(`k8s/overlays/${env}/kustomization.yaml`, { 820 | ...options, 821 | env, 822 | bases, 823 | secrets 824 | }) 825 | await writeSkaffold('skaffold.yaml', config.envs, options) 826 | await confirmWriteFile('.dna.json', JSON.stringify(config, null, 2) + '\n', { 827 | ...options, 828 | update: true, 829 | force: true, 830 | dontPrune: true 831 | }) 832 | } 833 | 834 | module.exports = async function DeployNodeApp(env, action, options) { 835 | if (!env) env = 'production' 836 | if (!action) { 837 | if (env === 'dev' || env === 'development') action = 'dev' 838 | else action = 'deploy' 839 | } 840 | const skaffoldPath = await ensureBinaries(options) 841 | const config = await readDNAConfig(options) 842 | 843 | if (!options.write) process.on('beforeExit', () => cleanupWrittenFiles(options)) 844 | 845 | async function deployMessage() { 846 | log(`Deploying to ${style.red.open}${env}${style.red.close}!`) 847 | if (!options.force && !process.env.CI) await sleep(1000) // Give administrators a chance to exit! 848 | } 849 | 850 | if (action === 'init') options.write = true 851 | if (action === 'add') options.update = true 852 | await init(action, env, config, options) 853 | 854 | let SKAFFOLD_NAMESPACE = 'default' 855 | if (kubeConfig && kubeConfig['current-context'] && kubeConfig.contexts) { 856 | const { context } = kubeConfig.contexts.find(c => c.name === kubeConfig['current-context']) 857 | if (context.namespace) SKAFFOLD_NAMESPACE = context.namespace 858 | } 859 | 860 | const execOptions = { 861 | stdio: 'inherit', 862 | catchErr: true, 863 | env: Object.assign({}, process.env, { SKAFFOLD_NAMESPACE }) 864 | } 865 | 866 | if (action === 'init') { 867 | if (process.env.REPO_BUILDER_PROMPT_JSON) { 868 | log(`KUBESAIL_REPO_BUILDER_INIT_OUTPUT|${JSON.stringify(config)}`) 869 | } 870 | log('Repo initialized') 871 | process.exit(0) 872 | } else if (action === 'deploy') { 873 | await deployMessage() 874 | execSyncWithEnv(`${skaffoldPath} run --profile=${env}`, execOptions) 875 | } else if (action === 'dev') { 876 | execSyncWithEnv(`${skaffoldPath} dev --profile=${env} --port-forward`, execOptions) 877 | } else if (['build'].includes(action)) { 878 | execSyncWithEnv(`${skaffoldPath} ${action} --profile=${env}`, execOptions) 879 | } else { 880 | process.stderr.write(`No such action "${action}"!\n`) 881 | process.exit(1) 882 | } 883 | } 884 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const USAGE = '[env] [action]' 4 | 5 | const program = require('commander') 6 | const deployNodeApp = require('./deployNodeApp') 7 | const dnaPackageJson = require(__dirname + '/../package.json') // eslint-disable-line 8 | 9 | let env 10 | let action 11 | 12 | program 13 | .name('deploy-node-app') 14 | .arguments(USAGE) 15 | .usage(USAGE) 16 | .version(dnaPackageJson.version) 17 | .action((_env, _action) => { 18 | env = _env 19 | action = _action 20 | }) 21 | .option( 22 | '-w, --write', 23 | 'Write files to project (writes out Dockerfile, skaffold.yaml, etc)', 24 | false 25 | ) 26 | .option('-u, --update', 'Update existing files', false) 27 | .option('-f, --force', 'Dont prompt if possible', false) 28 | .option('-l, --label [foo=bar,tier=service]', 'Add labels to created Kubernetes resources') 29 | .option('-t, --target ', 'Target project directory', '.') 30 | .option('-c, --config ', 'Kubernetes configuration file', '~/.kube/config') 31 | .option('-m, --modules ', 'Explicitly add modules') 32 | .option('--add', 'Add an additional build target') 33 | .option('--language ', 'Override language detection') 34 | .option('--project-name ', 'Answer the project name question') 35 | .option('--entrypoint ', 'Answer the entrypoint question') 36 | .option('--image ', 'Answer the image address question') 37 | .option('--ports ', 'Answer the ports question') 38 | .option('--address
', 'Answer the ingress address question') 39 | .option( 40 | '--no-prompts', 41 | 'Use default values whenever possible, implies --update and --force', 42 | false 43 | ) 44 | .parse(process.argv) 45 | 46 | const opts = program.opts() 47 | 48 | deployNodeApp(env, action, { 49 | language: opts.language || null, 50 | action: action || 'deploy', 51 | write: opts.write || false, 52 | update: opts.update || false, 53 | force: opts.force || false, 54 | config: opts.config === '~/.kube/config' ? null : opts.config, 55 | modules: (opts.modules || '').split(',').filter(Boolean), 56 | add: opts.add || false, 57 | target: opts.target || '.', 58 | labels: (opts.label || '') 59 | .split(',') 60 | .map(k => k.split('=').filter(Boolean)) 61 | .filter(Boolean), 62 | name: opts.projectName, 63 | entrypoint: opts.entrypoint || false, 64 | image: opts.image || false, 65 | ports: opts.ports 66 | ? opts.ports 67 | .split(',') 68 | .map(p => parseInt(p, 10)) 69 | .filter(Boolean) 70 | : null, 71 | address: opts.address || false, 72 | prompts: opts.prompts 73 | }) 74 | -------------------------------------------------------------------------------- /src/languages/nextjs.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const util = require('util') 3 | const path = require('path') 4 | const { writeTextLine } = require('../util') 5 | 6 | const readFile = util.promisify(fs.readFile) 7 | 8 | module.exports = { 9 | name: 'nextjs', 10 | skipHttpPrompt: true, 11 | skipEntrypointPrompt: true, 12 | suggestedPorts: [3000], 13 | 14 | detect: async function (options) { 15 | const nextConfigPath = path.join(options.target, './next.config.js') 16 | if (fs.existsSync(nextConfigPath)) { 17 | this.suggestedPorts = [3000] 18 | return true 19 | } 20 | const pkgPath = path.join(options.target, './package.json') 21 | if (fs.existsSync(pkgPath)) { 22 | try { 23 | const packageJson = JSON.parse(fs.readFileSync(pkgPath)) 24 | if (packageJson) { 25 | if (Object.keys(packageJson.dependencies).includes('next')) { 26 | return true 27 | } 28 | } 29 | } catch {} 30 | } 31 | }, 32 | 33 | entrypoint: () => 'yarn start', 34 | 35 | dockerfile: () => { 36 | return [ 37 | '# syntax=docker/dockerfile:1.3', 38 | '# Install dependencies only when needed', 39 | `FROM node:${process.versions.node.split('.')[0]} AS deps\n`, 40 | 'WORKDIR /home/node', 41 | 'COPY package.json yarn.loc[k] .npmr[c] ./', 42 | 'RUN yarn install', 43 | 44 | `FROM node:${process.versions.node.split('.')[0]} AS builder\n`, 45 | 'WORKDIR /home/node', 46 | 'ARG BUILD_ASSET_PREFIX', 47 | 'USER node', 48 | 'COPY --chown=node:node --from=deps /home/node/node_modules ./node_modules', 49 | 'COPY --chown=node:node . .', 50 | 'RUN echo "Building with asset prefix: ${BUILD_ASSET_PREFIX}" && BUILD_ASSET_PREFIX=$BUILD_ASSET_PREFIX yarn build', 51 | 52 | 'FROM builder AS dev', 53 | 'ENV NEXT_TELEMETRY_DISABLED="1" \\', 54 | ' NODE_ENV="development" \\', 55 | ' HOST="0.0.0.0" ', 56 | 'CMD ["yarn", "dev"]', 57 | 58 | `FROM node:${process.versions.node.split('.')[0]} AS runner\n`, 59 | 'WORKDIR /home/node', 60 | 'ARG BUILD_ASSET_PREFIX', 61 | 'ENV NODE_ENV="production" \\', 62 | ' NEXT_TELEMETRY_DISABLED="1" \\', 63 | ' HOST="0.0.0.0" ', 64 | 'COPY --from=builder --chown=node:node /home/node/ .', 65 | 'COPY --chown=node:node .env.loca[l] *.js *.json *.md *.lock ./', 66 | 'EXPOSE 3000', 67 | 'CMD ["node", "server.js"]' 68 | ].join('\n') 69 | }, 70 | 71 | artifact: (env, image) => { 72 | return { 73 | image, 74 | sync: {}, 75 | docker: { buildArgs: { ENV: env } } 76 | } 77 | }, 78 | 79 | matchModules: async function (modules, options) { 80 | let packageJson = {} 81 | try { 82 | packageJson = JSON.parse(await readFile(path.join(options.target, './package.json'))) 83 | } catch (err) {} 84 | const dependencies = Object.keys(packageJson.dependencies || []) 85 | // Don't bother loading module dependencies if we have no dependencies 86 | if (dependencies.length === 0) return [] 87 | const matchedModules = [] 88 | for (let i = 0; i < dependencies.length; i++) { 89 | const dep = dependencies[i] 90 | const mod = modules.find(mod => { 91 | return mod.languages && mod.languages[this.name] && mod.languages[this.name].includes(dep) 92 | }) 93 | if (mod) matchedModules.push(mod) 94 | } 95 | return matchedModules 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/languages/nginx.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const { prompt, debug } = require('../util') 4 | 5 | module.exports = { 6 | name: 'nginx', 7 | suggestedPorts: [80], 8 | suggestedEntrypoints: ['index.html', 'index.htm', 'public/index.html'], 9 | 10 | skipEntrypointPrompt: true, 11 | skipPortPrompt: true, 12 | skipHttpPrompt: true, 13 | 14 | entrypoint: () => { 15 | return null 16 | }, 17 | 18 | detect: async options => { 19 | // Look for common node.js based frontend packages 20 | let looksLikeFrontend = false 21 | let skipEntrypointPrompt = null 22 | 23 | const commonFrontendFiles = [ 24 | './src/index.html', 25 | './public/index.html', 26 | './public/index.htm', 27 | './index.html' 28 | ] 29 | 30 | // If there is a /public folder, they may just want to deploy that (completely static site, with no build pipeline?) 31 | for (let i = 0; i < commonFrontendFiles.length; i++) { 32 | if (fs.existsSync(commonFrontendFiles[i])) { 33 | looksLikeFrontend = true 34 | skipEntrypointPrompt = path.dirname(commonFrontendFiles[i]) 35 | break 36 | } 37 | } 38 | 39 | if (looksLikeFrontend) { 40 | const { useNginx } = await prompt([ 41 | { 42 | name: 'useNginx', 43 | type: 'confirm', 44 | message: 45 | 'This project looks like it might be a static site, would you like to use nginx to serve your resources once built?' 46 | } 47 | ]) 48 | if (!useNginx) return false 49 | process.stdout.write('\n') 50 | if (fs.existsSync('./package.json')) { 51 | const packageJson = JSON.parse(fs.readFileSync('./package.json')) 52 | const useYarn = fs.existsSync('./yarn.lock') 53 | const choices = Object.keys(packageJson.scripts).map(k => 54 | useYarn ? `yarn ${k}` : `npm run ${k}` 55 | ) 56 | const chooseFile = 'Choose a file or command instead' 57 | choices.push(chooseFile) 58 | choices.push('No build command') 59 | const defaultValue = choices.includes('build') ? 'build' : choices[0] 60 | let { buildStep } = await prompt([ 61 | { 62 | name: 'buildStep', 63 | type: 'list', 64 | message: 65 | 'This repo includes a package.json, should we run a build step to compile the project?', 66 | default: defaultValue, 67 | choices 68 | } 69 | ]) 70 | if (buildStep) { 71 | buildStep = [ 72 | `WORKDIR /build`, 73 | `COPY package.json package-lock.jso[n]` + (useYarn ? ' yarn.loc[k]' : '') + ' ./', 74 | "# Note that we're going to compile our project in the next command, so we need our development dependencies!", 75 | 'ENV NODE_ENV=development', 76 | 'RUN ' + 77 | (useYarn 78 | ? 'yarn install' 79 | : fs.existsSync('package-lock.json') 80 | ? 'npm ci' 81 | : 'npm install'), 82 | `COPY . .`, 83 | 'RUN ' + 84 | buildStep + 85 | ' && \\\n' + 86 | ' rm -rf /usr/share/nginx/html && \\\n' + 87 | ' mv -n dist artifact || true && \\\n' + 88 | ' mv -n build artifact || true', 89 | '\nFROM nginx', 90 | 'COPY --from=build /build/artifact /usr/share/nginx/html' 91 | ].join('\n') 92 | } 93 | return { buildStep, skipEntrypointPrompt, image: 'node as build' } 94 | } 95 | } 96 | 97 | return false 98 | }, 99 | 100 | dockerfile: ({ entrypoint, detectedOptions = {} }) => { 101 | const { buildStep, skipEntrypointPrompt, image } = detectedOptions 102 | if (typeof skipEntrypointPrompt === 'string') entrypoint = skipEntrypointPrompt 103 | debug('Nginx generating dockerfile', { entrypoint, detectedOptions }) 104 | return ( 105 | [ 106 | `FROM ${image || 'nginx'}`, 107 | buildStep || `COPY ${path.dirname(entrypoint)} /usr/share/nginx/html` 108 | ] 109 | .filter(Boolean) 110 | .join('\n') + '\n' 111 | ) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/languages/nodejs.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const util = require('util') 3 | const path = require('path') 4 | const { writeTextLine } = require('../util') 5 | 6 | const readFile = util.promisify(fs.readFile) 7 | 8 | module.exports = { 9 | name: 'nodejs', 10 | suggestedPorts: [3000], 11 | 12 | detect: async function (options) { 13 | const pkgPath = path.join(options.target, './package.json') 14 | let looksLikeNode = false 15 | if (fs.existsSync(pkgPath)) { 16 | try { 17 | const packageJson = JSON.parse(fs.readFileSync(pkgPath)) 18 | if (packageJson) { 19 | if (packageJson.name && packageJson.version) looksLikeNode = true 20 | if (packageJson.scripts && packageJson.scripts.start === 'react-scripts start') 21 | this.suggestedPorts = [3000] 22 | } 23 | } catch {} 24 | } 25 | if (looksLikeNode) { 26 | await writeTextLine('.gitignore', 'node_modules', { ...options, append: true }) 27 | await writeTextLine('.dockerignore', 'node_modules', { ...options, append: true }) 28 | } 29 | return looksLikeNode 30 | }, 31 | 32 | entrypoint: entrypoint => { 33 | if (!entrypoint.startsWith('npm') && !entrypoint.startsWith('node')) { 34 | entrypoint = 'node ' + entrypoint 35 | } 36 | return entrypoint 37 | }, 38 | 39 | dockerfile: () => { 40 | return [ 41 | `FROM node:${process.versions.node.split('.')[0]}\n`, 42 | "# We'll install a few common requirements here - if you have no native modules, you can safely remove the following RUN command", 43 | 'RUN apt-get update && \\', 44 | ' apt-get install -yqq nginx automake build-essential curl && \\', 45 | ' rm -rf /var/lib/apt/lists/*\n', 46 | 'USER node', 47 | 'RUN mkdir /home/node/app', 48 | 'WORKDIR /home/node/app\n', 49 | 'ARG ENV=production', 50 | 'ENV NODE_ENV $ENV', 51 | 'ENV CI=true\n', 52 | 'COPY --chown=node:node package.json yarn.loc[k] .npmr[c] ./', 53 | 'RUN yarn install', 54 | 'COPY --chown=node:node . .\n', 55 | 'CMD ["node"]' 56 | ].join('\n') 57 | }, 58 | 59 | artifact: (env, image) => { 60 | return { 61 | image, 62 | sync: {}, 63 | docker: { buildArgs: { ENV: env } } 64 | } 65 | }, 66 | 67 | matchModules: async function (modules, options) { 68 | let packageJson = {} 69 | try { 70 | packageJson = JSON.parse(await readFile(path.join(options.target, './package.json'))) 71 | } catch (err) {} 72 | const dependencies = Object.keys(packageJson.dependencies || []) 73 | // Don't bother loading module dependencies if we have no dependencies 74 | if (dependencies.length === 0) return [] 75 | const matchedModules = [] 76 | for (let i = 0; i < dependencies.length; i++) { 77 | const dep = dependencies[i] 78 | const mod = modules.find(mod => { 79 | return mod.languages && mod.languages[this.name] && mod.languages[this.name].includes(dep) 80 | }) 81 | if (mod) matchedModules.push(mod) 82 | } 83 | return matchedModules 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/languages/plain-dockerfile.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const { prompt, _debug, _writeTextLine } = require('../util') 3 | 4 | module.exports = { 5 | name: 'plain-dockerfile', 6 | skipEntrypointPrompt: true, 7 | skipPortPrompt: true, 8 | skipHttpPrompt: true, 9 | 10 | detect: async () => { 11 | if (fs.existsSync('./Dockerfile')) { 12 | const { simpleDockerfile } = await prompt([ 13 | { 14 | name: 'simpleDockerfile', 15 | type: 'confirm', 16 | message: 17 | "We can't detect the language for this project, but there does appear to be a Dockerfile - would you like to build and deploy this repo anyways?" 18 | } 19 | ]) 20 | if (!simpleDockerfile) return false 21 | return true 22 | } 23 | return false 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/languages/python.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const util = require('util') 3 | const path = require('path') 4 | 5 | const readFile = util.promisify(fs.readFile) 6 | 7 | module.exports = { 8 | name: 'python', 9 | 10 | detect: options => { 11 | return fs.existsSync(path.join(options.target, 'requirements.txt')) 12 | }, 13 | 14 | dockerfile: ({ entrypoint, ports }) => { 15 | if (fs.existsSync(entrypoint)) entrypoint = 'python ' + entrypoint 16 | return [ 17 | 'FROM python:3', 18 | 'WORKDIR /app', 19 | 'ARG ENV=production', 20 | 'RUN apt-get update && apt-get install -yqq inotify-tools', 21 | 'COPY requirements.txt ./', 22 | 'RUN pip install --no-cache-dir -r requirements.txt', 23 | 'COPY . .', 24 | entrypoint 25 | ? `CMD [${entrypoint 26 | .split(' ') 27 | .map(e => `"${e}"`) 28 | .join(', ')}]` 29 | : '' 30 | ].join('\n') 31 | }, 32 | 33 | artifact: (env, image) => { 34 | return { 35 | image, 36 | sync: {}, 37 | docker: { buildArgs: { ENV: env } } 38 | } 39 | }, 40 | 41 | matchModules: async function (modules, options) { 42 | const matchedModules = [] 43 | let requirementsFile = '' 44 | try { 45 | requirementsFile = ( 46 | await readFile(path.join(options.target, './requirements.txt')) 47 | ).toString() 48 | } catch (err) {} 49 | const dependencies = requirementsFile.split('\n') 50 | for (let i = 0; i < dependencies.length; i++) { 51 | const dep = dependencies[i].split(' ')[0] 52 | const mod = modules.find(mod => { 53 | return mod.languages && mod.languages[this.name] && mod.languages[this.name].includes(dep) 54 | }) 55 | if (mod) matchedModules.push(mod) 56 | } 57 | return matchedModules 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/languages/ruby.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | module.exports = { 5 | name: 'ruby', 6 | 7 | dockerfile: ({ entrypoint }) => 'FROM nginx\n\nCOPY . /usr/share/html/', 8 | 9 | detect: options => { 10 | return fs.existsSync(path.join(options.target, 'Gemfile')) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/modules/mongodb.js: -------------------------------------------------------------------------------- 1 | const { promptUserForValue, generateRandomStr } = require('../util') 2 | 3 | module.exports = { 4 | name: 'mongodb', 5 | image: 'mongo:latest', 6 | languages: { 7 | nodejs: ['mongodb', 'mongoose'], 8 | python: ['pymongo'], 9 | php: ['mongodb'], 10 | ruby: ['mongo'] 11 | }, 12 | ports: [27017], 13 | envs: { 14 | MONGO_INITDB_ROOT_USERNAME: promptUserForValue('MONGO_INITDB_ROOT_USERNAME', { 15 | defaultToProjectName: true 16 | }), 17 | MONGO_INITDB_ROOT_PASSWORD: generateRandomStr(), 18 | MONGO_INITDB_DATABASE: promptUserForValue('MONGO_INITDB_DATABASE', { 19 | defaultToProjectName: true 20 | }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/modules/postgres.js: -------------------------------------------------------------------------------- 1 | const { generateRandomStr, promptUserForValue } = require('../util') 2 | 3 | module.exports = { 4 | name: 'postgres', 5 | image: 'postgres:latest', 6 | languages: { 7 | nodejs: ['pg'], 8 | python: ['psycopg2'], 9 | php: ['pdo-pgsql'], // Note that practically all PHP installations will have PDO installed! 10 | ruby: ['pg'] 11 | }, 12 | ports: [5432], 13 | envs: { 14 | POSTGRES_USER: generateRandomStr(5), 15 | POSTGRES_DB: promptUserForValue('POSTGRES_DB', { defaultToProjectName: true }), 16 | POSTGRES_PASSWORD: generateRandomStr(), 17 | POSTGRES_PORT: 5432 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/redis.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'redis', 3 | image: 'redis:latest', 4 | languages: { 5 | nodejs: ['redis', 'ioredis', 'redis-streams-aggregator'], 6 | python: ['redis'], 7 | php: ['phpredis'], 8 | ruby: ['redis'] 9 | }, 10 | ports: [6379] 11 | } 12 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const os = require('os') 3 | const path = require('path') 4 | const readline = require('readline') 5 | // eslint-disable-next-line security/detect-child-process 6 | const execSync = require('child_process').execSync 7 | const util = require('util') 8 | const crypto = require('crypto') 9 | const stream = require('stream') 10 | const chalk = require('chalk') 11 | const diff = require('diff') 12 | const mkdirp = require('mkdirp') 13 | const inquirer = require('inquirer') 14 | const style = require('ansi-styles') 15 | const got = require('got') 16 | 17 | const pipeline = util.promisify(stream.pipeline) 18 | const readFile = util.promisify(fs.readFile) 19 | const writeFile = util.promisify(fs.writeFile) 20 | const ERR_ARROWS = `${style.red.open}>>${style.red.close}` 21 | 22 | // Tracks files written to during this process 23 | const filesWritten = [] 24 | const dirsWritten = [] 25 | 26 | function debug() { 27 | if (!process.env.DNA_DEBUG) return 28 | console.log(...arguments) // eslint-disable-line no-console 29 | } 30 | 31 | function log() { 32 | console.log(...arguments) // eslint-disable-line no-console 33 | } 34 | 35 | // Fatal is like log, but exits the process 36 | function fatal(message /*: string */) { 37 | process.stderr.write(`${ERR_ARROWS} ${message}\n`) 38 | process.exit(1) 39 | } 40 | 41 | // Diffs two strings prettily to stdout 42 | function tryDiff(content /*: string */, existingData /*: string */) { 43 | const compare = diff.diffLines(existingData, content) 44 | compare.forEach(part => 45 | process.stdout.write( 46 | part.added ? chalk.green(part.value) : part.removed ? chalk.red(part.value) : part.value 47 | ) 48 | ) 49 | } 50 | 51 | // A wrapper around prompt() 52 | function prompt(options) { 53 | return new Promise((resolve, reject) => { 54 | if (process.env.REPO_BUILDER_PROMPTS) { 55 | process.stdout.write('KUBESAIL_REPO_BUILDER_PROMPTS\n') 56 | const timeout = setTimeout(() => { 57 | log( 58 | 'Repo build timeout. Running with KUBESAIL_REPO_BUILDER_PROMPTS, questions must be answered in a separate process and this process resumed via SIGCONT.' 59 | ) 60 | process.exit(0) 61 | }, 30 * 60 * 1000) 62 | process.on('SIGCONT', () => { 63 | log('Prompts completed. Starting build...') 64 | clearTimeout(timeout) 65 | }) 66 | } else if (process.env.REPO_BUILDER_PROMPT_JSON) { 67 | let question = options 68 | if (Array.isArray(options)) { 69 | question = options[0] 70 | } 71 | log(`KUBESAIL_REPO_BUILDER_PROMPT_JSON|${JSON.stringify(question)}`) 72 | const rl = readline.createInterface({ input: process.stdin, output: process.stdout }) 73 | rl.on('line', line => { 74 | resolve({ [question.name]: line }) 75 | }) 76 | } else { 77 | resolve(inquirer.prompt(options)) 78 | } 79 | }) 80 | } 81 | 82 | let warnedAboutUnwrittenChanges = false 83 | function warnAboutUnwrittenChanges() { 84 | if (warnedAboutUnwrittenChanges) return 85 | warnedAboutUnwrittenChanges = true 86 | log( 87 | `${style.yellow.open}Warning!${style.yellow.close} Some changes were not written to disk. By default we try not to change anything! Obviously, this doesn\'t always work! Use "deploy-node-app --update" to write out changes.` 88 | ) 89 | } 90 | 91 | // Writes a file unless it already exists, then properly handles that 92 | // Can also diff before writing! 93 | async function confirmWriteFile(filePath, content, options = { update: false, force: false }) { 94 | const { update, force } = options 95 | const fullPath = path.join(options.target, filePath) 96 | const exists = fs.existsSync(fullPath) 97 | let doWrite = !exists 98 | if (!update && exists) { 99 | warnAboutUnwrittenChanges() 100 | return false 101 | } else if (exists && update && !force) { 102 | const existingData = (await readFile(fullPath)).toString() 103 | if (content === existingData) return false 104 | 105 | const YES_TEXT = 'Yes (update)' 106 | const NO_TEXT = 'No, dont touch' 107 | const SHOWDIFF_TEXT = 'Show diff' 108 | process.stdout.write('\n') 109 | const confirmUpdate = ( 110 | await prompt({ 111 | name: 'update', 112 | type: 'expand', 113 | message: `Would you like to update "${filePath}"?`, 114 | choices: [ 115 | { key: 'Y', value: YES_TEXT }, 116 | { key: 'N', value: NO_TEXT }, 117 | { key: 'D', value: SHOWDIFF_TEXT } 118 | ], 119 | default: 0 120 | }) 121 | ).update 122 | if (confirmUpdate === YES_TEXT) doWrite = true 123 | else if (confirmUpdate === SHOWDIFF_TEXT) { 124 | tryDiff(content, existingData) 125 | await confirmWriteFile(filePath, content, options) 126 | } 127 | } else if (force) { 128 | doWrite = true 129 | } 130 | 131 | if (doWrite) { 132 | try { 133 | // Don't document writes to existing files - ie: never delete a users files! 134 | if (!options.dontPrune && !fs.existsSync(fullPath)) filesWritten.push(fullPath) 135 | debug(`Writing ${fullPath}`) 136 | await writeFile(fullPath, content) 137 | } catch (err) { 138 | fatal(`Error writing ${filePath}: ${err.message}`) 139 | } 140 | return true 141 | } 142 | warnAboutUnwrittenChanges() 143 | } 144 | 145 | const mkdir = async (filePath, options) => { 146 | const fullPath = path.join(options.target, filePath) 147 | const created = await mkdirp(fullPath) 148 | if (created) { 149 | const dirParts = filePath.replace('./', '').split('/') 150 | if (!options.dontPrune) { 151 | for (let i = dirParts.length; i > 0; i--) { 152 | dirsWritten.push(path.join(options.target, dirParts.slice(0, i).join(path.sep))) 153 | } 154 | } 155 | } 156 | return created 157 | } 158 | 159 | // Cleans up files written by confirmWriteFile and directories written by mkdir 160 | // Does not delete non-empty directories! 161 | const cleanupWrittenFiles = options => { 162 | if (options.write) return 163 | filesWritten.forEach(file => { 164 | debug(`Removing file "${file}"`) 165 | fs.unlinkSync(file) 166 | }) 167 | const dirsToRemove = dirsWritten.filter((v, i, s) => s.indexOf(v) === i) 168 | for (let i = 0; i < dirsToRemove.length; i++) { 169 | const dir = dirsToRemove[i] 170 | const dirParts = dir.replace('./', '').split(path.sep) 171 | for (let i = dirParts.length; i >= 0; i--) { 172 | const dirPart = dirParts.slice(0, i).join(path.sep) 173 | if (!dirPart) continue 174 | else if (fs.existsSync(dirPart) && fs.readdirSync(dirPart).length === 0) { 175 | debug(`Removing directory "${dirPart}"`) 176 | fs.rmdirSync(dirPart) 177 | } else break 178 | } 179 | } 180 | } 181 | 182 | // Runs a shell command with our "process.env" - allows passing environment variables to skaffold, for example. 183 | const execSyncWithEnv = (cmd, options = {}) => { 184 | const mergedOpts = Object.assign({ catchErr: true }, options, { 185 | stdio: options.stdio || 'pipe', 186 | cwd: process.cwd(), 187 | env: process.env 188 | }) 189 | cmd = cmd.replace(/^\.\//, process.cwd() + path.sep) 190 | debug(`execSyncWithEnv: ${cmd}`) 191 | let output 192 | try { 193 | output = execSync(cmd, mergedOpts) 194 | } catch (err) { 195 | if (mergedOpts.catchErr) { 196 | return false 197 | } else { 198 | throw err 199 | } 200 | } 201 | if (output) return output.toString().trim() 202 | } 203 | 204 | // Ensures other applications are installed (eg: skaffold) 205 | async function ensureBinaries(options) { 206 | // Check for skaffold and download it if it does not exist 207 | const nodeModulesPath = `${options.target}/node_modules/.bin` 208 | await mkdirp(nodeModulesPath) 209 | const skaffoldVersion = 'v2.5.1' 210 | const skaffoldDownloadPath = `${nodeModulesPath}/skaffold-${skaffoldVersion}` 211 | let skaffoldPath = process.env.SKAFFOLD_PATH || skaffoldDownloadPath 212 | 213 | let skaffoldExists = fs.existsSync(skaffoldPath) 214 | if (os.platform() === 'win32') { 215 | if (!skaffoldExists && fs.existsSync(`${skaffoldPath}.exe`)) { 216 | skaffoldExists = true 217 | skaffoldPath = `${skaffoldPath}.exe` 218 | } 219 | } 220 | 221 | if (!skaffoldExists) { 222 | let skaffoldUri = '' 223 | switch (process.platform) { 224 | case 'darwin': 225 | skaffoldUri = `https://storage.googleapis.com/skaffold/releases/${skaffoldVersion}/skaffold-darwin-amd64` 226 | break 227 | case 'linux': 228 | skaffoldUri = `https://storage.googleapis.com/skaffold/releases/${skaffoldVersion}/skaffold-linux-amd64` 229 | break 230 | case 'win32': 231 | skaffoldUri = `https://storage.googleapis.com/skaffold/releases/${skaffoldVersion}/skaffold-windows-amd64.exe` 232 | break 233 | default: 234 | return fatal( 235 | "Can't determine platform! Please download skaffold manually - see https://skaffold.dev/docs/install/" 236 | ) 237 | } 238 | if (skaffoldUri) { 239 | log(`Downloading skaffold ${skaffoldVersion} to ${nodeModulesPath}...`) 240 | await mkdir(nodeModulesPath, options) 241 | await pipeline(got.stream(skaffoldUri), fs.createWriteStream(skaffoldDownloadPath)).catch( 242 | err => { 243 | log(`Failed to download skaffold ${skaffoldVersion} to ${nodeModulesPath}!`, { 244 | error: err.message 245 | }) 246 | fs.unlinkSync(skaffoldDownloadPath) 247 | process.exit(1) 248 | } 249 | ) 250 | fs.chmodSync(skaffoldDownloadPath, 0o775) 251 | return skaffoldDownloadPath 252 | } 253 | } 254 | 255 | return skaffoldPath 256 | } 257 | 258 | function promptUserForValue( 259 | name, 260 | { message, validate, defaultValue, type = 'input', defaultToProjectName } 261 | ) { 262 | return async (existing, options) => { 263 | defaultValue = defaultValue || existing 264 | if (defaultToProjectName) defaultValue = options.name 265 | if (defaultValue && (!options.update || !options.prompts)) return defaultValue 266 | if (!message) message = `Module "${options.name}" needs a setting: ${name}` 267 | process.stdout.write('\n') 268 | const values = await prompt([{ name, type, message, validate, default: defaultValue }]) 269 | return values[name] 270 | } 271 | } 272 | 273 | function generateRandomStr(length = 16) { 274 | return (existing, _options) => { 275 | if (existing) return existing 276 | return new Promise((resolve, reject) => { 277 | crypto.randomBytes(length, function (err, buff) { 278 | if (err) throw err 279 | resolve(buff.toString('hex')) 280 | }) 281 | }) 282 | } 283 | } 284 | 285 | async function readDNAConfig(options) { 286 | let dnaConfig = {} 287 | try { 288 | dnaConfig = JSON.parse(await readFile(path.join(options.target, '.dna.json'))) 289 | } catch (_err) {} 290 | return dnaConfig 291 | } 292 | 293 | // Idempotently writes a line of text to a file 294 | async function writeTextLine(file, line, options = { update: false, force: false, append: false }) { 295 | let existingContent 296 | try { 297 | existingContent = (await readFile(path.join(options.target, file))).toString() 298 | } catch (_err) {} 299 | if ( 300 | !existingContent || 301 | (existingContent && existingContent.indexOf(line) === -1 && options.append) 302 | ) { 303 | await confirmWriteFile(file, [existingContent, line].filter(Boolean).join('\n'), options) 304 | } 305 | } 306 | 307 | module.exports = { 308 | debug, 309 | fatal, 310 | log, 311 | mkdir, 312 | prompt, 313 | cleanupWrittenFiles, 314 | generateRandomStr, 315 | ensureBinaries, 316 | confirmWriteFile, 317 | writeTextLine, 318 | execSyncWithEnv, 319 | readDNAConfig, 320 | promptUserForValue 321 | } 322 | -------------------------------------------------------------------------------- /test/create-react-app/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /test/create-react-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /test/create-react-app/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `yarn start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `yarn test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `yarn build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `yarn eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `yarn build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /test/create-react-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-react-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.5.0", 8 | "@testing-library/user-event": "^7.2.1", 9 | "react": "^16.14.0", 10 | "react-dom": "^16.14.0", 11 | "react-scripts": "3.4.4" 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "build": "react-scripts build", 16 | "test": "react-scripts test", 17 | "eject": "react-scripts eject" 18 | }, 19 | "eslintConfig": { 20 | "extends": "react-app" 21 | }, 22 | "browserslist": { 23 | "production": [ 24 | ">0.2%", 25 | "not dead", 26 | "not op_mini all" 27 | ], 28 | "development": [ 29 | "last 1 chrome version", 30 | "last 1 firefox version", 31 | "last 1 safari version" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/create-react-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kubesail/deploy-node-app/d5011157f15c08b8d7fd5e2d9dc0a5569ba82925/test/create-react-app/public/favicon.ico -------------------------------------------------------------------------------- /test/create-react-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /test/create-react-app/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kubesail/deploy-node-app/d5011157f15c08b8d7fd5e2d9dc0a5569ba82925/test/create-react-app/public/logo192.png -------------------------------------------------------------------------------- /test/create-react-app/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kubesail/deploy-node-app/d5011157f15c08b8d7fd5e2d9dc0a5569ba82925/test/create-react-app/public/logo512.png -------------------------------------------------------------------------------- /test/create-react-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /test/create-react-app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /test/create-react-app/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/create-react-app/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from './logo.svg'; 3 | import './App.css'; 4 | 5 | function App() { 6 | return ( 7 |
8 |
9 | logo 10 |

11 | Edit src/App.js and save to reload. 12 |

13 | 19 | Learn React 20 | 21 |
22 |
23 | ); 24 | } 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /test/create-react-app/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /test/create-react-app/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /test/create-react-app/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want your app to work offline and load faster, you can change 15 | // unregister() to register() below. Note this comes with some pitfalls. 16 | // Learn more about service workers: https://bit.ly/CRA-PWA 17 | serviceWorker.unregister(); 18 | -------------------------------------------------------------------------------- /test/create-react-app/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/create-react-app/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /test/create-react-app/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const { expect } = require('chai') 3 | const { execSyncWithEnv } = require('../src/util') 4 | 5 | const describe = global.describe 6 | const it = global.it 7 | const cmd = 'node ./src/index.js' 8 | const debug = process.env.DNA_DEBUG // Turns on execSyncWithEnv printouts 9 | 10 | function wroteDNAConfigProperly (path, { language, uri, image, entrypoint, ports }) { 11 | const cfg = JSON.parse(fs.readFileSync(`${path}/.dna.json`)) 12 | expect(cfg.envs.production[0]).to.be.an('Object') 13 | expect(cfg.envs.production[0].language).to.equal(language) 14 | expect(cfg.envs.production[0].uri).to.equal(uri) 15 | expect(cfg.envs.production[0].image).to.equal(image) 16 | expect(cfg.envs.production[0].entrypoint).to.equal(entrypoint) 17 | expect(cfg.envs.production[0].ports).to.have.members(ports) 18 | } 19 | 20 | function wroteYamlStructureProperly (path, name, env = 'production') { 21 | expect(fs.existsSync(`${path}/k8s/base/${name}/deployment.yaml`), 'deployment.yaml').to.equal(true) 22 | expect(fs.existsSync(`${path}/k8s/base/${name}/kustomization.yaml`), 'kustomization.yaml').to.equal(true) 23 | expect(fs.existsSync(`${path}/k8s/overlays/${env}/kustomization.yaml`), `overlays/${env}/kustomization.yaml`).to.equal(true) 24 | expect(fs.existsSync(`${path}/Dockerfile`, 'Dockerfile')).to.equal(true) 25 | expect(fs.existsSync(`${path}/skaffold.yaml`, 'skaffold.yaml')).to.equal(true) 26 | } 27 | 28 | describe('Deploy-node-app init', function () { 29 | describe('Nginx', function () { 30 | describe('Simple', function () { 31 | const path = 'test/nginx-simple' 32 | const opts = { 33 | language: 'nginx', 34 | name: 'nginx-simple', 35 | uri: 'nginx-simple.test', 36 | image: 'kubesail/nginx-simple-test', 37 | entrypoint: 'public/index.html', 38 | ports: [8000] 39 | } 40 | 41 | it('Runs init and writes out supporting config', () => { 42 | execSyncWithEnv(`${cmd} production init \ 43 | --no-prompts -t ${path} --config=kubeconfig.yaml \ 44 | --language=${opts.language} --project-name=${opts.name} --entrypoint=${opts.entrypoint} \ 45 | --ports=${opts.ports.join(',')} --address=${opts.uri} \ 46 | --image=${opts.image}`, { catchErr: false, debug, stdio: 'inherit' }) 47 | expect(fs.existsSync(`${path}/k8s`), 'k8s/').to.equal(true) 48 | expect(fs.existsSync(`${path}/Dockerfile`, 'Dockerfile')).to.equal(true) 49 | expect(fs.existsSync(`${path}/skaffold.yaml`, 'skaffold.yaml')).to.equal(true) 50 | expect(fs.existsSync(`${path}/.dna.json`, '.dna.json')).to.equal(true) 51 | expect(fs.existsSync(`${path}/.dockerignore`, '.dockerignore')).to.equal(true) 52 | expect(fs.existsSync(`${path}/.gitignore`, '.gitignore')).to.equal(true) 53 | }) 54 | 55 | it('Updates DNA Config properly', () => { 56 | wroteYamlStructureProperly(path, opts.name) 57 | wroteDNAConfigProperly(path, opts) 58 | expect(fs.existsSync(`${path}/k8s/base/${opts.name}/ingress.yaml`), 'ingress.yaml').to.equal(true) 59 | expect(fs.existsSync(`${path}/k8s/base/${opts.name}/service.yaml`), 'service.yaml').to.equal(true) 60 | }) 61 | }) 62 | }) 63 | 64 | describe('nodejs', function () { 65 | describe('simple', function () { 66 | const path = 'test/nodejs-simple' 67 | const opts = { 68 | language: 'nodejs', 69 | name: 'nodejs-simple', 70 | uri: 'nodejs-simple.test', 71 | image: 'kubesail/nodejs-simple-test', 72 | entrypoint: 'index.js', 73 | ports: [8001] 74 | } 75 | it('Runs init without exception', () => { 76 | execSyncWithEnv(`${cmd} production init \ 77 | --no-prompts -t ${path} --config=kubeconfig.yaml --update --force \ 78 | --language=${opts.language} --project-name=${opts.name} --entrypoint=${opts.entrypoint} \ 79 | --ports=${opts.ports.join(',')} --address=${opts.uri} \ 80 | --image=${opts.image}`, { catchErr: false, debug, stdio: 'inherit' }) 81 | expect(fs.existsSync(`${path}/k8s`), 'k8s/').to.equal(true) 82 | expect(fs.existsSync(`${path}/Dockerfile`, 'Dockerfile')).to.equal(true) 83 | expect(fs.existsSync(`${path}/skaffold.yaml`, 'skaffold.yaml')).to.equal(true) 84 | wroteDNAConfigProperly(path, opts) 85 | }) 86 | it('Updates DNA Config properly', () => { 87 | wroteYamlStructureProperly(path, opts.name) 88 | wroteDNAConfigProperly(path, opts) 89 | expect(fs.existsSync(`${path}/k8s/base/${opts.name}/ingress.yaml`), 'ingress.yaml').to.equal(true) 90 | expect(fs.existsSync(`${path}/k8s/base/${opts.name}/service.yaml`), 'service.yaml').to.equal(true) 91 | }) 92 | }) 93 | 94 | describe('postgres', function () { 95 | const path = 'test/nodejs-postgres' 96 | const opts = { 97 | language: 'nodejs', 98 | name: 'nodejs-postgres', 99 | uri: 'nodejs-postgres.test', 100 | image: 'kubesail/nodejs-postgres-test', 101 | entrypoint: 'index.js', 102 | ports: [8002] 103 | } 104 | it('Runs init without exception', () => { 105 | execSyncWithEnv(`${cmd} production init \ 106 | --no-prompts -t ${path} --config=kubeconfig.yaml --update --force \ 107 | --language=${opts.language} --project-name=${opts.name} --entrypoint=${opts.entrypoint} \ 108 | --ports=${opts.ports.join(',')} --address=${opts.uri} \ 109 | --image=${opts.image}`, { catchErr: false, debug, stdio: 'inherit' }) 110 | expect(fs.existsSync(`${path}/k8s`), 'k8s/').to.equal(true) 111 | expect(fs.existsSync(`${path}/Dockerfile`, 'Dockerfile')).to.equal(true) 112 | expect(fs.existsSync(`${path}/skaffold.yaml`, 'skaffold.yaml')).to.equal(true) 113 | wroteDNAConfigProperly(path, opts) 114 | }) 115 | it('Updates DNA Config properly', () => { 116 | wroteYamlStructureProperly(path, opts.name) 117 | wroteDNAConfigProperly(path, opts) 118 | expect(fs.existsSync(`${path}/k8s/base/${opts.name}/ingress.yaml`), 'ingress.yaml').to.equal(true) 119 | expect(fs.existsSync(`${path}/k8s/base/${opts.name}/service.yaml`), 'service.yaml').to.equal(true) 120 | }) 121 | }) 122 | 123 | describe('redis', function () { 124 | const path = 'test/nodejs-redis' 125 | const opts = { 126 | language: 'nodejs', 127 | name: 'nodejs-redis', 128 | uri: 'nodejs-redis.test', 129 | image: 'kubesail/nodejs-redis-test', 130 | entrypoint: 'index.js', 131 | ports: [8003] 132 | } 133 | it('Runs init without exception', () => { 134 | execSyncWithEnv(`${cmd} production init \ 135 | --no-prompts -t ${path} --config=kubeconfig.yaml --update --force \ 136 | --language=${opts.language} --project-name=${opts.name} --entrypoint=${opts.entrypoint} \ 137 | --ports=${opts.ports.join(',')} --address=${opts.uri} \ 138 | --image=${opts.image}`, { catchErr: false, debug, stdio: 'inherit' }) 139 | expect(fs.existsSync(`${path}/k8s`), 'k8s/').to.equal(true) 140 | expect(fs.existsSync(`${path}/Dockerfile`, 'Dockerfile')).to.equal(true) 141 | expect(fs.existsSync(`${path}/skaffold.yaml`, 'skaffold.yaml')).to.equal(true) 142 | wroteDNAConfigProperly(path, opts) 143 | }) 144 | it('Updates DNA Config properly', () => { 145 | wroteYamlStructureProperly(path, opts.name) 146 | wroteDNAConfigProperly(path, opts) 147 | expect(fs.existsSync(`${path}/k8s/base/${opts.name}/ingress.yaml`), 'ingress.yaml').to.equal(true) 148 | expect(fs.existsSync(`${path}/k8s/base/${opts.name}/service.yaml`), 'service.yaml').to.equal(true) 149 | }) 150 | }) 151 | 152 | describe('mongodb', function () { 153 | const path = 'test/nodejs-mongodb' 154 | const opts = { 155 | language: 'nodejs', 156 | name: 'nodejs-mongodb', 157 | uri: 'nodejs-mongodb.test', 158 | image: 'kubesail/nodejs-mongodb-test', 159 | entrypoint: 'src/index.js', 160 | ports: [8005] 161 | } 162 | it('Runs init without exception', () => { 163 | execSyncWithEnv(`${cmd} production init \ 164 | --no-prompts -t ${path} --config=kubeconfig.yaml --update --force \ 165 | --language=${opts.language} --project-name=${opts.name} --entrypoint=${opts.entrypoint} \ 166 | --ports=${opts.ports.join(',')} --address=${opts.uri} \ 167 | --image=${opts.image}`, { catchErr: false, debug, stdio: 'inherit' }) 168 | expect(fs.existsSync(`${path}/k8s`), 'k8s/').to.equal(true) 169 | expect(fs.existsSync(`${path}/Dockerfile`, 'Dockerfile')).to.equal(true) 170 | expect(fs.existsSync(`${path}/skaffold.yaml`, 'skaffold.yaml')).to.equal(true) 171 | wroteDNAConfigProperly(path, opts) 172 | }) 173 | it('Updates DNA Config properly', () => { 174 | wroteYamlStructureProperly(path, opts.name) 175 | wroteDNAConfigProperly(path, opts) 176 | expect(fs.existsSync(`${path}/k8s/base/${opts.name}/ingress.yaml`), 'ingress.yaml').to.equal(true) 177 | expect(fs.existsSync(`${path}/k8s/base/${opts.name}/service.yaml`), 'service.yaml').to.equal(true) 178 | }) 179 | }) 180 | 181 | describe('python', function () { 182 | describe('simple', function () { 183 | const path = 'test/python-simple' 184 | const opts = { 185 | language: 'python', 186 | name: 'python-simple', 187 | uri: 'python-simple.test', 188 | image: 'kubesail/python-simple-test', 189 | entrypoint: 'server.py', 190 | ports: [8005] 191 | } 192 | it('Runs init without exception', () => { 193 | execSyncWithEnv(`${cmd} production init \ 194 | --no-prompts -t ${path} --config=kubeconfig.yaml --update --force \ 195 | --language=${opts.language} --project-name=${opts.name} --entrypoint=${opts.entrypoint} \ 196 | --ports=${opts.ports.join(',')} --address=${opts.uri} \ 197 | --image=${opts.image}`, { catchErr: false, debug, stdio: 'inherit' }) 198 | expect(fs.existsSync(`${path}/k8s`), 'k8s/').to.equal(true) 199 | expect(fs.existsSync(`${path}/Dockerfile`, 'Dockerfile')).to.equal(true) 200 | expect(fs.existsSync(`${path}/skaffold.yaml`, 'skaffold.yaml')).to.equal(true) 201 | wroteDNAConfigProperly(path, opts) 202 | }) 203 | it('Updates DNA Config properly', () => { 204 | wroteYamlStructureProperly(path, opts.name) 205 | wroteDNAConfigProperly(path, opts) 206 | expect(fs.existsSync(`${path}/k8s/base/${opts.name}/ingress.yaml`), 'ingress.yaml').to.equal(true) 207 | expect(fs.existsSync(`${path}/k8s/base/${opts.name}/service.yaml`), 'service.yaml').to.equal(true) 208 | }) 209 | }) 210 | 211 | describe('redis', function () { 212 | const path = 'test/python-redis' 213 | const opts = { 214 | language: 'python', 215 | name: 'python-redis', 216 | uri: 'python-redis.test', 217 | image: 'kubesail/python-redis-test', 218 | entrypoint: 'server.py', 219 | ports: [8005] 220 | } 221 | it('Runs init without exception', () => { 222 | execSyncWithEnv(`${cmd} production init \ 223 | --no-prompts -t ${path} --config=kubeconfig.yaml --update --force \ 224 | --language=${opts.language} --project-name=${opts.name} --entrypoint=${opts.entrypoint} \ 225 | --ports=${opts.ports.join(',')} --address=${opts.uri} \ 226 | --image=${opts.image}`, { catchErr: false, debug, stdio: 'inherit' }) 227 | expect(fs.existsSync(`${path}/k8s`), 'k8s/').to.equal(true) 228 | expect(fs.existsSync(`${path}/Dockerfile`, 'Dockerfile')).to.equal(true) 229 | expect(fs.existsSync(`${path}/skaffold.yaml`, 'skaffold.yaml')).to.equal(true) 230 | wroteDNAConfigProperly(path, opts) 231 | }) 232 | it('Updates DNA Config properly', () => { 233 | wroteYamlStructureProperly(path, opts.name) 234 | wroteDNAConfigProperly(path, opts) 235 | expect(fs.existsSync(`${path}/k8s/base/${opts.name}/ingress.yaml`), 'ingress.yaml').to.equal(true) 236 | expect(fs.existsSync(`${path}/k8s/base/${opts.name}/service.yaml`), 'service.yaml').to.equal(true) 237 | }) 238 | }) 239 | }) 240 | }) 241 | 242 | describe('ruby', function () { 243 | describe('simple', function () { 244 | const path = 'test/ruby-simple' 245 | const opts = { 246 | language: 'ruby', 247 | name: 'ruby-simple', 248 | uri: 'ruby-simple.test', 249 | image: 'kubesail/ruby-simple-test', 250 | entrypoint: 'index.rb', 251 | ports: [8080] 252 | } 253 | it('Runs init without exception', () => { 254 | execSyncWithEnv(`${cmd} production init \ 255 | --no-prompts -t ${path} --config=kubeconfig.yaml --update --force \ 256 | --language=${opts.language} --project-name=${opts.name} --entrypoint=${opts.entrypoint} \ 257 | --ports=${opts.ports.join(',')} --address=${opts.uri} \ 258 | --image=${opts.image}`, { catchErr: false, debug, stdio: 'inherit' }) 259 | expect(fs.existsSync(`${path}/k8s`), 'k8s/').to.equal(true) 260 | expect(fs.existsSync(`${path}/Dockerfile`, 'Dockerfile')).to.equal(true) 261 | expect(fs.existsSync(`${path}/skaffold.yaml`, 'skaffold.yaml')).to.equal(true) 262 | wroteDNAConfigProperly(path, opts) 263 | }) 264 | it('Updates DNA Config properly', () => { 265 | wroteYamlStructureProperly(path, opts.name) 266 | wroteDNAConfigProperly(path, opts) 267 | expect(fs.existsSync(`${path}/k8s/base/${opts.name}/ingress.yaml`), 'ingress.yaml').to.equal(true) 268 | expect(fs.existsSync(`${path}/k8s/base/${opts.name}/service.yaml`), 'service.yaml').to.equal(true) 269 | }) 270 | }) 271 | 272 | describe('redis', function () { 273 | const path = 'test/ruby-redis' 274 | const opts = { 275 | language: 'ruby', 276 | name: 'ruby-redis', 277 | uri: 'ruby-redis.test', 278 | image: 'kubesail/ruby-redis-test', 279 | entrypoint: 'app/index.rb', 280 | ports: [8080] 281 | } 282 | it('Runs init without exception', () => { 283 | execSyncWithEnv(`${cmd} production init \ 284 | --no-prompts -t ${path} --config=kubeconfig.yaml --update --force \ 285 | --language=${opts.language} --project-name=${opts.name} --entrypoint=${opts.entrypoint} \ 286 | --ports=${opts.ports.join(',')} --address=${opts.uri} \ 287 | --image=${opts.image}`, { catchErr: false, debug, stdio: 'inherit' }) 288 | expect(fs.existsSync(`${path}/k8s`), 'k8s/').to.equal(true) 289 | expect(fs.existsSync(`${path}/Dockerfile`, 'Dockerfile')).to.equal(true) 290 | expect(fs.existsSync(`${path}/skaffold.yaml`, 'skaffold.yaml')).to.equal(true) 291 | wroteDNAConfigProperly(path, opts) 292 | }) 293 | it('Updates DNA Config properly', () => { 294 | wroteYamlStructureProperly(path, opts.name) 295 | wroteDNAConfigProperly(path, opts) 296 | expect(fs.existsSync(`${path}/k8s/base/${opts.name}/ingress.yaml`), 'ingress.yaml').to.equal(true) 297 | expect(fs.existsSync(`${path}/k8s/base/${opts.name}/service.yaml`), 'service.yaml').to.equal(true) 298 | }) 299 | }) 300 | }) 301 | }) 302 | -------------------------------------------------------------------------------- /test/nginx-simple/README.md: -------------------------------------------------------------------------------- 1 | # Simple nginx example 2 | 3 | `deploy-node-app` should "just work" when targeting a directory full of static HTML! 4 | 5 | Note that live development works by default here, do `deploy-node-app dev` means zero-configuration remote development on static sites with Kubernetes! 6 | 7 | Take a look at [the Nginx driver](https://github.com/kubesail/deploy-node-app/tree/master/src/languages/nginx.js) to learn a bit more! 8 | 9 | -------------------------------------------------------------------------------- /test/nginx-simple/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Simple Nginx Example! 4 | 5 | 6 | -------------------------------------------------------------------------------- /test/nodejs-hotreload/README.md: -------------------------------------------------------------------------------- 1 | # Node.js start-on-change example 2 | 3 | `deploy-node-app`'s default skaffold setup will syncronize files from the project directory into the remote container, but it's up to the container to do things with those changes! In otherwords, we do not _restart_ the container on changes, we just sync the files. 4 | 5 | Take a look at the "package.json" file in this directory: 6 | 7 | - We've installed `nodemon`, a tool that will restart our process when files change 8 | - We've defined a "development" environment which has a different "entrypoint": "npm run development" 9 | - In our "package.json", we've setup 'development' to mean nodemon! 10 | 11 | This means `deploy-node-app development` features live reloading, and there was no container specific knowledge required other than defining a custom "entrypoint"! 12 | -------------------------------------------------------------------------------- /test/nodejs-hotreload/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-hotreload", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "production": "node src/index.js", 8 | "development": "nodemon src/index.js" 9 | }, 10 | "deploy-node-app": { 11 | "language": "nodejs", 12 | "ports": [ 13 | 8000 14 | ], 15 | "envs": { 16 | "development": { 17 | "uri": "", 18 | "image": "example/hotreload", 19 | "entrypoint": "npm run development" 20 | } 21 | } 22 | }, 23 | "dependencies": { 24 | "express": "^4.17.1" 25 | }, 26 | "devDependencies": { 27 | "nodemon": "^2.0.3" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/nodejs-hotreload/src/index.js: -------------------------------------------------------------------------------- 1 | 2 | const express = require('express') 3 | const app = express() 4 | 5 | app.get('/', (_req, res) => res.send('Hello World!')) 6 | 7 | app.listen(8000, () => process.stdout.write('A simple Node.js example app!\n')) 8 | -------------------------------------------------------------------------------- /test/nodejs-mongodb/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-mongodb", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "express": "^4.17.1", 8 | "mongodb": "^3.5.6" 9 | }, 10 | "scripts": { 11 | "dev": "nodemon src/index.js" 12 | }, 13 | "deploy-node-app": { 14 | "language": "nodejs", 15 | "ports": [ 16 | 8000 17 | ], 18 | "envs": { 19 | "dev": { 20 | "uri": "", 21 | "image": "seand/nodejs-mongodb", 22 | "entrypoint": "npm run dev" 23 | } 24 | } 25 | }, 26 | "devDependencies": { 27 | "nodemon": "^2.0.3" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/nodejs-mongodb/src/index.js: -------------------------------------------------------------------------------- 1 | 2 | // Simple mongodb powered webserver example! 3 | 4 | const express = require('express') 5 | const app = express() 6 | const MongoClient = require('mongodb').MongoClient 7 | 8 | // Connection URL 9 | const username = process.env.MONGO_INITDB_ROOT_USERNAME 10 | const password = process.env.MONGO_INITDB_ROOT_PASSWORD 11 | const database = process.env.MONGO_INITDB_DATABASE 12 | const url = `mongodb://${username}:${password}@mongodb:27017` 13 | 14 | MongoClient.connect(url, { useUnifiedTopology: true }, function (err, client) { 15 | if (err) { 16 | console.error('Failed to connect to MongoDB! Retrying...', err) 17 | return setTimeout(() => { process.exit(2) }, 3000) 18 | } 19 | const db = client.db(database) 20 | const hitcounter = db.collection('hitcounter') 21 | 22 | // Simple mongo powered view-counter to show mongodb is working properly! 23 | app.get('/', (_req, res) => { 24 | hitcounter.findOneAndUpdate( 25 | { page: '/' }, 26 | { $inc: { views: 1 } }, 27 | { upsert: true }, 28 | function (err, doc) { 29 | if (err) throw err 30 | const views = doc.value ? doc.value.views : 0 31 | const message = `Hello World from MongoDB! View count: ${views + 1}\n` 32 | process.stdout.write(message) 33 | res.send(message) 34 | }) 35 | }) 36 | app.listen(8000, () => process.stdout.write('A simple Node.js example app with MongoDB!\n')) 37 | }) 38 | -------------------------------------------------------------------------------- /test/nodejs-postgres/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dna-postgres-test-app", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "pg": "^7.18.2", 8 | "express": "^4.17.1" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/nodejs-postgres/src/index.js: -------------------------------------------------------------------------------- 1 | 2 | const express = require('express') 3 | const { Client } = require('pg') 4 | 5 | const client = new Client({ 6 | user: process.env.POSTGRES_USER, 7 | host: 'postgres', 8 | database: process.env.POSTGRES_DB, 9 | password: process.env.POSTGRES_PASSWORD, 10 | port: process.env.POSTGRES_PORT 11 | }) 12 | const app = express() 13 | 14 | client.connect().then(() => { 15 | app.listen(8000, () => process.stdout.write('A simple Node.js example app with Postgres!!\n')) 16 | }).catch(err => { 17 | console.error('Failed to connect to postgres! Retrying...', err.code) 18 | setTimeout(() => { process.exit(2) }, 3000) 19 | }) 20 | 21 | app.get('/', (_req, res) => { 22 | client.query('SELECT $1::text as message', ['Hello world!']).then(response => { 23 | process.stdout.write('GET /\n') 24 | res.send(`Hello World from Postgres: ${response.rows[0].message}`) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /test/nodejs-redis/index.js: -------------------------------------------------------------------------------- 1 | 2 | const express = require('express') 3 | const app = express() 4 | 5 | const Redis = require('redis') 6 | const redis = Redis.createClient({ host: 'redis' }) 7 | 8 | app.get('/', (_req, res) => { 9 | redis.get('nodejs-counter', (err, reply) => { 10 | if (err) { 11 | console.error('Failed to connect to Redis!', err.code) 12 | return res.sendStatus(500) 13 | } 14 | res.send(`Hello World from redis! Hit count: "${reply || 0}"`) 15 | redis.incr('nodejs-counter') 16 | }) 17 | }) 18 | 19 | app.listen(8000, () => process.stdout.write('A simple Node.js example app with Redis!\n')) 20 | -------------------------------------------------------------------------------- /test/nodejs-redis/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dna-redis-test-app", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "redis": "^3.0.2", 8 | "express": "^4.17.1" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/nodejs-simple/index.js: -------------------------------------------------------------------------------- 1 | 2 | const express = require('express') 3 | const app = express() 4 | 5 | app.get('/', (_req, res) => res.send('Hello World!')) 6 | 7 | app.listen(8000, () => process.stdout.write('A simple Node.js example app!\n')) 8 | -------------------------------------------------------------------------------- /test/nodejs-simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "express": "^4.17.1" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/python-redis/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sigint_handler() 4 | { 5 | kill $PID 6 | exit 7 | } 8 | 9 | trap sigint_handler SIGINT 10 | 11 | while true; do 12 | $@ & 13 | PID=$! 14 | inotifywait -e modify -e move -e create -e delete -e attrib -r `pwd` 15 | kill $PID 16 | done 17 | -------------------------------------------------------------------------------- /test/python-redis/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "deploy-node-app": { 3 | "language": "python", 4 | "ports": [ 5 | 8000 6 | ], 7 | "envs": { 8 | "dev": { 9 | "uri": "", 10 | "image": "example/python-redis", 11 | "entrypoint": "./init.sh server.py" 12 | } 13 | } 14 | }, 15 | "name": "python-redis" 16 | } 17 | -------------------------------------------------------------------------------- /test/python-redis/requirements.txt: -------------------------------------------------------------------------------- 1 | redis 2 | -------------------------------------------------------------------------------- /test/python-redis/server.py: -------------------------------------------------------------------------------- 1 | from http.server import BaseHTTPRequestHandler, HTTPServer 2 | import time 3 | import redis 4 | 5 | db = redis.Redis( 6 | host='redis', 7 | port=6379) 8 | 9 | class RedisExample(BaseHTTPRequestHandler): 10 | def do_GET(self): 11 | self.send_response(200) 12 | self.send_header("Content-type", "text/html") 13 | self.end_headers() 14 | hitcounter = db.get('python-hitcounter') 15 | self.wfile.write(bytes("Hello from a Python Redis example! Hits: {hitcounter}", "utf-8")) 16 | hitcounter = db.incr('python-hitcounter') 17 | 18 | if __name__ == "__main__": 19 | webServer = HTTPServer(('0.0.0.0', 8000), RedisExample) 20 | print("Python redis example started!") 21 | 22 | try: 23 | webServer.serve_forever() 24 | except KeyboardInterrupt: 25 | pass 26 | 27 | webServer.server_close() 28 | print("Server stopped.") 29 | -------------------------------------------------------------------------------- /test/python-simple/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kubesail/deploy-node-app/d5011157f15c08b8d7fd5e2d9dc0a5569ba82925/test/python-simple/requirements.txt -------------------------------------------------------------------------------- /test/python-simple/server.py: -------------------------------------------------------------------------------- 1 | from http.server import BaseHTTPRequestHandler, HTTPServer 2 | import time 3 | 4 | class SimpleExample(BaseHTTPRequestHandler): 5 | def do_GET(self): 6 | self.send_response(200) 7 | self.send_header("Content-type", "text/html") 8 | self.end_headers() 9 | self.wfile.write(bytes("Hello from a Python example!", "utf-8")) 10 | 11 | if __name__ == "__main__": 12 | webServer = HTTPServer(('0.0.0.0', 8080), SimpleExample) 13 | print("Python example started!") 14 | 15 | try: 16 | webServer.serve_forever() 17 | except KeyboardInterrupt: 18 | pass 19 | 20 | webServer.server_close() 21 | print("Server stopped.") 22 | -------------------------------------------------------------------------------- /test/ruby-redis/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'redis', '< 4' 3 | gem 'rack' 4 | -------------------------------------------------------------------------------- /test/ruby-redis/app/index.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | require 'time' 3 | require 'rack' 4 | require 'rack/utils' 5 | 6 | # app = Rack::Lobster.new 7 | server = TCPServer.open('0.0.0.0', 9000) 8 | 9 | app = Proc.new do |env| 10 | req = Rack::Request.new(env) 11 | case req.path 12 | when "/" 13 | body = "Hello world from a Ruby webserver!!" 14 | [200, {'Content-Type' => 'text/html', "Content-Length" => body.length.to_s}, [body]] 15 | else 16 | [404, {"Content-Type" => "text/html"}, ["Ah!!!"]] 17 | end 18 | end 19 | 20 | while connection = server.accept 21 | request = connection.gets 22 | method, full_path = request.split(' ') 23 | path = full_path.split('?') 24 | 25 | status, headers, body = app.call({ 26 | 'REQUEST_METHOD' => method, 27 | 'PATH_INFO' => path 28 | }) 29 | 30 | head = "HTTP/1.1 200\r\n" \ 31 | "Date: #{Time.now.httpdate}\r\n" \ 32 | "Status: #{Rack::Utils::HTTP_STATUS_CODES[status]}\r\n" 33 | 34 | headers.each do |k,v| 35 | head << "#{k}: #{v}\r\n" 36 | end 37 | 38 | connection.write "#{head}\r\n" 39 | 40 | body.each do |part| 41 | connection.write part 42 | end 43 | 44 | body.close if body.respond_to?(:close) 45 | 46 | connection.close 47 | end 48 | -------------------------------------------------------------------------------- /test/ruby-simple/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'rack' 3 | -------------------------------------------------------------------------------- /test/ruby-simple/index.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | require 'time' 3 | require 'rack' 4 | require 'rack/utils' 5 | 6 | # app = Rack::Lobster.new 7 | server = TCPServer.open('0.0.0.0', 9000) 8 | 9 | app = Proc.new do |env| 10 | req = Rack::Request.new(env) 11 | case req.path 12 | when "/" 13 | body = "Hello world from a Ruby webserver!!" 14 | [200, {'Content-Type' => 'text/html', "Content-Length" => body.length.to_s}, [body]] 15 | else 16 | [404, {"Content-Type" => "text/html"}, ["Ah!!!"]] 17 | end 18 | end 19 | 20 | while connection = server.accept 21 | request = connection.gets 22 | method, full_path = request.split(' ') 23 | path = full_path.split('?') 24 | 25 | status, headers, body = app.call({ 26 | 'REQUEST_METHOD' => method, 27 | 'PATH_INFO' => path 28 | }) 29 | 30 | head = "HTTP/1.1 200\r\n" \ 31 | "Date: #{Time.now.httpdate}\r\n" \ 32 | "Status: #{Rack::Utils::HTTP_STATUS_CODES[status]}\r\n" 33 | 34 | headers.each do |k,v| 35 | head << "#{k}: #{v}\r\n" 36 | end 37 | 38 | connection.write "#{head}\r\n" 39 | 40 | body.each do |part| 41 | connection.write part 42 | end 43 | 44 | body.close if body.respond_to?(:close) 45 | 46 | connection.close 47 | end 48 | --------------------------------------------------------------------------------