├── test
├── python-simple
│ ├── requirements.txt
│ └── server.py
├── create-react-app
│ ├── .dockerignore
│ ├── public
│ │ ├── robots.txt
│ │ ├── favicon.ico
│ │ ├── logo192.png
│ │ ├── logo512.png
│ │ ├── manifest.json
│ │ └── index.html
│ ├── src
│ │ ├── setupTests.js
│ │ ├── App.test.js
│ │ ├── index.css
│ │ ├── index.js
│ │ ├── App.js
│ │ ├── App.css
│ │ ├── logo.svg
│ │ └── serviceWorker.js
│ ├── .gitignore
│ ├── package.json
│ └── README.md
├── python-redis
│ ├── requirements.txt
│ ├── init.sh
│ ├── package.json
│ └── server.py
├── ruby-simple
│ ├── Gemfile
│ └── index.rb
├── ruby-redis
│ ├── Gemfile
│ └── app
│ │ └── index.rb
├── nginx-simple
│ ├── public
│ │ └── index.html
│ └── README.md
├── nodejs-simple
│ ├── package.json
│ └── index.js
├── nodejs-redis
│ ├── package.json
│ └── index.js
├── nodejs-hotreload
│ ├── src
│ │ └── index.js
│ ├── package.json
│ └── README.md
├── nodejs-postgres
│ ├── package.json
│ └── src
│ │ └── index.js
├── nodejs-mongodb
│ ├── package.json
│ └── src
│ │ └── index.js
└── index.js
├── .gitattributes
├── .npmignore
├── docs
└── kubesail-logo.png
├── bin
├── clean.sh
├── lint.sh
└── test.sh
├── .editorconfig
├── .dockerignore
├── .gitignore
├── src
├── modules
│ ├── redis.js
│ ├── postgres.js
│ └── mongodb.js
├── languages
│ ├── ruby.js
│ ├── plain-dockerfile.js
│ ├── python.js
│ ├── nodejs.js
│ ├── nextjs.js
│ └── nginx.js
├── index.js
├── util.js
└── deployNodeApp.js
├── .circleci
└── config.yml
├── Dockerfile
├── LICENSE
├── .eslintrc.json
├── package.json
└── README.md
/test/python-simple/requirements.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/create-react-app/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/test/python-redis/requirements.txt:
--------------------------------------------------------------------------------
1 | redis
2 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | yarn.lock binary
2 | *.secret binary
3 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | test
3 | docs
4 | Dockerfile
--------------------------------------------------------------------------------
/test/ruby-simple/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 | gem 'rack'
3 |
--------------------------------------------------------------------------------
/test/ruby-redis/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 | gem 'redis', '< 4'
3 | gem 'rack'
4 |
--------------------------------------------------------------------------------
/docs/kubesail-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kubesail/deploy-node-app/HEAD/docs/kubesail-logo.png
--------------------------------------------------------------------------------
/bin/clean.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | git clean -xdf test/ > /dev/null
4 | git checkout -- test/*/package.json
5 |
--------------------------------------------------------------------------------
/test/create-react-app/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/test/nginx-simple/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Simple Nginx Example!
4 |
5 |
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/test/create-react-app/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kubesail/deploy-node-app/HEAD/test/create-react-app/public/favicon.ico
--------------------------------------------------------------------------------
/test/create-react-app/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kubesail/deploy-node-app/HEAD/test/create-react-app/public/logo192.png
--------------------------------------------------------------------------------
/test/create-react-app/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kubesail/deploy-node-app/HEAD/test/create-react-app/public/logo512.png
--------------------------------------------------------------------------------
/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/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-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-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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
23 | );
24 | }
25 |
26 | export default App;
27 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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-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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/test/create-react-app/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # **deploy-node-app**
2 |
3 | [](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 | 
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 | [
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------