├── .circleci └── config.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github └── dependabot.yml ├── .gitignore ├── .mocharc.json ├── LICENSE ├── README.md ├── bin ├── dev ├── dev.cmd ├── run └── run.cmd ├── cloudy-init-example.gif ├── package.json ├── src ├── commands │ ├── destroy.ts │ ├── doctor.ts │ ├── export.ts │ ├── import.ts │ ├── init.ts │ ├── preview.ts │ └── up.ts ├── constants.ts └── index.ts ├── test └── tsconfig.json ├── tsconfig.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2.1 3 | 4 | orbs: 5 | release-management: salesforce/npm-release-management@4 6 | 7 | workflows: 8 | version: 2 9 | test-and-release: 10 | jobs: 11 | - release-management/test-package: 12 | matrix: 13 | parameters: 14 | os: 15 | - linux 16 | - windows 17 | node_version: 18 | - latest 19 | - lts 20 | - maintenance 21 | dependabot-automerge: 22 | triggers: 23 | - schedule: 24 | cron: '0 2,5,8,11 * * *' 25 | filters: 26 | branches: 27 | only: 28 | - main 29 | jobs: 30 | - release-management/dependabot-automerge 31 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "oclif", 4 | "oclif-typescript" 5 | ], 6 | "rules": { 7 | "unicorn/prefer-spread": [ 8 | "off" 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | versioning-strategy: increase 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | labels: 9 | - "dependencies" 10 | open-pull-requests-limit: 100 11 | pull-request-branch-name: 12 | separator: "-" 13 | ignore: 14 | - dependency-name: "fs-extra" 15 | - dependency-name: "*" 16 | update-types: ["version-update:semver-major"] 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *-debug.log 2 | *-error.log 3 | /.nyc_output 4 | /dist 5 | /lib 6 | /package-lock.json 7 | /tmp 8 | node_modules 9 | oclif.manifest.json 10 | /projects 11 | /.history 12 | .DS_Store 13 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": [ 3 | "test/helpers/init.js", 4 | "ts-node/register" 5 | ], 6 | "watch-extensions": [ 7 | "ts" 8 | ], 9 | "recursive": true, 10 | "reporter": "spec", 11 | "timeout": 60000 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Salesforce 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Cloudy 2 | ================= 3 | 4 | # Description 5 | 6 | **Cloudy** is an "infrastructure as code" tool for managing production-grade cloud clusters. It's based on [Pulumi](https://pulumi.com/) that mostly using [Terraform](https://www.terraform.io/). 7 | 8 | _Tired to manage tons of information about the cloud clusters deployment?_ 9 | 10 | _DevOps is not a primary skill in your company?_ 11 | 12 | _Your infrastructure is a mess?_ 13 | 14 | **Cloudy is your solution,** 15 | **spend minutes instead of weeks** 16 | 17 | # Features 18 | 19 | * Deploy and manage multiple cloud clusters in parallel: AWS, Azure, Google, and others 20 | * Networking, DNS, firewall, load balancer, firewall rules, and more 21 | * Nodes clustering and scaling 22 | * Cloud database and storage management 23 | * Automated backups 24 | * Incremental infrastructure updates 25 | * Supported platforms: 26 | - [AWS](https://github.com/cloudytool/pulumi-aws-cluster) 27 | - GCP (next release) 28 | 29 | # How it works 30 | 31 | **Cloudy** asks some questions about your cloud cluster and then creates a Pulumi project. The folder contains javascript files and code that define the cloud resources. Thankfully, the tool allows you to change the config file and resource structures: scale, change node types, disk size, etc... By calling `cloudy up PROJECTNAME` Pulumi deploys the cloud resources to your cloud provider and saving the state. To export the state use `cloudy export PROJECTNAME`. 32 | 33 | This approach provides maximum flexibility and less friction to start the stack fast. 34 | 35 | Save state and code to your git repository, deploy again in minutes. 36 | 37 | # Requirements 38 | 39 | Install before using: 40 | 41 | * [Git](https://git-scm.com/downloads) 42 | * [Yarn](https://classic.yarnpkg.com/lang/en/docs/install) 43 | * [Pulumi](https://www.pulumi.com/docs/get-started/install) 44 | 45 | # Usage 46 | 47 | ```sh-session 48 | $ npm install -g @cloudytool/cloudy 49 | $ cloudy COMMAND 50 | running command... 51 | $ cloudy (--version) 52 | @cloudytool/cloudy/0.0.3 darwin-x64 node-v16.0.0 53 | $ cloudy --help [COMMAND] 54 | USAGE 55 | $ cloudy COMMAND 56 | ... 57 | ``` 58 | 59 | 60 | # Quick start 61 | 62 | ```sh-session 63 | $ cloudy init aws-dev-cluster 64 | ``` 65 | 66 | ![Cloudy init example](cloudy-init-example.gif) 67 | 68 | To deploy: 69 | 70 | ```sh-session 71 | $ cloudy up aws-dev-cluster 72 | ``` 73 | 74 | To destroy after: 75 | 76 | ```sh-session 77 | $ cloudy destroy aws-dev-cluster 78 | ``` 79 | 80 | # Commands 81 | 82 | - [Cloudy](#cloudy) 83 | - [Description](#description) 84 | - [Features](#features) 85 | - [How it works](#how-it-works) 86 | - [Requirements](#requirements) 87 | - [Usage](#usage) 88 | - [Quick start](#quick-start) 89 | - [Commands](#commands) 90 | - [`cloudy destroy PROJECTNAME`](#cloudy-destroy-projectname) 91 | - [`cloudy doctor`](#cloudy-doctor) 92 | - [`cloudy export PROJECTNAME`](#cloudy-export-projectname) 93 | - [`cloudy help [COMMAND]`](#cloudy-help-command) 94 | - [`cloudy import PROJECTNAME`](#cloudy-import-projectname) 95 | - [`cloudy init PROJECTNAME`](#cloudy-init-projectname) 96 | - [`cloudy preview PROJECTNAME`](#cloudy-preview-projectname) 97 | - [`cloudy up PROJECTNAME`](#cloudy-up-projectname) 98 | 99 | ## `cloudy destroy PROJECTNAME` 100 | 101 | Destroy Pulumi project deployment 102 | 103 | ``` 104 | USAGE 105 | $ cloudy destroy [PROJECTNAME] [-r ] 106 | 107 | FLAGS 108 | -r, --root= Root path to the project 109 | 110 | DESCRIPTION 111 | Destroy Pulumi project deployment 112 | 113 | EXAMPLES 114 | $ cloudy destroy aws-cluster 115 | ``` 116 | 117 | _See code: [dist/commands/destroy.ts](https://github.com/cloudytool/cloudy/blob/v0.0.3/dist/commands/destroy.ts)_ 118 | 119 | ## `cloudy doctor` 120 | 121 | Check CLI issues 122 | 123 | ``` 124 | USAGE 125 | $ cloudy doctor 126 | 127 | DESCRIPTION 128 | Check CLI issues 129 | 130 | EXAMPLES 131 | $ cloudy doctor 132 | ``` 133 | 134 | _See code: [dist/commands/doctor.ts](https://github.com/cloudytool/cloudy/blob/v0.0.3/dist/commands/doctor.ts)_ 135 | 136 | ## `cloudy export PROJECTNAME` 137 | 138 | Export Pulumi project state 139 | 140 | ``` 141 | USAGE 142 | $ cloudy export [PROJECTNAME] [-r ] 143 | 144 | FLAGS 145 | -r, --root= Root path to the project 146 | 147 | DESCRIPTION 148 | Export Pulumi project state 149 | 150 | EXAMPLES 151 | $ cloudy export aws-cluster 152 | ``` 153 | 154 | _See code: [dist/commands/export.ts](https://github.com/cloudytool/cloudy/blob/v0.0.3/dist/commands/export.ts)_ 155 | 156 | ## `cloudy help [COMMAND]` 157 | 158 | Display help for cloudy. 159 | 160 | ``` 161 | USAGE 162 | $ cloudy help [COMMAND] [-n] 163 | 164 | ARGUMENTS 165 | COMMAND Command to show help for. 166 | 167 | FLAGS 168 | -n, --nested-commands Include all nested commands in the output. 169 | 170 | DESCRIPTION 171 | Display help for cloudy. 172 | ``` 173 | 174 | _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v5.1.12/src/commands/help.ts)_ 175 | 176 | ## `cloudy import PROJECTNAME` 177 | 178 | Import Pulumi project state 179 | 180 | ``` 181 | USAGE 182 | $ cloudy import [PROJECTNAME] [-r ] 183 | 184 | FLAGS 185 | -r, --root= Root path to the project 186 | 187 | DESCRIPTION 188 | Import Pulumi project state 189 | 190 | EXAMPLES 191 | $ cloudy import aws-cluster 192 | ``` 193 | 194 | _See code: [dist/commands/import.ts](https://github.com/cloudytool/cloudy/blob/v0.0.3/dist/commands/import.ts)_ 195 | 196 | ## `cloudy init PROJECTNAME` 197 | 198 | Initialize a new project 199 | 200 | ``` 201 | USAGE 202 | $ cloudy init [PROJECTNAME] [-r ] 203 | 204 | FLAGS 205 | -r, --root= Root path to the project 206 | 207 | DESCRIPTION 208 | Initialize a new project 209 | 210 | EXAMPLES 211 | $ cloudy init aws-cluster 212 | ``` 213 | 214 | _See code: [dist/commands/init.ts](https://github.com/cloudytool/cloudy/blob/v0.0.3/dist/commands/init.ts)_ 215 | 216 | ## `cloudy preview PROJECTNAME` 217 | 218 | Preview Pulumi project deployment update 219 | 220 | ``` 221 | USAGE 222 | $ cloudy preview [PROJECTNAME] [-r ] 223 | 224 | FLAGS 225 | -r, --root= Root path to the project 226 | 227 | DESCRIPTION 228 | Preview Pulumi project deployment update 229 | 230 | EXAMPLES 231 | $ cloudy preview aws-cluster 232 | ``` 233 | 234 | _See code: [dist/commands/preview.ts](https://github.com/cloudytool/cloudy/blob/v0.0.3/dist/commands/preview.ts)_ 235 | 236 | ## `cloudy up PROJECTNAME` 237 | 238 | Run Pulumi project deployment update 239 | 240 | ``` 241 | USAGE 242 | $ cloudy up [PROJECTNAME] [-r ] 243 | 244 | FLAGS 245 | -r, --root= Root path to the project 246 | 247 | DESCRIPTION 248 | Run Pulumi project deployment update 249 | 250 | EXAMPLES 251 | $ cloudy up aws-cluster 252 | ``` 253 | 254 | _See code: [dist/commands/up.ts](https://github.com/cloudytool/cloudy/blob/v0.0.3/dist/commands/up.ts)_ 255 | 256 | -------------------------------------------------------------------------------- /bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const oclif = require('@oclif/core') 4 | 5 | const path = require('path') 6 | const project = path.join(__dirname, '..', 'tsconfig.json') 7 | 8 | // In dev mode -> use ts-node and dev plugins 9 | process.env.NODE_ENV = 'development' 10 | 11 | require('ts-node').register({project}) 12 | 13 | // In dev mode, always show stack traces 14 | oclif.settings.debug = true; 15 | 16 | // Start the CLI 17 | oclif.run().then(oclif.flush).catch(oclif.Errors.handle) 18 | -------------------------------------------------------------------------------- /bin/dev.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\dev" %* -------------------------------------------------------------------------------- /bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const oclif = require('@oclif/core') 4 | 5 | oclif.run().then(require('@oclif/core/flush')).catch(require('@oclif/core/handle')) 6 | -------------------------------------------------------------------------------- /bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /cloudy-init-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudytool/cloudy/80003f7c2e1ac95a8cbd436581e7703d70470921/cloudy-init-example.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cloudytool/cloudy", 3 | "private": false, 4 | "version": "0.0.3", 5 | "description": "Cloudy is an \"infrastructure as code\" tool for managing production-grade cloud clusters.", 6 | "author": "Ivan Fokeev @ifokeev", 7 | "bin": { 8 | "cloudy": "./bin/run" 9 | }, 10 | "homepage": "https://github.com/cloudytool/cloudy", 11 | "license": "MIT", 12 | "main": "dist/index.js", 13 | "repository": "cloudytool/cloudy", 14 | "files": [ 15 | "/bin", 16 | "/dist", 17 | "/npm-shrinkwrap.json", 18 | "/oclif.manifest.json" 19 | ], 20 | "dependencies": { 21 | "@oclif/core": "^1", 22 | "@oclif/errors": "^1.3.5", 23 | "@oclif/plugin-help": "^5", 24 | "@oclif/plugin-not-found": "^2.3.1", 25 | "@oclif/plugin-update": "^3.0.0", 26 | "@oclif/plugin-warn-if-update-available": "^2.0.4", 27 | "cli-ux": "^6.0.9", 28 | "fs-extra": "^10.1.0", 29 | "git-clone": "^0.2.0", 30 | "which": "^2.0.2", 31 | "write-yaml-file": "^4.2.0" 32 | }, 33 | "devDependencies": { 34 | "@oclif/test": "^2", 35 | "@types/chai": "^4", 36 | "@types/fs-extra": "^9.0.13", 37 | "@types/git-clone": "^0.2.0", 38 | "@types/mocha": "^9.0.0", 39 | "@types/node": "^16.9.4", 40 | "@types/which": "^2.0.1", 41 | "chai": "^4", 42 | "eslint": "^7.32.0", 43 | "eslint-config-oclif": "^4", 44 | "eslint-config-oclif-typescript": "^1.0.2", 45 | "globby": "^11", 46 | "mocha": "^9", 47 | "oclif": "^2", 48 | "shx": "^0.3.3", 49 | "ts-node": "^10.2.1", 50 | "tslib": "^2.3.1", 51 | "typescript": "^4.4.3" 52 | }, 53 | "oclif": { 54 | "bin": "cloudy", 55 | "dirname": "cloudy", 56 | "commands": "./dist/commands", 57 | "plugins": [ 58 | "@oclif/plugin-help" 59 | ], 60 | "macos": { 61 | "identifier": "com.cloudy.cli" 62 | }, 63 | "update": { 64 | "s3": { 65 | "bucket": "cloudy-releases" 66 | } 67 | }, 68 | "topicSeparator": " " 69 | }, 70 | "scripts": { 71 | "build": "shx rm -rf dist && tsc -b", 72 | "lint": "eslint . --ext .ts --config .eslintrc", 73 | "lint:show-rules": "eslint --print-config .eslintrc", 74 | "postpack": "shx rm -f oclif.manifest.json", 75 | "posttest": "yarn lint", 76 | "prepack": "yarn build && oclif manifest && oclif readme", 77 | "test": "mocha --forbid-only \"test/**/*.test.ts\"", 78 | "version": "oclif readme && git add README.md", 79 | "push": "yarn publish --access public" 80 | }, 81 | "engines": { 82 | "node": ">=12.0.0" 83 | }, 84 | "bugs": "https://github.com/cloudytool/cloudy/issues", 85 | "keywords": [ 86 | "oclif", 87 | "pulumi", 88 | "cloud", 89 | "infrastructure", 90 | "as code", 91 | "cli", 92 | "cloudy", 93 | "cloudy lab" 94 | ], 95 | "types": "dist/index.d.ts" 96 | } 97 | -------------------------------------------------------------------------------- /src/commands/destroy.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path' 2 | import {execSync} from 'node:child_process' 3 | import {Command, Flags} from '@oclif/core' 4 | import Doctor from './doctor' 5 | 6 | import {PULUMI_CONFIG_PASSPHRASE, PULUMI_CONFIG_PASSPHRASE_FILE} from '../constants' 7 | 8 | export default class Destroy extends Command { 9 | static description = 'Destroy Pulumi project deployment' 10 | 11 | static examples = [ 12 | '<%= config.bin %> <%= command.id %> aws-cluster', 13 | ] 14 | 15 | static flags = { 16 | root: Flags.string({char: 'r', description: 'Root path to the project'}), 17 | } 18 | 19 | static args = [{ 20 | name: 'projectName', 21 | required: true, 22 | }] 23 | 24 | public async run(): Promise { 25 | const {args, flags} = await this.parse(Destroy) 26 | 27 | const cwd = process.cwd() 28 | const {projectName} = args 29 | const projectRoot = flags.root ?? cwd 30 | const projectPath = path.join(projectRoot, 'projects', projectName) 31 | 32 | await Doctor.run() 33 | 34 | const cmdName = this.constructor.name.toLowerCase() 35 | 36 | const cmd = [ 37 | `export PULUMI_CONFIG_PASSPHRASE="${PULUMI_CONFIG_PASSPHRASE}"`, 38 | `export PULUMI_CONFIG_PASSPHRASE_FILE="${PULUMI_CONFIG_PASSPHRASE_FILE}"`, 39 | `cd ${projectPath}`, 40 | 'yarn', 41 | `${process.env.SHELL} ${cmdName}.sh ${projectName}`, 42 | ].join(' && ') 43 | 44 | execSync(`(${cmd})`, {stdio: 'inherit'}) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/commands/doctor.ts: -------------------------------------------------------------------------------- 1 | import {Command} from '@oclif/core' 2 | import * as which from 'which' 3 | 4 | export default class Doctor extends Command { 5 | static description = 'Check CLI issues' 6 | 7 | static examples = [ 8 | '<%= config.bin %> <%= command.id %>', 9 | ] 10 | 11 | public async run(): Promise { 12 | const hasGit = which.sync('git', {nothrow: true}) 13 | const hasYarn = which.sync('yarn', {nothrow: true}) 14 | const hasPulumi = which.sync('pulumi', {nothrow: true}) 15 | 16 | if (!hasGit) { 17 | this.error('Please install git (https://git-scm.com/downloads)') 18 | } 19 | 20 | if (!hasYarn) { 21 | this.error('Please install yarn (https://classic.yarnpkg.com/lang/en/docs/install)') 22 | } 23 | 24 | if (!hasPulumi) { 25 | this.error('Please install pulumi (https://www.pulumi.com/docs/get-started/install)') 26 | } 27 | 28 | this.log('** Everything looks good! **') 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/commands/export.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path' 2 | import {execSync} from 'node:child_process' 3 | import {Command, Flags} from '@oclif/core' 4 | import Doctor from './doctor' 5 | 6 | import {PULUMI_CONFIG_PASSPHRASE, PULUMI_CONFIG_PASSPHRASE_FILE} from '../constants' 7 | 8 | export default class Export extends Command { 9 | static description = 'Export Pulumi project state' 10 | 11 | static examples = [ 12 | '<%= config.bin %> <%= command.id %> aws-cluster', 13 | ] 14 | 15 | static flags = { 16 | root: Flags.string({char: 'r', description: 'Root path to the project'}), 17 | } 18 | 19 | static args = [{ 20 | name: 'projectName', 21 | required: true, 22 | }] 23 | 24 | public async run(): Promise { 25 | const {args, flags} = await this.parse(Export) 26 | 27 | const cwd = process.cwd() 28 | const {projectName} = args 29 | const projectRoot = flags.root ?? cwd 30 | const projectPath = path.join(projectRoot, 'projects', projectName) 31 | 32 | await Doctor.run() 33 | 34 | const cmdName = this.constructor.name.toLowerCase() 35 | 36 | const cmd = [ 37 | `export PULUMI_CONFIG_PASSPHRASE="${PULUMI_CONFIG_PASSPHRASE}"`, 38 | `export PULUMI_CONFIG_PASSPHRASE_FILE="${PULUMI_CONFIG_PASSPHRASE_FILE}"`, 39 | `cd ${projectPath}`, 40 | 'yarn', 41 | `${process.env.SHELL} ${cmdName}.sh ${projectName}`, 42 | ].join(' && ') 43 | 44 | execSync(`(${cmd})`, {stdio: 'inherit'}) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/commands/import.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path' 2 | import {execSync} from 'node:child_process' 3 | import {Command, Flags} from '@oclif/core' 4 | import Doctor from './doctor' 5 | 6 | import {PULUMI_CONFIG_PASSPHRASE, PULUMI_CONFIG_PASSPHRASE_FILE} from '../constants' 7 | 8 | export default class Import extends Command { 9 | static description = 'Import Pulumi project state' 10 | 11 | static examples = [ 12 | '<%= config.bin %> <%= command.id %> aws-cluster', 13 | ] 14 | 15 | static flags = { 16 | root: Flags.string({char: 'r', description: 'Root path to the project'}), 17 | } 18 | 19 | static args = [{ 20 | name: 'projectName', 21 | required: true, 22 | }] 23 | 24 | public async run(): Promise { 25 | const {args, flags} = await this.parse(Import) 26 | 27 | const cwd = process.cwd() 28 | const {projectName} = args 29 | const projectRoot = flags.root ?? cwd 30 | const projectPath = path.join(projectRoot, 'projects', projectName) 31 | 32 | await Doctor.run() 33 | 34 | const cmdName = this.constructor.name.toLowerCase() 35 | 36 | const cmd = [ 37 | `export PULUMI_CONFIG_PASSPHRASE="${PULUMI_CONFIG_PASSPHRASE}"`, 38 | `export PULUMI_CONFIG_PASSPHRASE_FILE="${PULUMI_CONFIG_PASSPHRASE_FILE}"`, 39 | `cd ${projectPath}`, 40 | 'yarn', 41 | `${process.env.SHELL} ${cmdName}.sh ${projectName}`, 42 | ].join(' && ') 43 | 44 | execSync(`(${cmd})`, {stdio: 'inherit'}) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/commands/init.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path' 2 | import * as crypto from 'node:crypto' 3 | import * as fs from 'fs-extra' 4 | 5 | import {Command, Flags} from '@oclif/core' 6 | import cli from 'cli-ux' 7 | import * as writeYamlFile from 'write-yaml-file' 8 | import {execSync} from 'node:child_process' 9 | 10 | type regionZones = { 11 | [key: string]: string[] 12 | } 13 | 14 | type regionAmi = { 15 | [key: string]: string 16 | } 17 | 18 | const getAvailableZones = (region: string) => { 19 | const regionZones: regionZones = { 20 | 'us-east-1': ['us-east-1a', 'us-east-1b', 'us-east-1c', 'us-east-1d', 'us-east-1e'], 21 | 'us-east-2': ['us-east-2a', 'us-east-2b', 'us-east-2c'], 22 | 23 | 'us-west-1': ['us-west-1a', 'us-west-1b'], 24 | 'us-west-2': ['us-west-2a', 'us-west-2b', 'us-west-2c'], 25 | 26 | 'eu-west-1': ['eu-west-1a', 'eu-west-1b', 'eu-west-1c'], 27 | 'eu-central-1': ['eu-central-1a', 'eu-central-1b'], 28 | 29 | 'ap-southeast-1': ['ap-southeast-1a', 'ap-southeast-1b'], 30 | 'ap-southeast-2': ['ap-southeast-2a', 'ap-southeast-2b', 'ap-southeast-2c'], 31 | 32 | 'ap-northeast-1': ['ap-northeast-1a', 'ap-northeast-1b', 'ap-northeast-1c'], 33 | 'ap-northeast-2': ['ap-northeast-2a', 'ap-northeast-2c'], 34 | 35 | 'ap-south-1': ['ap-south-1a', 'ap-south-1b'], 36 | 37 | 'sa-east-1': ['sa-east-1a', 'sa-east-1b', 'sa-east-1c'], 38 | } 39 | 40 | return regionZones?.[region] 41 | } 42 | 43 | const getUbuntu2004Ami = (region: string) => { 44 | const regionAmi: regionAmi = { 45 | 'us-east-1': 'ami-0924c0eab44755b7a', 46 | 'us-east-2': 'ami-0f2891f9820eeec74', 47 | 48 | 'us-west-1': 'ami-01c850eb8ee4f6f48', 49 | 'us-west-2': 'ami-085ba1368c44ae288', 50 | 51 | 'eu-west-1': 'ami-0344d9b64e880a596', 52 | 'eu-west-2': 'ami-0459f75db72a4b9a7', 53 | 'eu-central-1': 'ami-07a99561151e14879', 54 | 55 | 'ap-southeast-1': 'ami-0580691dc3aeb85f4', 56 | 'ap-southeast-2': 'ami-02aeffc52375f7e34', 57 | 58 | 'ap-northeast-1': 'ami-0e4cd2f26990dfb1f', 59 | 'ap-northeast-2': 'ami-09a0f33f03e35493b', 60 | 61 | 'ap-south-1': 'ami-0ae52aabc5b26f25a', 62 | 63 | 'sa-east-1': 'ami-0b91f999dbc798855', 64 | } 65 | 66 | return regionAmi?.[region] 67 | } 68 | 69 | export default class Init extends Command { 70 | static description = 'Initialize a new project' 71 | 72 | static examples = [ 73 | '<%= config.bin %> <%= command.id %> aws-cluster', 74 | ] 75 | 76 | static flags = { 77 | root: Flags.string({char: 'r', description: 'Root path to the project'}), 78 | } 79 | 80 | static args = [{ 81 | name: 'projectName', 82 | required: true, 83 | }] 84 | 85 | public async run(): Promise { 86 | const {args, flags} = await this.parse(Init) 87 | 88 | const cwd = process.cwd() 89 | const {projectName} = args 90 | const projectRoot = flags.root ?? cwd 91 | const projectPath = path.join(projectRoot, 'projects', projectName) 92 | 93 | const pulumiConfigPath = path.join(projectPath, `Pulumi.${projectName}.yaml`) 94 | 95 | if (fs.existsSync(projectPath)) { 96 | this.error('Project already exists') 97 | } 98 | 99 | const awsProfile = await cli.prompt('AWS credentials profile', {default: 'default'}) 100 | const awsRegion = await cli.prompt('AWS region', {default: 'us-east-1'}) 101 | 102 | const awsZones = getAvailableZones(awsRegion) 103 | 104 | if (!awsZones) { 105 | this.error(`No zones found for region ${awsRegion}`) 106 | } 107 | 108 | const awsZone = await cli.prompt('AWS zone', {default: awsZones[0]}) 109 | 110 | const workersCount = await cli.prompt('Workers count', {default: '1'}) 111 | const instanceType = await cli.prompt('Worker instance type', {default: 't2.medium'}) 112 | const ebsVolumeSize = await cli.prompt('EBS volume size (GB)', {default: '70'}) 113 | 114 | let domainName = await cli.prompt('Project domain name') 115 | const lastDomainChar = domainName.charAt(domainName.length - 1) 116 | 117 | // remove last dot from the domain 118 | if (lastDomainChar === '.') { 119 | domainName = domainName.slice(0, -1) 120 | } 121 | 122 | const dbNeeded = await cli.confirm('Do you need cloud database? (yes/no)') 123 | 124 | let dbUser 125 | let dbPass 126 | let dbName 127 | 128 | let dbConfig = { 129 | 'db:allocatedStorage': 30, 130 | 'db:maxAllocatedStorage': 100, 131 | 'db:engine': 'postgres', 132 | 'db:engineVersion': 12.1, 133 | 'db:engineGroupName': 'default.postgres12', 134 | 'db:instanceClass': 'db.t3.medium', 135 | 'db:user': '', 136 | 'db:pass': '', 137 | 'db:databaseName': '', 138 | 'db:storageType': 'gp2', 139 | 'db:backupRetentionPeriod': 3, 140 | 'db:backupWindow': '03:00-04:00', 141 | 'db:deleteAutomatedBackups': false, 142 | 'db:deletionProtection': false, 143 | 'db:finalSnapshotIdentifier': `${projectName}-db-final-snapshot`, 144 | 'db:skipFinalSnapshot': true, 145 | } 146 | 147 | if (dbNeeded) { 148 | dbUser = `${projectName}user` 149 | dbPass = crypto.randomBytes(20).toString('hex') 150 | dbName = dbUser 151 | 152 | dbConfig = { 153 | ...dbConfig, 154 | 'db:user': dbUser, 155 | 'db:pass': dbPass, 156 | 'db:databaseName': dbName, 157 | } 158 | } 159 | 160 | execSync(`git clone --progress https://github.com/cloudylab-net/pulumi-aws-cluster.git ${projectPath}`, {stdio: 'inherit'}) 161 | 162 | const ami = getUbuntu2004Ami(awsRegion) 163 | 164 | const users = [{ 165 | 's3-examples': { 166 | Version: '2012-10-17', 167 | Statement: [{ 168 | Sid: 'AllObjectActions', 169 | Action: ['s3:*Object'], 170 | Effect: 'Allow', 171 | Resource: '*', 172 | }], 173 | }, 174 | }] 175 | 176 | const pulumiConfig = { 177 | encryptionsalt: 'v1:NXHcHtVQQ4M=:v1:6QZlxzHc1KMxWwiv:gYgSzNAgfFItMtLFZhGSSfsf5S5jPQ==', 178 | config: { 179 | 'cluster:workerTokenPath': '/tmp/swarm/worker_token', 180 | 'aws:profile': awsProfile, 181 | 'aws:region': awsRegion, 182 | 'ec2:projectName': projectName, 183 | 'ec2:masters': 1, 184 | 'ec2:slaves': Number.parseInt(workersCount, 10), 185 | 'ec2:default/ami': ami, 186 | 'ec2:default/machineType': instanceType, 187 | 'ec2:default/zone': awsZone, 188 | 'ec2:default/rootVolumeSize': 40, 189 | 'ec2:default/ebsVolumeSize': Number.parseInt(ebsVolumeSize, 10), 190 | 'ec2:default/ebsDeviseName': '/dev/sdb', 191 | 'app:domainName': domainName, 192 | 'iam:users': users, 193 | ...dbConfig, 194 | }, 195 | } 196 | 197 | await writeYamlFile(pulumiConfigPath, pulumiConfig) 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/commands/preview.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path' 2 | import {execSync} from 'node:child_process' 3 | import {Command, Flags} from '@oclif/core' 4 | import Doctor from './doctor' 5 | 6 | import {PULUMI_CONFIG_PASSPHRASE, PULUMI_CONFIG_PASSPHRASE_FILE} from '../constants' 7 | 8 | export default class Preview extends Command { 9 | static description = 'Preview Pulumi project deployment update' 10 | 11 | static examples = [ 12 | '<%= config.bin %> <%= command.id %> aws-cluster', 13 | ] 14 | 15 | static flags = { 16 | root: Flags.string({char: 'r', description: 'Root path to the project'}), 17 | } 18 | 19 | static args = [{ 20 | name: 'projectName', 21 | required: true, 22 | }] 23 | 24 | public async run(): Promise { 25 | const {args, flags} = await this.parse(Preview) 26 | 27 | const cwd = process.cwd() 28 | const {projectName} = args 29 | const projectRoot = flags.root ?? cwd 30 | const projectPath = path.join(projectRoot, 'projects', projectName) 31 | 32 | await Doctor.run() 33 | 34 | const cmdName = this.constructor.name.toLowerCase() 35 | 36 | const cmd = [ 37 | `export PULUMI_CONFIG_PASSPHRASE="${PULUMI_CONFIG_PASSPHRASE}"`, 38 | `export PULUMI_CONFIG_PASSPHRASE_FILE="${PULUMI_CONFIG_PASSPHRASE_FILE}"`, 39 | `cd ${projectPath}`, 40 | 'yarn', 41 | `${process.env.SHELL} ${cmdName}.sh ${projectName}`, 42 | ].join(' && ') 43 | 44 | execSync(`(${cmd})`, {stdio: 'inherit'}) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/commands/up.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path' 2 | import {execSync} from 'node:child_process' 3 | import {Command, Flags} from '@oclif/core' 4 | import Doctor from './doctor' 5 | 6 | import {PULUMI_CONFIG_PASSPHRASE, PULUMI_CONFIG_PASSPHRASE_FILE} from '../constants' 7 | 8 | export default class Up extends Command { 9 | static description = 'Run Pulumi project deployment update' 10 | 11 | static examples = [ 12 | '<%= config.bin %> <%= command.id %> aws-cluster', 13 | ] 14 | 15 | static flags = { 16 | root: Flags.string({char: 'r', description: 'Root path to the project'}), 17 | } 18 | 19 | static args = [{ 20 | name: 'projectName', 21 | required: true, 22 | }] 23 | 24 | public async run(): Promise { 25 | const {args, flags} = await this.parse(Up) 26 | 27 | const cwd = process.cwd() 28 | const {projectName} = args 29 | const projectRoot = flags.root ?? cwd 30 | const projectPath = path.join(projectRoot, 'projects', projectName) 31 | 32 | await Doctor.run() 33 | 34 | const cmdName = this.constructor.name.toLowerCase() 35 | 36 | const cmd = [ 37 | `export PULUMI_CONFIG_PASSPHRASE="${PULUMI_CONFIG_PASSPHRASE}"`, 38 | `export PULUMI_CONFIG_PASSPHRASE_FILE="${PULUMI_CONFIG_PASSPHRASE_FILE}"`, 39 | `cd ${projectPath}`, 40 | 'yarn', 41 | `${process.env.SHELL} ${cmdName}.sh ${projectName}`, 42 | ].join(' && ') 43 | 44 | execSync(`(${cmd})`, {stdio: 'inherit'}) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const PULUMI_CONFIG_PASSPHRASE = '' 2 | export const PULUMI_CONFIG_PASSPHRASE_FILE = '.pulumi-passphrase' 3 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export {run} from '@oclif/core' 2 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "module": "es2022", 5 | "moduleResolution": "Node", 6 | "noEmit": true 7 | }, 8 | "references": [ 9 | {"path": ".."} 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "importHelpers": true, 5 | "module": "commonjs", 6 | "outDir": "dist", 7 | "rootDir": "src", 8 | "strict": true, 9 | "target": "es2019" 10 | }, 11 | "include": [ 12 | "src/**/*" 13 | ] 14 | } 15 | --------------------------------------------------------------------------------