├── presets ├── nest+next │ ├── gitignore │ ├── env.dist │ ├── nest+next-kool.yml │ ├── nest+next-docker-compose.yml │ └── config.yml ├── nginx │ ├── index.html │ ├── nginx.yml │ └── config.yml ├── nodejs │ ├── node-scripts.yml │ ├── app.js │ ├── docker-compose.yml │ └── config.yml ├── wordpress │ ├── environment │ └── config.yml ├── nestjs │ ├── env.dist │ ├── kool.nestjs.yml │ ├── docker-compose.nestjs.yml │ └── config.yml ├── expressjs │ ├── app.js │ ├── package.json │ ├── docker-compose.yml │ └── config.yml ├── symfony │ ├── symfony-scripts.yml │ └── config.yml ├── laravel │ ├── vite.config.js │ └── config.yml ├── laravel+octane │ └── vite.config.js ├── codeigniter │ ├── codeigniter-scripts.yml │ └── config.yml ├── golang-cli │ ├── config.yml │ └── kool.yml ├── hugo │ ├── kool.yml │ ├── docker-compose.yml │ └── config.yml ├── nuxtjs │ ├── docker-compose.yml │ └── config.yml ├── nextjs │ ├── docker-compose.yml │ └── config.yml ├── php │ └── config.yml └── adonis │ └── config.yml ├── core ├── environment │ ├── .env.testing │ ├── asuser.go │ ├── uid.go │ ├── uid_test.go │ ├── asuser_test.go │ ├── env_storage.go │ ├── env.go │ ├── fake_env_storage.go │ └── env_test.go ├── parser │ ├── testing_files │ │ └── kool.yml │ ├── errors_test.go │ ├── fake_parser.go │ └── errors.go ├── presets │ ├── fixtures │ │ └── presets │ │ │ └── foo │ │ │ └── config.yml │ ├── configs_test.go │ ├── config.go │ ├── parser_test.go │ ├── fake_parser_test.go │ └── fake_parser.go ├── shell │ ├── errors.go │ ├── fake_terminal_test.go │ ├── fake_terminal.go │ ├── terminal_size.go │ ├── lookupCache.go │ ├── errors_test.go │ ├── lookupCache_test.go │ ├── terminal.go │ ├── prompt_input.go │ ├── terminal_test.go │ ├── fake_prompt_select.go │ ├── fake_prompt_select_test.go │ ├── table_writer.go │ ├── fake_table_writer_test.go │ ├── prompt_select.go │ ├── prompt_select_test.go │ ├── fake_table_writer.go │ └── table_writer_test.go ├── network │ ├── fake_network.go │ ├── fake_network_test.go │ ├── network.go │ └── network_test.go ├── utils │ └── loading.go ├── automate │ ├── recipes.go │ └── actions.go └── builder │ ├── fake_command_test.go │ └── fake_command.go ├── templates ├── scripts │ ├── wordpress.yml │ ├── mongodb.yml │ ├── mysql.yml │ ├── maria.yml │ ├── yarn-nextjs.yml │ ├── yarn-nuxtjs.yml │ ├── postgresql.yml │ ├── php.yml │ ├── npm-nextjs.yml │ ├── npm-nuxtjs.yml │ ├── yarn-adonis.yml │ ├── yarn-expressjs.yml │ ├── yarn.yml │ ├── npm-adonis.yml │ ├── npm-expressjs.yml │ ├── yarn-laravel.yml │ ├── npm.yml │ ├── npm-laravel.yml │ ├── laravel.yml │ ├── laravel+octane+roadrunner.yml │ └── laravel+octane+swoole.yml ├── misc │ ├── pdf.yml │ ├── yarn-vitejs.yml │ └── npm-vitejs.yml ├── cache │ ├── memcached16.yml │ └── redis7.yml ├── docker-compose.yml ├── kool.yml ├── app │ ├── php8.yml │ ├── php74.yml │ ├── php81.yml │ ├── php82.yml │ ├── php83.yml │ ├── wordpress74.yml │ ├── wordpress80.yml │ ├── node-adonis.yml │ ├── php82-swoole.yml │ └── php82-roadrunner.yml └── database │ ├── mongodb.yml │ ├── mariadb105.yml │ ├── postgresql13.yml │ ├── mysql57.yml │ ├── mysql8.yml │ ├── postgresql13-adonis.yml │ ├── mysql57-adonis.yml │ └── mysql8-adonis.yml ├── recipes ├── yarn.yml ├── php-8.yml ├── php-7.4.yml ├── php-8.1.yml ├── php-8.2.yml ├── php-8.3.yml ├── redis-7.yml ├── npm.yml ├── pdf.yml ├── memcached-1.6.yml ├── mongodb.yml ├── mysql-8.yml ├── mysql-5.7.yml ├── maria-10.5.yml ├── npm-laravel.yml ├── postgresql-13.yml ├── yarn-laravel.yml ├── php-8.2-swoole.yml ├── php-8.2-roadrunner.yml ├── pick-node-pkg-mgr.yml ├── pick-laravel-node.yml ├── pick-cache.yml ├── create-nestjs.yml ├── pick-db.yml ├── pick-php.yml ├── create-symfony.yml ├── create-laravel.yml └── create-codeigniter.yml ├── services ├── updater │ ├── writeable.go │ ├── writeable_linux.go │ └── fake_updater.go ├── user │ └── elevated.go ├── checker │ ├── fake_checker.go │ ├── fake_checker_test.go │ ├── errors.go │ └── checker.go ├── cloud │ ├── api │ │ ├── api.go │ │ ├── deploy_error.go │ │ ├── deploy_destroy.go │ │ ├── deploy_ping.go │ │ ├── deploy_start.go │ │ ├── deploy_exec.go │ │ ├── deploy_status.go │ │ ├── errors_test.go │ │ ├── deploy_create.go │ │ └── errors.go │ ├── setup │ │ └── cloud_setup_parser.go │ └── deployer.go ├── yamler │ ├── output.go │ ├── output_test.go │ └── merger_test.go └── compose │ ├── fake_parser_test.go │ └── fake_parser.go ├── .editorconfig ├── .env.dist ├── commands ├── kool_service_test.go ├── fake_kool_service_test.go ├── preset_test.go ├── fake_kool_service.go ├── kool_service.go ├── kool_upgrade_available_checker.go ├── restart.go ├── info_test.go ├── cloud_deploy_destroy.go └── cloud_deploy_destroy_test.go ├── embed.go ├── docs ├── 02-Kool-Cloud │ ├── 08-Access-Running-Containers.md │ ├── 17-Custom-Resources.md │ ├── 15-Deploy-Lifecycle-Hooks.md │ ├── 10-Persistent-Storage.md │ └── 05-Environment-Variables.md ├── 15-Snippets │ └── Generate-PDFs.md ├── 20-Troubleshooting │ ├── intro.md │ ├── kool-share.md │ ├── mysql-perm-error.md │ └── hmr-eer-empty-response.md ├── 05-Commands-Reference │ ├── kool-status.md │ ├── kool-create.md │ ├── kool-info.md │ ├── kool-self-update.md │ ├── kool-restart.md │ ├── kool-cloud-setup.md │ ├── kool-recipe.md │ ├── kool-cloud-destroy.md │ ├── kool-run.md │ ├── kool-stop.md │ ├── kool-exec.md │ ├── kool-preset.md │ ├── kool-cloud-help.md │ ├── kool-share.md │ ├── kool-start.md │ ├── kool-logs.md │ ├── kool-cloud-deploy.md │ ├── kool-cloud-exec.md │ ├── kool-docker.md │ ├── kool-cloud-logs.md │ ├── kool-cloud.md │ ├── kool-completion.md │ └── 0-kool.md ├── 10-Docker-Images │ └── 1-Introduction.md └── 03-Presets │ ├── 1-Creating-a-Preset.md │ └── 0-About-Presets.md ├── Dockerfile ├── .gitignore ├── .github ├── workflows │ ├── scan.yml │ ├── release-drafter.yml │ ├── docker-description.yml │ ├── golangci-lint.yml │ ├── test.yml │ └── docker.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── stale.yml ├── PULL_REQUEST_TEMPLATE.md └── release-drafter.yml ├── main.go ├── make_release.sh ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── SECURITY.md ├── RELEASE.md ├── LICENSE.md ├── kool.yml └── CONTRIBUTING.md /presets/nest+next/gitignore: -------------------------------------------------------------------------------- 1 | *.env 2 | -------------------------------------------------------------------------------- /core/environment/.env.testing: -------------------------------------------------------------------------------- 1 | VAR_TESTING_FILE=1 2 | -------------------------------------------------------------------------------- /presets/nginx/index.html: -------------------------------------------------------------------------------- 1 | Hello World with Kool 2 | -------------------------------------------------------------------------------- /core/parser/testing_files/kool.yml: -------------------------------------------------------------------------------- 1 | scripts: 2 | testing: "echo testing" 3 | -------------------------------------------------------------------------------- /presets/nodejs/node-scripts.yml: -------------------------------------------------------------------------------- 1 | scripts: 2 | node: kool exec app node 3 | -------------------------------------------------------------------------------- /presets/wordpress/environment: -------------------------------------------------------------------------------- 1 | DB_USERNAME=user 2 | DB_PASSWORD=pass 3 | DB_DATABASE=wordpress 4 | -------------------------------------------------------------------------------- /templates/scripts/wordpress.yml: -------------------------------------------------------------------------------- 1 | scripts: 2 | php: kool exec app php 3 | wp: kool exec app wp 4 | -------------------------------------------------------------------------------- /recipes/yarn.yml: -------------------------------------------------------------------------------- 1 | title: "Yarn" 2 | 3 | actions: 4 | - merge: scripts/yarn.yml 5 | dst: kool.yml 6 | -------------------------------------------------------------------------------- /recipes/php-8.yml: -------------------------------------------------------------------------------- 1 | title: "PHP 8.0" 2 | 3 | actions: 4 | - merge: app/php8.yml 5 | dst: docker-compose.yml 6 | -------------------------------------------------------------------------------- /recipes/php-7.4.yml: -------------------------------------------------------------------------------- 1 | title: "PHP 7.4" 2 | 3 | actions: 4 | - merge: app/php74.yml 5 | dst: docker-compose.yml 6 | -------------------------------------------------------------------------------- /recipes/php-8.1.yml: -------------------------------------------------------------------------------- 1 | title: "PHP 8.1" 2 | 3 | actions: 4 | - merge: app/php81.yml 5 | dst: docker-compose.yml 6 | -------------------------------------------------------------------------------- /recipes/php-8.2.yml: -------------------------------------------------------------------------------- 1 | title: "PHP 8.2" 2 | 3 | actions: 4 | - merge: app/php82.yml 5 | dst: docker-compose.yml 6 | -------------------------------------------------------------------------------- /recipes/php-8.3.yml: -------------------------------------------------------------------------------- 1 | title: "PHP 8.3" 2 | 3 | actions: 4 | - merge: app/php83.yml 5 | dst: docker-compose.yml 6 | -------------------------------------------------------------------------------- /recipes/redis-7.yml: -------------------------------------------------------------------------------- 1 | title: "Redis 7" 2 | 3 | actions: 4 | - merge: cache/redis7.yml 5 | dst: docker-compose.yml 6 | -------------------------------------------------------------------------------- /recipes/npm.yml: -------------------------------------------------------------------------------- 1 | title: "npm - Node Package Manager" 2 | 3 | actions: 4 | - merge: scripts/npm.yml 5 | dst: kool.yml 6 | -------------------------------------------------------------------------------- /recipes/pdf.yml: -------------------------------------------------------------------------------- 1 | title: "Microservice: PDF generator" 2 | 3 | actions: 4 | - merge: misc/pdf.yml 5 | dst: docker-compose.yml 6 | -------------------------------------------------------------------------------- /recipes/memcached-1.6.yml: -------------------------------------------------------------------------------- 1 | title: "Memcached 1.6" 2 | 3 | actions: 4 | - merge: cache/memcached16.yml 5 | dst: docker-compose.yml 6 | -------------------------------------------------------------------------------- /presets/nestjs/env.dist: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # Kool variables - mostly used to tweak docker-compose.yml containers. 4 | # 5 | # KOOL_APP_PORT= 6 | # KOOL_GLOBAL_NETWORK= 7 | -------------------------------------------------------------------------------- /templates/scripts/mongodb.yml: -------------------------------------------------------------------------------- 1 | scripts: 2 | # CLI access to MongoDB 3 | mongodb: kool exec database mongosh --port=27017 --username=root --password=root 4 | -------------------------------------------------------------------------------- /templates/scripts/mysql.yml: -------------------------------------------------------------------------------- 1 | scripts: 2 | # CLI access to MySQL 3 | mysql: kool exec -e MYSQL_PWD=$DB_PASSWORD database mysql -u $DB_USERNAME $DB_DATABASE 4 | -------------------------------------------------------------------------------- /services/updater/writeable.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | // +build !linux 3 | 4 | package updater 5 | 6 | func isWriteable(_ string) bool { 7 | return false 8 | } 9 | -------------------------------------------------------------------------------- /templates/scripts/maria.yml: -------------------------------------------------------------------------------- 1 | scripts: 2 | # CLI access to MariaDB 3 | maria: kool exec -e MYSQL_PWD=$DB_PASSWORD database mariadb -u $DB_USERNAME $DB_DATABASE 4 | -------------------------------------------------------------------------------- /templates/scripts/yarn-nextjs.yml: -------------------------------------------------------------------------------- 1 | scripts: 2 | yarn: kool exec app yarn 3 | 4 | setup: 5 | - kool docker kooldev/node:20 yarn install 6 | - kool start 7 | -------------------------------------------------------------------------------- /templates/scripts/yarn-nuxtjs.yml: -------------------------------------------------------------------------------- 1 | scripts: 2 | yarn: kool exec app yarn 3 | 4 | setup: 5 | - kool docker kooldev/node:20 yarn install 6 | - kool start 7 | -------------------------------------------------------------------------------- /templates/scripts/postgresql.yml: -------------------------------------------------------------------------------- 1 | scripts: 2 | # CLI access to PostgreSQL 3 | psql: kool exec -e PGPASSWORD=$DB_PASSWORD database psql -U $DB_USERNAME $DB_DATABASE 4 | -------------------------------------------------------------------------------- /recipes/mongodb.yml: -------------------------------------------------------------------------------- 1 | title: "Mongodb" 2 | 3 | actions: 4 | - merge: database/mongodb.yml 5 | dst: docker-compose.yml 6 | - merge: scripts/mongodb.yml 7 | dst: kool.yml 8 | -------------------------------------------------------------------------------- /recipes/mysql-8.yml: -------------------------------------------------------------------------------- 1 | title: "MySQL 8" 2 | 3 | actions: 4 | - merge: database/mysql8.yml 5 | dst: docker-compose.yml 6 | - merge: scripts/mysql.yml 7 | dst: kool.yml 8 | -------------------------------------------------------------------------------- /templates/scripts/php.yml: -------------------------------------------------------------------------------- 1 | scripts: 2 | php: kool exec app php 3 | composer: kool exec app composer 4 | 5 | setup: 6 | - kool start 7 | # - add more setup commands 8 | -------------------------------------------------------------------------------- /recipes/mysql-5.7.yml: -------------------------------------------------------------------------------- 1 | title: "MySQL 5.7" 2 | 3 | actions: 4 | - merge: database/mysql57.yml 5 | dst: docker-compose.yml 6 | - merge: scripts/mysql.yml 7 | dst: kool.yml 8 | -------------------------------------------------------------------------------- /recipes/maria-10.5.yml: -------------------------------------------------------------------------------- 1 | title: "MariaDB 10.5" 2 | 3 | actions: 4 | - merge: database/mariadb105.yml 5 | dst: docker-compose.yml 6 | - merge: scripts/maria.yml 7 | dst: kool.yml 8 | -------------------------------------------------------------------------------- /templates/scripts/npm-nextjs.yml: -------------------------------------------------------------------------------- 1 | scripts: 2 | npm: kool exec app npm 3 | npx: kool exec app npx 4 | 5 | setup: 6 | - kool docker kooldev/node:20 npm install 7 | - kool start 8 | -------------------------------------------------------------------------------- /templates/scripts/npm-nuxtjs.yml: -------------------------------------------------------------------------------- 1 | scripts: 2 | npm: kool exec app npm 3 | npx: kool exec app npx 4 | 5 | setup: 6 | - kool docker kooldev/node:20 npm install 7 | - kool start 8 | -------------------------------------------------------------------------------- /recipes/npm-laravel.yml: -------------------------------------------------------------------------------- 1 | title: "npm for Laravel" 2 | 3 | actions: 4 | - merge: scripts/npm-laravel.yml 5 | dst: kool.yml 6 | - merge: misc/npm-vitejs.yml 7 | dst: docker-compose.yml 8 | -------------------------------------------------------------------------------- /core/environment/asuser.go: -------------------------------------------------------------------------------- 1 | package environment 2 | 3 | func initAsuser(envStorage EnvStorage) { 4 | if envStorage.Get("KOOL_ASUSER") == "" { 5 | envStorage.Set("KOOL_ASUSER", uid()) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /recipes/postgresql-13.yml: -------------------------------------------------------------------------------- 1 | title: "PostgreSQL 13" 2 | 3 | actions: 4 | - merge: database/postgresql13.yml 5 | dst: docker-compose.yml 6 | - merge: scripts/postgresql.yml 7 | dst: kool.yml 8 | -------------------------------------------------------------------------------- /recipes/yarn-laravel.yml: -------------------------------------------------------------------------------- 1 | title: "Yarn (Laravel)" 2 | 3 | actions: 4 | - merge: scripts/yarn-laravel.yml 5 | dst: kool.yml 6 | - merge: misc/yarn-vitejs.yml 7 | dst: docker-compose.yml 8 | -------------------------------------------------------------------------------- /templates/misc/pdf.yml: -------------------------------------------------------------------------------- 1 | services: 2 | # see: https://github.com/kool-dev/pdf 3 | pdf: 4 | image: "kooldev/pdf:latest" 5 | expose: 6 | - 3000 7 | networks: 8 | - kool_local 9 | -------------------------------------------------------------------------------- /templates/scripts/yarn-adonis.yml: -------------------------------------------------------------------------------- 1 | scripts: 2 | adonis: kool exec app adonis 3 | yarn: kool exec app yarn 4 | 5 | setup: 6 | - kool docker kooldev/node:20 yarn install 7 | - kool start 8 | -------------------------------------------------------------------------------- /templates/scripts/yarn-expressjs.yml: -------------------------------------------------------------------------------- 1 | scripts: 2 | node: kool exec app node 3 | yarn: kool exec app yarn 4 | 5 | setup: 6 | - kool docker kooldev/node:20 yarn install 7 | - kool start 8 | -------------------------------------------------------------------------------- /templates/scripts/yarn.yml: -------------------------------------------------------------------------------- 1 | scripts: 2 | # yarn - helpers for JS handling 3 | yarn: kool docker kooldev/node:20 yarn 4 | setup: 5 | - kool run yarn install 6 | reset: 7 | - kool run yarn install 8 | -------------------------------------------------------------------------------- /presets/nest+next/env.dist: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # Kool variables - mostly used to tweak docker-compose.yml containers. 4 | # 5 | # KOOL_FRONTEND_PORT= 6 | # KOOL_BACKEND_PORT= 7 | # KOOL_DATABASE_PORT= 8 | # KOOL_GLOBAL_NETWORK= 9 | -------------------------------------------------------------------------------- /templates/cache/memcached16.yml: -------------------------------------------------------------------------------- 1 | services: 2 | cache: 3 | image: memcached:1.6-alpine 4 | volumes: 5 | - cache:/data:delegated 6 | networks: 7 | - kool_local 8 | 9 | volumes: 10 | cache: 11 | -------------------------------------------------------------------------------- /templates/scripts/npm-adonis.yml: -------------------------------------------------------------------------------- 1 | scripts: 2 | adonis: kool exec app adonis 3 | npm: kool exec app npm 4 | npx: kool exec app npx 5 | 6 | setup: 7 | - kool docker kooldev/node:20 npm install 8 | - kool start 9 | -------------------------------------------------------------------------------- /templates/scripts/npm-expressjs.yml: -------------------------------------------------------------------------------- 1 | scripts: 2 | node: kool exec app node 3 | npm: kool exec app npm 4 | npx: kool exec app npx 5 | 6 | setup: 7 | - kool docker kooldev/node:20 npm install 8 | - kool start 9 | -------------------------------------------------------------------------------- /templates/scripts/yarn-laravel.yml: -------------------------------------------------------------------------------- 1 | scripts: 2 | # yarn - helpers for JS handling 3 | yarn: kool docker kooldev/node:20 yarn 4 | before-start: 5 | - kool run yarn install 6 | reset: 7 | - kool run yarn install 8 | -------------------------------------------------------------------------------- /services/updater/writeable_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package updater 5 | 6 | import "golang.org/x/sys/unix" 7 | 8 | func isWriteable(path string) bool { 9 | return unix.Access(path, unix.W_OK) == nil 10 | } 11 | -------------------------------------------------------------------------------- /templates/scripts/npm.yml: -------------------------------------------------------------------------------- 1 | scripts: 2 | # npm - helpers for JS handling 3 | npm: kool docker kooldev/node:20 npm 4 | npx: kool docker kooldev/node:20 npx 5 | setup: 6 | - kool run npm install 7 | reset: 8 | - kool run npm install 9 | -------------------------------------------------------------------------------- /presets/nginx/nginx.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | image: kooldev/nginx:static 4 | ports: 5 | - "${KOOL_APP_PORT:-80}:80" 6 | volumes: 7 | - .:/app:delegated 8 | networks: 9 | - kool_local 10 | - kool_global 11 | -------------------------------------------------------------------------------- /templates/scripts/npm-laravel.yml: -------------------------------------------------------------------------------- 1 | scripts: 2 | # npm - helpers for JS handling 3 | npm: kool docker kooldev/node:20 npm 4 | npx: kool docker kooldev/node:20 npx 5 | before-start: 6 | - kool run npm install 7 | reset: 8 | - kool run npm install 9 | -------------------------------------------------------------------------------- /core/presets/fixtures/presets/foo/config.yml: -------------------------------------------------------------------------------- 1 | tags: [ 'foo' ] 2 | 3 | create: 4 | - name: creating foo 5 | actions: 6 | - scripts: 7 | - echo "foo" 8 | 9 | preset: 10 | - name: 'preset step' 11 | actions: 12 | - copy: bar 13 | -------------------------------------------------------------------------------- /templates/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | # 3 | # Services definitions 4 | # 5 | services: 6 | # 7 | # Networks definitions 8 | # 9 | networks: 10 | kool_local: 11 | kool_global: 12 | external: true 13 | name: "${KOOL_GLOBAL_NETWORK:-kool_global}" 14 | -------------------------------------------------------------------------------- /templates/misc/yarn-vitejs.yml: -------------------------------------------------------------------------------- 1 | services: 2 | node: 3 | image: kooldev/node:20 4 | command: ["yarn", "dev"] 5 | ports: 6 | - "3001:3001" 7 | volumes: 8 | - .:/app:delegated 9 | networks: 10 | - kool_local 11 | - kool_global 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = tab 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [{*.yaml,*.yml,*.sh}] 12 | indent_style = space 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.env.dist: -------------------------------------------------------------------------------- 1 | # GOOS variable for your local development; since the Go docker images are 2 | # Linux based, in case you are developing on MacOS you must uncomment the line below. 3 | #GOOS=darwin 4 | 5 | # GOARCH the architecture to be used for kool run compile script. 6 | #GOARCH=amd64 7 | -------------------------------------------------------------------------------- /services/user/elevated.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import "os" 4 | 5 | // CurrentUserIsElevated returns true if the current user 6 | // executing the program has admin privileges (sudo/administrator) 7 | func CurrentUserIsElevated() bool { 8 | return os.Getuid() == 0 9 | } 10 | -------------------------------------------------------------------------------- /templates/cache/redis7.yml: -------------------------------------------------------------------------------- 1 | services: 2 | cache: 3 | image: redis:7-alpine 4 | volumes: 5 | - cache:/data:delegated 6 | networks: 7 | - kool_local 8 | healthcheck: 9 | test: ["CMD", "redis-cli", "ping"] 10 | 11 | volumes: 12 | cache: 13 | -------------------------------------------------------------------------------- /templates/misc/npm-vitejs.yml: -------------------------------------------------------------------------------- 1 | services: 2 | node: 3 | image: kooldev/node:20 4 | command: ["npm", "run", "dev"] 5 | ports: 6 | - "3001:3001" 7 | volumes: 8 | - .:/app:delegated 9 | networks: 10 | - kool_local 11 | - kool_global 12 | -------------------------------------------------------------------------------- /presets/expressjs/app.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const app = express(); 3 | const port = 3000; 4 | 5 | app.get("/", (req, res) => { 6 | res.send("Hello World"); 7 | }) 8 | 9 | app.listen(port, () => { 10 | console.log("Server running at http://localhost:"+port+"/"); 11 | }) 12 | -------------------------------------------------------------------------------- /presets/nestjs/kool.nestjs.yml: -------------------------------------------------------------------------------- 1 | scripts: 2 | setup: 3 | # copy .env file 4 | - cp .env.dist .env 5 | # install backend deps 6 | - kool docker kooldev/node:20 npm install 7 | 8 | # helpers 9 | npm: kool exec app npm 10 | npx: kool exec app npx 11 | nest: kool run npx @nestjs/cli 12 | -------------------------------------------------------------------------------- /core/environment/uid.go: -------------------------------------------------------------------------------- 1 | package environment 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | func initUid(envStorage EnvStorage) { 9 | if envStorage.Get("UID") == "" { 10 | envStorage.Set("UID", uid()) 11 | } 12 | } 13 | 14 | func uid() string { 15 | return fmt.Sprintf("%d", os.Getuid()) 16 | } 17 | -------------------------------------------------------------------------------- /templates/kool.yml: -------------------------------------------------------------------------------- 1 | # Here you can define shortcuts and aliases to common tasks (commands) 2 | # you will run in your local environment or CI or deploy. 3 | # Use the scripts defined below with: 4 | # $ kool run 21 | ``` 22 | 23 | Not This: 24 | 25 | ```html 26 | 27 | ``` 28 | 29 | Using it with asset might cause problems while using `kool share`. -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:stable 2 | 3 | ARG USERNAME=kool 4 | ARG USER_UID=1000 5 | ARG USER_GID=$USER_UID 6 | 7 | RUN groupadd --gid $USER_GID $USERNAME \ 8 | && useradd --uid $USER_UID --gid $USER_GID -s /bin/bash -m $USERNAME \ 9 | && apt-get update \ 10 | && apt-get install -y sudo \ 11 | && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \ 12 | && chmod 0440 /etc/sudoers.d/$USERNAME 13 | 14 | # install kool 15 | RUN apt update && apt install curl git -y && curl -fsSL https://kool.dev/install | bash 16 | 17 | WORKDIR /home/kool 18 | 19 | USER $USERNAME 20 | 21 | RUN bash -c "$(curl -fsSL https://raw.githubusercontent.com/ohmybash/oh-my-bash/master/tools/install.sh)" 22 | -------------------------------------------------------------------------------- /templates/database/mysql8.yml: -------------------------------------------------------------------------------- 1 | services: 2 | database: 3 | image: mysql/mysql-server:8.0 4 | command: --default-authentication-plugin=mysql_native_password 5 | ports: 6 | - "${KOOL_DATABASE_PORT:-3306}:3306" 7 | environment: 8 | MYSQL_ROOT_HOST: "%" 9 | MYSQL_ROOT_PASSWORD: "${DB_PASSWORD-rootpass}" 10 | MYSQL_DATABASE: "${DB_DATABASE-database}" 11 | MYSQL_USER: "${DB_USERNAME-user}" 12 | MYSQL_PASSWORD: "${DB_PASSWORD-pass}" 13 | MYSQL_ALLOW_EMPTY_PASSWORD: 1 14 | volumes: 15 | - database:/var/lib/mysql:delegated 16 | networks: 17 | - kool_local 18 | healthcheck: 19 | test: ["CMD", "mysqladmin", "ping"] 20 | 21 | volumes: 22 | database: 23 | -------------------------------------------------------------------------------- /docs/05-Commands-Reference/kool-cloud-setup.md: -------------------------------------------------------------------------------- 1 | ## kool cloud setup 2 | 3 | Set up local configuration files for deployment 4 | 5 | ``` 6 | kool cloud setup 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for setup 13 | ``` 14 | 15 | ### Options inherited from parent commands 16 | 17 | ``` 18 | --domain string Environment domain name to deploy to 19 | --token string Token to authenticate with Kool.dev Cloud API 20 | --verbose Increases output verbosity 21 | -w, --working_dir string Changes the working directory for the command 22 | ``` 23 | 24 | ### SEE ALSO 25 | 26 | * [kool cloud](kool_cloud) - Interact with Kool.dev Cloud and manage your deployments. 27 | 28 | -------------------------------------------------------------------------------- /docs/20-Troubleshooting/mysql-perm-error.md: -------------------------------------------------------------------------------- 1 | ### `kool run mysql`, `kool run maria` or `kool run setup` 2 | 3 | **Problem:** 4 | 5 | > Access denied for root on mysql and mariaDB databases 6 | 7 | **Answer:** 8 | 9 | > Stop your containers with the purge option in order to delete all volumes 10 | 11 | ```shell 12 | kool stop --purge 13 | ``` 14 | 15 | > Make sure your DB username is other than root and define a password on the .env file 16 | 17 | ```diff 18 | -DB_USERNAME=root 19 | +DB_USERNAME= 20 | 21 | -DB_PASSWORD= 22 | +DB_PASSWORD= 23 | ``` 24 | 25 | > Start your container 26 | 27 | ```shell 28 | kool start 29 | ``` 30 | 31 | > Run your setup 32 | 33 | ```shell 34 | kool run setup 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/05-Commands-Reference/kool-recipe.md: -------------------------------------------------------------------------------- 1 | ## kool recipe 2 | 3 | Adds configuration for some recipe in the current work directory. 4 | 5 | ### Synopsis 6 | 7 | Run the defines steps for a recipe which can add/edit files the current project directory in order to add some new service or configuration. 8 | 9 | ``` 10 | kool recipe [RECIPE] 11 | ``` 12 | 13 | ### Options 14 | 15 | ``` 16 | -h, --help help for recipe 17 | ``` 18 | 19 | ### Options inherited from parent commands 20 | 21 | ``` 22 | --verbose Increases output verbosity 23 | -w, --working_dir string Changes the working directory for the command 24 | ``` 25 | 26 | ### SEE ALSO 27 | 28 | * [kool](kool) - Cloud native environments made easy 29 | 30 | -------------------------------------------------------------------------------- /templates/database/postgresql13-adonis.yml: -------------------------------------------------------------------------------- 1 | services: 2 | database: 3 | image: postgres:13-alpine 4 | ports: 5 | - "${KOOL_DATABASE_PORT:-5432}:5432" 6 | environment: 7 | POSTGRES_DB: "${DB_DATABASE-database}" 8 | POSTGRES_USER: "${DB_USER-user}" 9 | POSTGRES_PASSWORD: "${DB_PASSWORD-pass}" 10 | POSTGRES_HOST_AUTH_METHOD: "trust" 11 | volumes: 12 | - database:/var/lib/postgresql/data:delegated 13 | networks: 14 | - kool_local 15 | healthcheck: 16 | test: ["CMD", "pg_isready", "-q", "-d", "$DB_DATABASE", "-U", "$DB_USER"] 17 | 18 | volumes: 19 | database: 20 | 21 | scripts: 22 | psql: kool exec -e PGPASSWORD=$DB_PASSWORD database psql -U $DB_USER $DB_DATABASE 23 | -------------------------------------------------------------------------------- /docs/05-Commands-Reference/kool-cloud-destroy.md: -------------------------------------------------------------------------------- 1 | ## kool cloud destroy 2 | 3 | Destroy an environment deployed to Kool.dev Cloud 4 | 5 | ``` 6 | kool cloud destroy 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for destroy 13 | ``` 14 | 15 | ### Options inherited from parent commands 16 | 17 | ``` 18 | --domain string Environment domain name to deploy to 19 | --token string Token to authenticate with Kool.dev Cloud API 20 | --verbose Increases output verbosity 21 | -w, --working_dir string Changes the working directory for the command 22 | ``` 23 | 24 | ### SEE ALSO 25 | 26 | * [kool cloud](kool_cloud) - Interact with Kool.dev Cloud and manage your deployments. 27 | 28 | -------------------------------------------------------------------------------- /.github/workflows/docker-description.yml: -------------------------------------------------------------------------------- 1 | name: Sync Docker Hub Description 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - README.md 9 | - .github/workflows/docker-description.yml 10 | 11 | jobs: 12 | docker-description: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Sync Docker Hub Description 18 | if: github.ref == 'refs/heads/main' && github.repository == 'kool-dev/kool' 19 | uses: peter-evans/dockerhub-description@v2 20 | env: 21 | DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} 22 | DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} 23 | DOCKERHUB_REPOSITORY: kooldev/kool 24 | -------------------------------------------------------------------------------- /core/shell/errors_test.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestIsUserCancelledError(t *testing.T) { 10 | err := fmt.Errorf("error") 11 | 12 | if IsUserCancelledError(err) { 13 | t.Error("method IsUserCancelledError should return false for non user cancelled errors") 14 | } 15 | 16 | err = ErrUserCancelled 17 | 18 | if !IsUserCancelledError(err) { 19 | t.Error("method IsUserCancelledError should return true for user cancelled errors") 20 | } 21 | } 22 | 23 | func TestExitable(t *testing.T) { 24 | err := errors.New("some error") 25 | 26 | exitable := ErrExitable{Err: err} 27 | 28 | if exitable.Error() != err.Error() { 29 | t.Error("error should be the same") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /docs/05-Commands-Reference/kool-run.md: -------------------------------------------------------------------------------- 1 | ## kool run 2 | 3 | Execute a script defined in kool.yml 4 | 5 | ### Synopsis 6 | 7 | Execute the specified SCRIPT, as defined in the kool.yml file. 8 | A single-line SCRIPT can be run with optional arguments. 9 | 10 | ``` 11 | kool run SCRIPT [--] [ARG...] 12 | ``` 13 | 14 | ### Options 15 | 16 | ``` 17 | -e, --env stringArray Environment variables. 18 | -h, --help help for run 19 | ``` 20 | 21 | ### Options inherited from parent commands 22 | 23 | ``` 24 | --verbose Increases output verbosity 25 | -w, --working_dir string Changes the working directory for the command 26 | ``` 27 | 28 | ### SEE ALSO 29 | 30 | * [kool](kool) - Cloud native environments made easy 31 | 32 | -------------------------------------------------------------------------------- /services/cloud/setup/cloud_setup_parser.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | const KoolDeployFile string = "kool.cloud.yml" 9 | 10 | type CloudSetupParser interface { 11 | HasDeployConfig() bool 12 | ConfigFilePath() string 13 | } 14 | 15 | type DefaultCloudSetupParser struct { 16 | pwd string 17 | } 18 | 19 | func NewDefaultCloudSetupParser(pwd string) *DefaultCloudSetupParser { 20 | return &DefaultCloudSetupParser{pwd} 21 | } 22 | 23 | func (s *DefaultCloudSetupParser) ConfigFilePath() string { 24 | return filepath.Join(s.pwd, KoolDeployFile) 25 | } 26 | 27 | func (s *DefaultCloudSetupParser) HasDeployConfig() (has bool) { 28 | _, err := os.Stat(s.ConfigFilePath()) 29 | 30 | return err == nil 31 | } 32 | -------------------------------------------------------------------------------- /templates/database/mysql57-adonis.yml: -------------------------------------------------------------------------------- 1 | services: 2 | database: 3 | image: mysql/mysql-server:5.7 4 | ports: 5 | - "${KOOL_DATABASE_PORT:-3306}:3306" 6 | environment: 7 | MYSQL_ROOT_HOST: "%" 8 | MYSQL_ROOT_PASSWORD: "${DB_PASSWORD-rootpass}" 9 | MYSQL_DATABASE: "${DB_DATABASE-database}" 10 | MYSQL_USER: "${DB_USER-user}" 11 | MYSQL_PASSWORD: "${DB_PASSWORD-pass}" 12 | MYSQL_ALLOW_EMPTY_PASSWORD: 1 13 | volumes: 14 | - database:/var/lib/mysql:delegated 15 | networks: 16 | - kool_local 17 | healthcheck: 18 | test: ["CMD", "mysqladmin", "ping"] 19 | 20 | volumes: 21 | database: 22 | 23 | scripts: 24 | mysql: kool exec -e MYSQL_PWD=$DB_PASSWORD database mysql -u $DB_USER $DB_DATABASE 25 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Our current version v3 is an LTS version - first version to bring the cloud services portion to the general public. 6 | 7 | | Version | Supported | Policy | 8 | | :----: | :----: | -- | 9 | | 3.x | :white_check_mark: | Rolling updates as new releases with semantic versioning. | 10 | | 2.x | :x: | You must upgrade to v3 for continued security updates. | 11 | | 1.x | :x: | You must upgrade to v3 for continued security updates. | 12 | 13 | 14 | ## Reporting a Vulnerability 15 | 16 | If you find any security issue or vulnerability please send reach out by email to [contact@kool.dev](mailto:contact@kool.dev) before making it publicly available for previous assessment and counter mesures. 17 | -------------------------------------------------------------------------------- /presets/nginx/config.yml: -------------------------------------------------------------------------------- 1 | # Which tags are related to this preset; used for branching the choices on preset wizard 2 | tags: [ 'Static' ] 3 | 4 | name: 'Nginx' 5 | 6 | # Create defines the workflow for creating a new Project where this preset can then be installed 7 | create: 8 | - name: Creating new NGINX Application 9 | actions: 10 | - scripts: 11 | - mkdir -p $CREATE_DIRECTORY/public 12 | 13 | # Preset defines the workflow for installing this preset in the current working directory 14 | preset: 15 | - name: 'Copy basic config files' 16 | actions: 17 | - copy: index.html 18 | dst: public/index.html 19 | - copy: docker-compose.yml 20 | - merge: nginx.yml 21 | dst: docker-compose.yml 22 | - copy: kool.yml 23 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 15 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - R&D 8 | - security 9 | - enhancement 10 | # Label to use when marking an issue as stale 11 | staleLabel: wontfix 12 | # Comment to post when marking an issue as stale. Set to `false` to disable 13 | markComment: > 14 | This issue has been automatically marked as stale because it has not had 15 | recent activity. It will be closed if no further activity occurs. Thank you 16 | for your contributions. 17 | # Comment to post when closing a stale issue. Set to `false` to disable 18 | closeComment: false 19 | -------------------------------------------------------------------------------- /services/cloud/api/deploy_error.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "fmt" 4 | 5 | // DeployError consumes the API endpoint to inform a build error for a deployment 6 | type DeployError struct { 7 | Endpoint 8 | } 9 | 10 | // NewDeployError creates a new DeployError instance 11 | func NewDeployError(created *DeployCreateResponse, err error) (c *DeployError) { 12 | c = &DeployError{ 13 | Endpoint: NewDefaultEndpoint("POST"), 14 | } 15 | 16 | c.SetPath("deploy/error") 17 | c.Body().Set("id", fmt.Sprintf("%d", created.Deploy.ID)) 18 | c.Body().Set("err", err.Error()) 19 | 20 | return 21 | } 22 | 23 | // Run calls deploy/error in the Kool Dev API 24 | func (c *DeployError) Run() (err error) { 25 | var resp interface{} 26 | c.SetResponseReceiver(&resp) 27 | err = c.DoCall() 28 | return 29 | } 30 | -------------------------------------------------------------------------------- /core/shell/lookupCache_test.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | ) 7 | 8 | func TestLookupCache(t *testing.T) { 9 | cache := newLookupCache() 10 | 11 | if cache.mtx == nil { 12 | t.Error("missing mtx on lookupCache") 13 | } 14 | 15 | if cache.cache != nil { 16 | t.Errorf("unexpected default non-nil value for cache: %v", cache.cache) 17 | } 18 | 19 | if exists, err := cache.fetch("unknown"); err != nil || exists { 20 | t.Errorf("bad return for fetching unknown key from cache") 21 | } 22 | 23 | var err = errors.New("error") 24 | cache.set("key", err) 25 | 26 | if exists, cached := cache.fetch("key"); !exists || !errors.Is(cached, err) { 27 | t.Errorf("failed to fetch error from cache - exists: %v got: %v expected: %v", exists, cached, err) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | permissions: 8 | contents: read 9 | jobs: 10 | golangci: 11 | name: lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | # ref: https://github.com/golangci/golangci-lint-action/issues/442#issuecomment-1203786890 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-go@v3 17 | with: 18 | go-version-file: go.mod 19 | cache: true 20 | cache-dependency-path: go.sum 21 | - name: Install golangci-lint 22 | run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.54.1 23 | - name: Run golangci-lint 24 | run: golangci-lint run --version --verbose --out-format=github-actions 25 | -------------------------------------------------------------------------------- /docs/05-Commands-Reference/kool-stop.md: -------------------------------------------------------------------------------- 1 | ## kool stop 2 | 3 | Stop and destroy running service containers 4 | 5 | ### Synopsis 6 | 7 | Stop and destroy the specified [SERVICE] containers, which were started 8 | using 'kool start'. If no [SERVICE] is provided, all running containers are stopped. 9 | 10 | ``` 11 | kool stop [SERVICE...] 12 | ``` 13 | 14 | ### Options 15 | 16 | ``` 17 | -h, --help help for stop 18 | --purge Remove all persistent data from volume mounts on containers 19 | ``` 20 | 21 | ### Options inherited from parent commands 22 | 23 | ``` 24 | --verbose Increases output verbosity 25 | -w, --working_dir string Changes the working directory for the command 26 | ``` 27 | 28 | ### SEE ALSO 29 | 30 | * [kool](kool) - Cloud native environments made easy 31 | 32 | -------------------------------------------------------------------------------- /presets/nuxtjs/config.yml: -------------------------------------------------------------------------------- 1 | # Which tags are related to this preset; used for branching the choices on preset wizard 2 | tags: [ 'Javascript' ] 3 | 4 | name: 'NuxtJS' 5 | 6 | # Create defines the workflow for creating a new Project where this preset can then be installed 7 | create: 8 | - name: Creating new Nuxt Application 9 | actions: 10 | - scripts: 11 | - docker pull -q kooldev/node:20 12 | - kool docker kooldev/node:20 yarn create nuxt-app $CREATE_DIRECTORY 13 | 14 | # Preset defines the workflow for installing this preset in the current working directory 15 | preset: 16 | - name: 'Copy basic config files' 17 | actions: 18 | - copy: docker-compose.yml 19 | - copy: kool.yml 20 | 21 | - name: 'Customize your setup' 22 | actions: 23 | - recipe: pick-node-pkg-mgr 24 | -------------------------------------------------------------------------------- /presets/nextjs/config.yml: -------------------------------------------------------------------------------- 1 | # Which tags are related to this preset; used for branching the choices on preset wizard 2 | tags: [ 'Javascript' ] 3 | 4 | name: 'NextJS' 5 | 6 | # Create defines the workflow for creating a new Project where this preset can then be installed 7 | create: 8 | - name: Creating new NextJS Application 9 | actions: 10 | - scripts: 11 | - docker pull -q kooldev/node:20 12 | - kool docker kooldev/node:20 yarn create next-app $CREATE_DIRECTORY 13 | 14 | # Preset defines the workflow for installing this preset in the current working directory 15 | preset: 16 | - name: 'Copy basic config files' 17 | actions: 18 | - copy: docker-compose.yml 19 | - copy: kool.yml 20 | 21 | - name: 'Customize your setup' 22 | actions: 23 | - recipe: pick-node-pkg-mgr 24 | -------------------------------------------------------------------------------- /docs/05-Commands-Reference/kool-exec.md: -------------------------------------------------------------------------------- 1 | ## kool exec 2 | 3 | Execute a command inside a running service container 4 | 5 | ### Synopsis 6 | 7 | Execute a COMMAND inside the specified SERVICE container (similar to an SSH session). 8 | 9 | ``` 10 | kool exec [OPTIONS] SERVICE COMMAND [--] [ARG...] 11 | ``` 12 | 13 | ### Options 14 | 15 | ``` 16 | -d, --detach Detached mode: Run command in the background. 17 | -e, --env stringArray Environment variables. 18 | -h, --help help for exec 19 | ``` 20 | 21 | ### Options inherited from parent commands 22 | 23 | ``` 24 | --verbose Increases output verbosity 25 | -w, --working_dir string Changes the working directory for the command 26 | ``` 27 | 28 | ### SEE ALSO 29 | 30 | * [kool](kool) - Cloud native environments made easy 31 | 32 | -------------------------------------------------------------------------------- /docs/05-Commands-Reference/kool-preset.md: -------------------------------------------------------------------------------- 1 | ## kool preset 2 | 3 | Install configuration files customized for Kool in the current directory 4 | 5 | ### Synopsis 6 | 7 | Initialize a project using the specified [PRESET] by installing configuration 8 | files customized for Kool in the current working directory. If no [PRESET] is provided, 9 | an interactive wizard will present the available options. 10 | 11 | ``` 12 | kool preset [PRESET] 13 | ``` 14 | 15 | ### Options 16 | 17 | ``` 18 | -h, --help help for preset 19 | ``` 20 | 21 | ### Options inherited from parent commands 22 | 23 | ``` 24 | --verbose Increases output verbosity 25 | -w, --working_dir string Changes the working directory for the command 26 | ``` 27 | 28 | ### SEE ALSO 29 | 30 | * [kool](kool) - Cloud native environments made easy 31 | 32 | -------------------------------------------------------------------------------- /templates/database/mysql8-adonis.yml: -------------------------------------------------------------------------------- 1 | services: 2 | database: 3 | image: mysql/mysql-server:8.0 4 | command: --default-authentication-plugin=mysql_native_password 5 | ports: 6 | - "${KOOL_DATABASE_PORT:-3306}:3306" 7 | environment: 8 | MYSQL_ROOT_HOST: "%" 9 | MYSQL_ROOT_PASSWORD: "${DB_PASSWORD-rootpass}" 10 | MYSQL_DATABASE: "${DB_DATABASE-database}" 11 | MYSQL_USER: "${DB_USER-user}" 12 | MYSQL_PASSWORD: "${DB_PASSWORD-pass}" 13 | MYSQL_ALLOW_EMPTY_PASSWORD: 1 14 | volumes: 15 | - database:/var/lib/mysql:delegated 16 | networks: 17 | - kool_local 18 | healthcheck: 19 | test: ["CMD", "mysqladmin", "ping"] 20 | 21 | volumes: 22 | database: 23 | 24 | scripts: 25 | mysql: kool exec -e MYSQL_PWD=$DB_PASSWORD database mysql -u $DB_USER $DB_DATABASE 26 | -------------------------------------------------------------------------------- /core/network/fake_network_test.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | ) 7 | 8 | func TestFakeHandler(t *testing.T) { 9 | f := &FakeHandler{} 10 | 11 | _ = f.HandleGlobalNetwork("testing_network") 12 | 13 | if !f.CalledHandleGlobalNetwork || f.NetworkNameArg != "testing_network" { 14 | t.Error("failed to use mocked HandleGlobalNetwork function on FakeHandler") 15 | } 16 | } 17 | 18 | func TestFailedFakeChecker(t *testing.T) { 19 | f := &FakeHandler{MockError: errors.New("fake error")} 20 | 21 | err := f.HandleGlobalNetwork("testing_network") 22 | 23 | if err == nil { 24 | t.Error("failed to use mocked failed HandleGlobalNetwork function on FakeHandler") 25 | } else if err.Error() != "fake error" { 26 | t.Error("failed to use mocked failed HandleGlobalNetwork function on FakeHandler") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /core/shell/terminal.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import "github.com/moby/term" 4 | 5 | const standardTermWidth int = 80 6 | 7 | // TerminalChecker holds logic to check if environment is a terminal 8 | type TerminalChecker interface { 9 | IsTerminal(...interface{}) bool 10 | } 11 | 12 | // DefaultTerminalChecker holds logic to check if file descriptors are a TTY 13 | type DefaultTerminalChecker struct{} 14 | 15 | // IsTerminal checks if input is a terminal 16 | func (t *DefaultTerminalChecker) IsTerminal(fds ...interface{}) bool { 17 | for i := range fds { 18 | if _, isTerminal := term.GetFdInfo(fds[i]); !isTerminal { 19 | return false 20 | } 21 | } 22 | 23 | return true 24 | } 25 | 26 | // NewTerminalChecker creates a new terminal checker 27 | func NewTerminalChecker() TerminalChecker { 28 | return &DefaultTerminalChecker{} 29 | } 30 | -------------------------------------------------------------------------------- /presets/symfony/config.yml: -------------------------------------------------------------------------------- 1 | # Which tags are related to this preset; used for branching the choices on preset wizard 2 | tags: [ 'PHP' ] 3 | 4 | name: 'Symfony' 5 | 6 | # Create defines the workflow for creating a new Project where this preset can then be installed 7 | create: 8 | - name: Creating new Symfony Application 9 | actions: 10 | - recipe: create-symfony 11 | 12 | # Preset defines the workflow for installing this preset in the current working directory 13 | preset: 14 | - name: 'Copy basic config files' 15 | actions: 16 | - copy: docker-compose.yml 17 | - copy: kool.yml 18 | - merge: symfony-scripts.yml 19 | dst: kool.yml 20 | 21 | - name: 'Customize your setup' 22 | actions: 23 | - recipe: pick-php 24 | - recipe: pick-db 25 | - recipe: pick-cache 26 | - recipe: pick-node-pkg-mgr 27 | -------------------------------------------------------------------------------- /docs/05-Commands-Reference/kool-cloud-help.md: -------------------------------------------------------------------------------- 1 | ## kool cloud help 2 | 3 | Help about any command 4 | 5 | ### Synopsis 6 | 7 | Help provides help for any command in the application. 8 | Simply type cloud help [path to command] for full details. 9 | 10 | ``` 11 | kool cloud help [command] [flags] 12 | ``` 13 | 14 | ### Options 15 | 16 | ``` 17 | -h, --help help for help 18 | ``` 19 | 20 | ### Options inherited from parent commands 21 | 22 | ``` 23 | --domain string Environment domain name to deploy to 24 | --token string Token to authenticate with Kool.dev Cloud API 25 | --verbose Increases output verbosity 26 | -w, --working_dir string Changes the working directory for the command 27 | ``` 28 | 29 | ### SEE ALSO 30 | 31 | * [kool cloud](kool_cloud) - Interact with Kool.dev Cloud and manage your deployments. 32 | 33 | -------------------------------------------------------------------------------- /presets/codeigniter/config.yml: -------------------------------------------------------------------------------- 1 | # Which tags are related to this preset; used for branching the choices on preset wizard 2 | tags: [ 'PHP' ] 3 | 4 | name: 'CodeIgniter' 5 | 6 | # Create defines the workflow for creating a new Project where this preset can then be installed 7 | create: 8 | - name: Creating new CodeIgniter Application 9 | actions: 10 | - recipe: create-codeigniter 11 | 12 | # Preset defines the workflow for installing this preset in the current working directory 13 | preset: 14 | - name: 'Copy basic config files' 15 | actions: 16 | - copy: docker-compose.yml 17 | - copy: kool.yml 18 | - merge: codeigniter-scripts.yml 19 | dst: kool.yml 20 | 21 | - name: 'Customize your setup' 22 | actions: 23 | - recipe: pick-php 24 | - recipe: pick-db 25 | - recipe: pick-cache 26 | - recipe: pick-node-pkg-mgr 27 | -------------------------------------------------------------------------------- /presets/laravel/config.yml: -------------------------------------------------------------------------------- 1 | # Which tags are related to this preset; used for branching the choices on preset wizard 2 | tags: [ 'PHP' ] 3 | 4 | name: 'Laravel' 5 | 6 | # Create defines the workflow for creating a new Project where this preset can then be installed 7 | create: 8 | - name: Creating new Laravel Application 9 | actions: 10 | - recipe: create-laravel 11 | 12 | # Preset defines the workflow for installing this preset in the current working directory 13 | preset: 14 | - name: 'Copy basic config files' 15 | actions: 16 | - copy: docker-compose.yml 17 | - copy: kool.yml 18 | - copy: vite.config.js 19 | - merge: scripts/laravel.yml 20 | dst: kool.yml 21 | 22 | - name: 'Customize your setup' 23 | actions: 24 | - recipe: pick-php 25 | - recipe: pick-db 26 | - recipe: pick-cache 27 | - recipe: pick-laravel-node 28 | -------------------------------------------------------------------------------- /services/cloud/api/deploy_destroy.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // DeployDestroy holds data and logic for consuming the "destroy" endpoint 4 | type DeployDestroy struct { 5 | Endpoint 6 | } 7 | 8 | // DeployDestroyResponse holds data from the "destroy" endpoint 9 | type DeployDestroyResponse struct { 10 | Environment struct { 11 | ID int `json:"id"` 12 | } `json:"environment"` 13 | } 14 | 15 | // NewDeployDestroy creates a new caller for Deploy API exec endpoint 16 | func NewDeployDestroy() (d *DeployDestroy) { 17 | d = &DeployDestroy{ 18 | Endpoint: NewDefaultEndpoint("DELETE"), 19 | } 20 | 21 | d.SetPath("deploy/destroy") 22 | 23 | return 24 | } 25 | 26 | // Call performs the request to the endpoint 27 | func (s *DeployDestroy) Call() (resp *DeployDestroyResponse, err error) { 28 | resp = &DeployDestroyResponse{} 29 | s.SetResponseReceiver(resp) 30 | err = s.DoCall() 31 | return 32 | } 33 | -------------------------------------------------------------------------------- /docs/05-Commands-Reference/kool-share.md: -------------------------------------------------------------------------------- 1 | ## kool share 2 | 3 | Live share your local environment on the Internet using an HTTP tunnel 4 | 5 | ``` 6 | kool share 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for share 13 | --port uint The port from the target service that should be shared. If not provided, it will default to port 80. 14 | --service string The name of the local service container you want to share. (default "app") 15 | --subdomain string The subdomain used to generate your public https://subdomain.kool.live URL. 16 | ``` 17 | 18 | ### Options inherited from parent commands 19 | 20 | ``` 21 | --verbose Increases output verbosity 22 | -w, --working_dir string Changes the working directory for the command 23 | ``` 24 | 25 | ### SEE ALSO 26 | 27 | * [kool](kool) - Cloud native environments made easy 28 | 29 | -------------------------------------------------------------------------------- /presets/nodejs/config.yml: -------------------------------------------------------------------------------- 1 | # Which tags are related to this preset; used for branching the choices on preset wizard 2 | tags: [ 'Javascript' ] 3 | 4 | name: 'Vanilla Node.js' 5 | 6 | # Create defines the workflow for creating a new Project where this preset can then be installed 7 | create: 8 | - name: Creating new Node.js application 9 | actions: 10 | - scripts: 11 | - mkdir $CREATE_DIRECTORY 12 | 13 | # Preset defines the workflow for installing this preset in the current working directory 14 | preset: 15 | - name: 'Copy basic config files' 16 | actions: 17 | - copy: docker-compose.yml 18 | - copy: kool.yml 19 | - copy: app.js 20 | - merge: node-scripts.yml 21 | dst: kool.yml 22 | 23 | - name: 'Customize your setup' 24 | actions: 25 | - scripts: 26 | - kool docker kooldev/node:20 npm init -f 27 | - recipe: pick-node-pkg-mgr 28 | -------------------------------------------------------------------------------- /presets/php/config.yml: -------------------------------------------------------------------------------- 1 | # Which tags are related to this preset; used for branching the choices on preset wizard 2 | tags: [ 'PHP' ] 3 | 4 | # Name of the preset 5 | name: 'Vanilla PHP (Nginx+PHP-FPM)' 6 | 7 | # Create defines the workflow for creating a new Project where this preset can then be installed 8 | create: 9 | - name: Creating new PHP Application 10 | actions: 11 | - scripts: 12 | - mkdir -p $CREATE_DIRECTORY/public 13 | - echo " $CREATE_DIRECTORY/public/index.php 14 | 15 | # Preset defines the workflow for installing this preset in the current working directory 16 | preset: 17 | - name: 'Copy basic config files' 18 | actions: 19 | - copy: docker-compose.yml 20 | - copy: kool.yml 21 | - merge: scripts/php.yml 22 | dst: kool.yml 23 | 24 | - name: 'Customize your setup' 25 | actions: 26 | - recipe: pick-php 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug, help wanted 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | A clear and concise description of what the bug is. Please make sure you didn't find an existing issue with the same subject. 13 | 14 | **Kool version and environment** 15 | 16 | Please provide the version you are running (`kool -v`), which environment (OS, shell, docker, DinD), and also what environment variables you are using (`kool info`). 17 | 18 | **To Reproduce** 19 | 20 | Steps to reproduce the behavior: 21 | 22 | **Expected behavior** 23 | 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots** 27 | 28 | If applicable, add screenshots to help explain your problem. 29 | 30 | **Additional context** 31 | 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release process 2 | 3 | For generating a new release we follow the steps: 4 | 5 | - Get latest `main` branch. 6 | - Make sure docs are updated - `kool run make-docs` 7 | - Make sure formatting is correct - `kool run fmt` 8 | - Make sure there are no syntax/stylistic errors - `kool run lint` 9 | - Make sure tests are passing - `kool run test` 10 | - Pick the version name you wanna build - `export BUILD_VERSION=0.0.0` (taking into consideration [Semantic Versioning rules for Major, Minor and Patch versions](https://semver.org/#summary)) 11 | - Build all artifacts - `bash build_artifacts.sh` 12 | - Review if documentation is updated accordingly (docs/) 13 | - Upload to the existing draft release all the artifacts built at dist/ folder. 14 | - Publish the new version (which will create the tag as well) 15 | - In case of version bumping, check if we need to update `SECURITY.md` to show what version we currently support. 16 | -------------------------------------------------------------------------------- /core/presets/parser_test.go: -------------------------------------------------------------------------------- 1 | package presets 2 | 3 | import ( 4 | "embed" 5 | "testing" 6 | 7 | "github.com/leaanthony/debme" 8 | ) 9 | 10 | //go:embed fixtures/* 11 | var fixtures embed.FS 12 | 13 | func TestPresetParser(t *testing.T) { 14 | root, _ := debme.FS(fixtures, "fixtures") 15 | 16 | SetSource(root) 17 | 18 | m := make(map[string]bool) 19 | p := NewParser() 20 | 21 | for _, t := range p.GetTags() { 22 | m[t] = true 23 | } 24 | 25 | if !m["foo"] { 26 | t.Errorf("missing tag 'foo'; %+v", p.GetTags()) 27 | } 28 | 29 | if !p.Exists("foo") { 30 | t.Error("preset 'foo' should exist") 31 | } else if p.Exists("bar") { 32 | t.Error("preset 'bar' should not exist") 33 | } 34 | 35 | if len(p.GetPresets("foo")) != 1 { 36 | t.Error("should have found 1 preset with tag foo") 37 | } 38 | 39 | if len(p.GetPresets("bar")) != 0 { 40 | t.Error("should NOT have found any preset with tag bar") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /services/cloud/api/deploy_ping.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "fmt" 4 | 5 | // DeployPingResponse holds data returned from the deploy endpoint 6 | type DeployPingResponse struct { 7 | ID int `json:"id"` 8 | Status string `json:"status"` 9 | } 10 | 11 | // DeployPing consumes the API endpoint to create a new deployment 12 | type DeployPing struct { 13 | Endpoint 14 | } 15 | 16 | // NewDeployPing creates a new DeployStart instance 17 | func NewDeployPing(created *DeployCreateResponse) (c *DeployPing) { 18 | c = &DeployPing{ 19 | Endpoint: NewDefaultEndpoint("POST"), 20 | } 21 | 22 | c.SetPath("deploy/ping") 23 | c.Body().Set("id", fmt.Sprintf("%d", created.Deploy.ID)) 24 | 25 | return 26 | } 27 | 28 | // Run calls deploy/ping in the Kool Dev API 29 | func (c *DeployPing) Run() (resp *DeployPingResponse, err error) { 30 | resp = &DeployPingResponse{} 31 | c.SetResponseReceiver(resp) 32 | err = c.DoCall() 33 | return 34 | } 35 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | | Issue | # | 10 | | -----: | :-----: | 11 | | :beetle: Bug Fix | No/Yes | 12 | | :toolbox: Improvement | No/Yes | 13 | | :trophy: Feature | No/Yes | 14 | | :pencil: Refactor | No/Yes | 15 | | :x: Removed | No/Yes | 16 | | :open_book: Documentation | No/Yes | 17 | | :warning: Break Change | No/Yes | 18 | 19 | **Description** 20 | 21 | 22 | --- 23 | 24 | **Notes** 25 | 26 | 27 | -------------------------------------------------------------------------------- /core/utils/loading.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "time" 7 | 8 | "github.com/briandowns/spinner" 9 | ) 10 | 11 | func MakeFastLoading(loadingMsg, loadedMsg string, w io.Writer) *spinner.Spinner { 12 | return makeLoading(loadingMsg, loadedMsg, 14, w) 13 | } 14 | 15 | func MakeSlowLoading(loadingMsg, loadedMsg string, w io.Writer) *spinner.Spinner { 16 | return makeLoading(loadingMsg, loadedMsg, 21, w) 17 | } 18 | 19 | func makeLoading(loadingMsg, loadedMsg string, charset int, w io.Writer) (s *spinner.Spinner) { 20 | s = spinner.New( 21 | spinner.CharSets[charset], 22 | 100*time.Millisecond, 23 | // spinner.WithColor("green"), 24 | spinner.WithFinalMSG(loadedMsg+"\n"), 25 | spinner.WithSuffix(" "+loadingMsg), 26 | ) 27 | s.Prefix = " " 28 | s.HideCursor = true 29 | if fh, isFh := w.(*os.File); isFh { 30 | s.WriterFile = fh 31 | } else { 32 | s.Writer = w 33 | } 34 | s.Start() 35 | return 36 | } 37 | -------------------------------------------------------------------------------- /docs/05-Commands-Reference/kool-start.md: -------------------------------------------------------------------------------- 1 | ## kool start 2 | 3 | Start service containers defined in docker-compose.yml 4 | 5 | ### Synopsis 6 | 7 | Start one or more specified [SERVICE] containers. If no [SERVICE] is provided, 8 | all containers are started. If the containers are already running, they are recreated. 9 | 10 | ``` 11 | kool start [SERVICE...] 12 | ``` 13 | 14 | ### Options 15 | 16 | ``` 17 | -f, --foreground Start containers in foreground mode 18 | -h, --help help for start 19 | --profile string Specify a profile to enable 20 | -b, --rebuild Updates and builds service's images 21 | ``` 22 | 23 | ### Options inherited from parent commands 24 | 25 | ``` 26 | --verbose Increases output verbosity 27 | -w, --working_dir string Changes the working directory for the command 28 | ``` 29 | 30 | ### SEE ALSO 31 | 32 | * [kool](kool) - Cloud native environments made easy 33 | 34 | -------------------------------------------------------------------------------- /core/shell/prompt_input.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "github.com/AlecAivazis/survey/v2" 5 | "github.com/AlecAivazis/survey/v2/terminal" 6 | ) 7 | 8 | // PromptInput contract that holds logic for prompt an input string 9 | type PromptInput interface { 10 | Input(string, string) (string, error) 11 | } 12 | 13 | // DefaultPromptInput holds data for prompting a select question 14 | type DefaultPromptInput struct{} 15 | 16 | // NewPromptInput creates a new prompt select 17 | func NewPromptInput() PromptInput { 18 | return &DefaultPromptInput{} 19 | } 20 | 21 | // Ask prompt to the user a select question 22 | func (p *DefaultPromptInput) Input(question string, defaultInput string) (input string, err error) { 23 | prompt := &survey.Input{ 24 | Message: question, 25 | Default: defaultInput, 26 | } 27 | if err = survey.AskOne(prompt, &input); err != nil && err == terminal.InterruptErr { 28 | err = ErrUserCancelled 29 | } 30 | return 31 | } 32 | -------------------------------------------------------------------------------- /services/cloud/api/deploy_start.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "fmt" 4 | 5 | // DeployStartResponse holds data returned from the deploy endpoint 6 | type DeployStartResponse struct { 7 | ID int `json:"id"` 8 | Status string `json:"status"` 9 | } 10 | 11 | // DeployStart consumes the API endpoint to create a new deployment 12 | type DeployStart struct { 13 | Endpoint 14 | } 15 | 16 | // NewDeployStart creates a new DeployStart instance 17 | func NewDeployStart(created *DeployCreateResponse) (c *DeployStart) { 18 | c = &DeployStart{ 19 | Endpoint: NewDefaultEndpoint("POST"), 20 | } 21 | 22 | c.SetPath("deploy/start") 23 | c.Body().Set("id", fmt.Sprintf("%d", created.Deploy.ID)) 24 | 25 | return 26 | } 27 | 28 | // Run calls deploy/create in the Kool Dev API 29 | func (c *DeployStart) Run() (resp *DeployStartResponse, err error) { 30 | resp = &DeployStartResponse{} 31 | c.SetResponseReceiver(resp) 32 | err = c.DoCall() 33 | return 34 | } 35 | -------------------------------------------------------------------------------- /services/cloud/api/deploy_exec.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // DeployExec holds data and logic for consuming the "exec" endpoint 4 | type DeployExec struct { 5 | Endpoint 6 | } 7 | 8 | // DeployExecResponse holds data from the "exec" endpoint 9 | type DeployExecResponse struct { 10 | Server string `json:"server"` 11 | Namespace string `json:"namespace"` 12 | Path string `json:"path"` 13 | Token string `json:"token"` 14 | CA string `json:"ca.crt"` 15 | } 16 | 17 | // NewDeployExec creates a new caller for Deploy API exec endpoint 18 | func NewDeployExec() (e *DeployExec) { 19 | e = &DeployExec{ 20 | Endpoint: NewDefaultEndpoint("POST"), 21 | } 22 | 23 | e.SetPath("deploy/exec") 24 | 25 | return e 26 | } 27 | 28 | // Call performs the request to the endpoint 29 | func (s *DeployExec) Call() (resp *DeployExecResponse, err error) { 30 | resp = &DeployExecResponse{} 31 | s.SetResponseReceiver(resp) 32 | err = s.DoCall() 33 | return 34 | } 35 | -------------------------------------------------------------------------------- /services/cloud/api/deploy_status.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "fmt" 4 | 5 | // DeployCreate consumes the API endpoint to create a new deployment 6 | type DeployStatus struct { 7 | Endpoint 8 | } 9 | 10 | // DeployCreateResponse holds data returned from the deploy endpoint 11 | type DeployStatusResponse struct { 12 | ID int `json:"id"` 13 | Status string `json:"status"` 14 | } 15 | 16 | // NewDeployCreate creates a new DeployCreate instance 17 | func NewDeployStatus(created *DeployCreateResponse) (c *DeployStatus) { 18 | c = &DeployStatus{ 19 | Endpoint: NewDefaultEndpoint("GET"), 20 | } 21 | 22 | c.SetPath(fmt.Sprintf("deploy/%d/status", created.Deploy.ID)) 23 | 24 | return 25 | } 26 | 27 | // Run calls deploy/?/status in the Kool Dev API 28 | func (c *DeployStatus) Run() (resp *DeployStatusResponse, err error) { 29 | resp = &DeployStatusResponse{} 30 | 31 | c.SetResponseReceiver(resp) 32 | 33 | err = c.DoCall() 34 | 35 | return 36 | } 37 | -------------------------------------------------------------------------------- /core/automate/recipes.go: -------------------------------------------------------------------------------- 1 | package automate 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "io/fs" 7 | "strings" 8 | 9 | "gopkg.in/yaml.v2" 10 | ) 11 | 12 | type RecipeMetadata struct { 13 | Title string `yaml:"title"` 14 | Slug string 15 | } 16 | 17 | var recipesSource embed.FS 18 | 19 | func SetRecipesSource(src embed.FS) { 20 | recipesSource = src 21 | } 22 | 23 | func GetRecipes() (recipes []*RecipeMetadata, err error) { 24 | var ( 25 | entries []fs.DirEntry 26 | raw []byte 27 | ) 28 | 29 | if entries, err = recipesSource.ReadDir("recipes"); err != nil { 30 | return 31 | } 32 | 33 | for _, e := range entries { 34 | m := &RecipeMetadata{Slug: strings.ReplaceAll(e.Name(), ".yml", "")} 35 | if raw, err = recipesSource.ReadFile(fmt.Sprintf("recipes/%s", e.Name())); err != nil { 36 | return 37 | } 38 | if err = yaml.Unmarshal(raw, m); err != nil { 39 | recipes = nil 40 | return 41 | } 42 | recipes = append(recipes, m) 43 | } 44 | return 45 | } 46 | -------------------------------------------------------------------------------- /docs/05-Commands-Reference/kool-logs.md: -------------------------------------------------------------------------------- 1 | ## kool logs 2 | 3 | Display log output from running service containers 4 | 5 | ### Synopsis 6 | 7 | Display log output from all running service containers, 8 | or one or more specified [SERVICE...] containers. Add a '-f' option to the 9 | the command to follow the log output (i.e. 'kool logs -f [SERVICE...]'). 10 | 11 | ``` 12 | kool logs [OPTIONS] [SERVICE...] 13 | ``` 14 | 15 | ### Options 16 | 17 | ``` 18 | -f, --follow Follow log output. 19 | -h, --help help for logs 20 | -t, --tail int Number of lines to show from the end of the logs for each container. A value equal to 0 will show all lines. (default 25) 21 | ``` 22 | 23 | ### Options inherited from parent commands 24 | 25 | ``` 26 | --verbose Increases output verbosity 27 | -w, --working_dir string Changes the working directory for the command 28 | ``` 29 | 30 | ### SEE ALSO 31 | 32 | * [kool](kool) - Cloud native environments made easy 33 | 34 | -------------------------------------------------------------------------------- /presets/nest+next/nest+next-docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | frontend: 3 | image: kooldev/node:20 4 | command: ["npm", "run", "dev", "--", "-p", "${KOOL_FRONTEND_PORT:-80}"] 5 | ports: 6 | - "${KOOL_FRONTEND_PORT:-80}:${KOOL_FRONTEND_PORT:-80}" 7 | environment: 8 | ASUSER: "${KOOL_ASUSER:-0}" 9 | UID: "${UID:-0}" 10 | KOOL_FRONTEND_PORT: "${KOOL_FRONTEND_PORT:-80}" 11 | volumes: 12 | - .:/app:delegated 13 | working_dir: /app/frontend 14 | networks: 15 | - kool_local 16 | - kool_global 17 | backend: 18 | image: kooldev/node:20 19 | command: ["npm", "run", "start:dev"] 20 | ports: 21 | - "${KOOL_BACKEND_PORT:-81}:${KOOL_BACKEND_PORT:-81}" 22 | environment: 23 | ASUSER: "${KOOL_ASUSER:-0}" 24 | UID: "${UID:-0}" 25 | KOOL_BACKEND_PORT: "${KOOL_BACKEND_PORT:-81}" 26 | volumes: 27 | - ./backend:/app:delegated 28 | networks: 29 | - kool_local 30 | - kool_global 31 | -------------------------------------------------------------------------------- /core/shell/terminal_test.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "io" 5 | "os/exec" 6 | "testing" 7 | 8 | "github.com/creack/pty" 9 | ) 10 | 11 | func TestPipeIsTerminal(t *testing.T) { 12 | c1 := exec.Command("echo", "testing") 13 | c2 := exec.Command("cat") 14 | 15 | r, w := io.Pipe() 16 | 17 | c1.Stdout = w 18 | c2.Stdin = r 19 | 20 | _ = c1.Start() 21 | _ = c2.Start() 22 | go func() { 23 | defer w.Close() 24 | 25 | _ = c1.Wait() 26 | }() 27 | _ = c2.Wait() 28 | 29 | terminalChecker := NewTerminalChecker() 30 | 31 | if terminalChecker.IsTerminal(c1.Stdin, c1.Stdout) { 32 | t.Error("unexpected tty terminal on piped command") 33 | } 34 | } 35 | 36 | func TestPtyIsTerminal(t *testing.T) { 37 | c := exec.Command("echo", "testing") 38 | f, err := pty.Start(c) 39 | 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | terminalChecker := NewTerminalChecker() 45 | 46 | if !terminalChecker.IsTerminal(f) { 47 | t.Error("expecting tty terminal on command") 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /presets/nestjs/config.yml: -------------------------------------------------------------------------------- 1 | # Which tags are related to this preset; used for branching the choices on preset wizard 2 | tags: [ 'Typescript', 'Javascript' ] 3 | 4 | name: 'NestJS' 5 | 6 | # Create defines the workflow for creating a new Project where this preset can then be installed 7 | create: 8 | - name: Creating new NestJS Application 9 | actions: 10 | - scripts: 11 | - docker pull -q kooldev/node:20 12 | - recipe: create-nestjs 13 | 14 | # Preset defines the workflow for installing this preset in the current working directory 15 | preset: 16 | - name: 'Copy basic config files' 17 | actions: 18 | - copy: docker-compose.yml 19 | - copy: kool.yml 20 | - merge: docker-compose.nestjs.yml 21 | dst: docker-compose.yml 22 | - merge: kool.nestjs.yml 23 | dst: kool.yml 24 | - copy: env.dist 25 | dst: .env.dist 26 | 27 | - name: 'Customize your setup' 28 | actions: 29 | - recipe: pick-db 30 | - recipe: pick-cache 31 | -------------------------------------------------------------------------------- /commands/fake_kool_service.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "kool-dev/kool/core/shell" 5 | ) 6 | 7 | // FakeKoolService is a mock to be used on testing/replacement for KoolService interface 8 | type FakeKoolService struct { 9 | shell *shell.FakeShell 10 | 11 | ArgsExecute []string 12 | CalledExecute bool 13 | MockExecuteErr error 14 | } 15 | 16 | func newFakeKoolService() *FakeKoolService { 17 | return &FakeKoolService{ 18 | &shell.FakeShell{MockIsTerminal: true}, nil, false, nil, 19 | } 20 | } 21 | 22 | // Shell returns the stored (possibly fake) Shell implementation 23 | func (f *FakeKoolService) Shell() shell.Shell { 24 | if f.shell == nil { 25 | // workaround interface nil-nil requirement 26 | // for type/value 27 | return nil 28 | } 29 | 30 | return f.shell 31 | } 32 | 33 | // Execute mocks the function for testing 34 | func (f *FakeKoolService) Execute(args []string) (err error) { 35 | f.ArgsExecute = args 36 | f.CalledExecute = true 37 | err = f.MockExecuteErr 38 | return 39 | } 40 | -------------------------------------------------------------------------------- /presets/wordpress/config.yml: -------------------------------------------------------------------------------- 1 | # Which tags are related to this preset; used for branching the choices on preset wizard 2 | tags: [ 'PHP' ] 3 | 4 | name: 'Wordpress' 5 | 6 | # Create defines the workflow for creating a new Project where this preset can then be installed 7 | create: 8 | - name: Creating new Wordpress Application 9 | actions: 10 | - scripts: 11 | - mkdir $CREATE_DIRECTORY 12 | 13 | # Preset defines the workflow for installing this preset in the current working directory 14 | preset: 15 | - name: 'Copy basic config files' 16 | actions: 17 | - copy: docker-compose.yml 18 | - copy: kool.yml 19 | - copy: environment 20 | dst: .env 21 | # default PHP 8 only 22 | - merge: app/wordpress80.yml 23 | dst: docker-compose.yml 24 | - merge: scripts/wordpress.yml 25 | dst: kool.yml 26 | 27 | - name: 'Customize your setup' 28 | actions: 29 | - recipe: pick-db 30 | - recipe: pick-cache 31 | - recipe: pick-node-pkg-mgr 32 | -------------------------------------------------------------------------------- /services/cloud/api/errors_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestDefinedErrors(t *testing.T) { 8 | errs := map[string]error{ 9 | "ErrBadAPIServer": ErrBadAPIServer, 10 | "ErrDeployFailed": ErrDeployFailed, 11 | "ErrUnauthorized": ErrUnauthorized, 12 | "ErrPayloadValidation": ErrPayloadValidation, 13 | "ErrBadResponseStatus": ErrBadResponseStatus, 14 | "ErrUnexpectedResponse": ErrUnexpectedResponse, 15 | "ErrMissingToken": ErrMissingToken, 16 | } 17 | 18 | for e, v := range errs { 19 | if v == nil { 20 | t.Errorf("default error not defined: %s", e) 21 | } 22 | } 23 | } 24 | 25 | func TestApiErr(t *testing.T) { 26 | err := &ErrAPI{100, "message", nil} 27 | 28 | if err.Error() != "\n100 - message\n" { 29 | t.Errorf("unexpected error message: %s", err.Error()) 30 | } 31 | 32 | err.Errors = map[string]interface{}{ 33 | "foo": []interface{}{"bar"}, 34 | } 35 | 36 | if err.Error() != "\n100 - message\n\n\tfoo > bar\n" { 37 | t.Errorf("unexpected error message: %s", err.Error()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /core/shell/fake_prompt_select.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | // FakePromptSelect holds data for fake prompt select behavior 4 | type FakePromptSelect struct { 5 | CalledAsk bool 6 | MockAnswer map[string]string 7 | MockError map[string]error 8 | 9 | CalledConfirm []*struct { 10 | question string 11 | args []any 12 | } 13 | MockConfirm map[string]bool 14 | MockConfirmError map[string]error 15 | } 16 | 17 | // Ask mocked behavior for testing prompting a select question 18 | func (f *FakePromptSelect) Ask(question string, options []string) (answer string, err error) { 19 | f.CalledAsk = true 20 | answer = f.MockAnswer[question] 21 | err = f.MockError[question] 22 | return 23 | } 24 | 25 | // Confirm mocked behavior for testing prompting a confirm question 26 | func (f *FakePromptSelect) Confirm(question string, args ...any) (confirmed bool, err error) { 27 | f.CalledConfirm = append(f.CalledConfirm, &struct { 28 | question string 29 | args []any 30 | }{question, args}) 31 | confirmed = f.MockConfirm[question] 32 | err = f.MockConfirmError[question] 33 | return 34 | } 35 | -------------------------------------------------------------------------------- /docs/05-Commands-Reference/kool-cloud-deploy.md: -------------------------------------------------------------------------------- 1 | ## kool cloud deploy 2 | 3 | Deploy a local application to a Kool.dev Cloud environment 4 | 5 | ``` 6 | kool cloud deploy 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | --domain-extra stringArray List of extra domain aliases 13 | -h, --help help for deploy 14 | --platform string Platform for docker build (default: linux/amd64) (default "linux/amd64") 15 | --timeout uint Timeout in minutes for waiting the deployment to finish 16 | --www-redirect Redirect www to non-www domain 17 | ``` 18 | 19 | ### Options inherited from parent commands 20 | 21 | ``` 22 | --domain string Environment domain name to deploy to 23 | --token string Token to authenticate with Kool.dev Cloud API 24 | --verbose Increases output verbosity 25 | -w, --working_dir string Changes the working directory for the command 26 | ``` 27 | 28 | ### SEE ALSO 29 | 30 | * [kool cloud](kool_cloud) - Interact with Kool.dev Cloud and manage your deployments. 31 | 32 | -------------------------------------------------------------------------------- /docs/05-Commands-Reference/kool-cloud-exec.md: -------------------------------------------------------------------------------- 1 | ## kool cloud exec 2 | 3 | Execute a command inside a running service container deployed to Kool.dev Cloud 4 | 5 | ### Synopsis 6 | 7 | After deploying an application to Kool.dev Cloud using 'kool deploy', 8 | execute a COMMAND inside the specified SERVICE container (similar to an SSH session). 9 | Must use a KOOL_API_TOKEN environment variable for authentication. 10 | 11 | ``` 12 | kool cloud exec SERVICE [COMMAND] [--] [ARG...] 13 | ``` 14 | 15 | ### Options 16 | 17 | ``` 18 | -c, --container string Container target. (default "default") 19 | -h, --help help for exec 20 | ``` 21 | 22 | ### Options inherited from parent commands 23 | 24 | ``` 25 | --domain string Environment domain name to deploy to 26 | --token string Token to authenticate with Kool.dev Cloud API 27 | --verbose Increases output verbosity 28 | -w, --working_dir string Changes the working directory for the command 29 | ``` 30 | 31 | ### SEE ALSO 32 | 33 | * [kool cloud](kool_cloud) - Interact with Kool.dev Cloud and manage your deployments. 34 | 35 | -------------------------------------------------------------------------------- /commands/kool_service.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "kool-dev/kool/core/shell" 5 | ) 6 | 7 | // KoolService interface holds the contract for a 8 | // general service which implements some bigger chunk 9 | // of logic usually linked to a command. 10 | type KoolService interface { 11 | Execute([]string) error 12 | 13 | Shell() shell.Shell 14 | } 15 | 16 | // DefaultKoolService holds handlers and functions shared by all 17 | // services, meant to be used on commands when executing the services. 18 | type DefaultKoolService struct { 19 | shell shell.Shell 20 | } 21 | 22 | func newDefaultKoolService() *DefaultKoolService { 23 | return &DefaultKoolService{ 24 | shell.NewShell(), 25 | } 26 | } 27 | 28 | // Shell exposes the attached shell implementation 29 | func (k *DefaultKoolService) Shell() shell.Shell { 30 | return k.shell 31 | } 32 | 33 | // Fake changes the internal dependencies (most notably shell) 34 | // to be the fake conterpart of the real implementation. 35 | // Meant for tests only. 36 | func (k *DefaultKoolService) Fake() *DefaultKoolService { 37 | k.shell = &shell.FakeShell{MockIsTerminal: true} 38 | return k 39 | } 40 | -------------------------------------------------------------------------------- /docs/05-Commands-Reference/kool-docker.md: -------------------------------------------------------------------------------- 1 | ## kool docker 2 | 3 | Create a new container (a powered up 'docker run') 4 | 5 | ### Synopsis 6 | 7 | A helper for 'docker run'. Any [OPTIONS] added before the 8 | IMAGE name will be used by 'docker run' itself (i.e. --env='VAR=VALUE'). 9 | Add an optional [COMMAND] to execute on the IMAGE, and use [--] after 10 | the [COMMAND] to provide optional arguments required by the COMMAND. 11 | 12 | ``` 13 | kool docker [OPTIONS] IMAGE [COMMAND] [--] [ARG...] 14 | ``` 15 | 16 | ### Options 17 | 18 | ``` 19 | -e, --env stringArray Environment variables. 20 | -h, --help help for docker 21 | -n, --network stringArray Connect a container to a network. 22 | -p, --publish stringArray Publish a container's port(s) to the host. 23 | -v, --volume stringArray Bind mount a volume. 24 | ``` 25 | 26 | ### Options inherited from parent commands 27 | 28 | ``` 29 | --verbose Increases output verbosity 30 | -w, --working_dir string Changes the working directory for the command 31 | ``` 32 | 33 | ### SEE ALSO 34 | 35 | * [kool](kool) - Cloud native environments made easy 36 | 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Kool Dev Sistemas de Informacao LTDA 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /presets/expressjs/config.yml: -------------------------------------------------------------------------------- 1 | # Which tags are related to this preset; used for branching the choices on preset wizard 2 | tags: [ 'Javascript' ] 3 | 4 | name: 'ExpressJS' 5 | 6 | # Create defines the workflow for creating a new Project where this preset can then be installed 7 | create: 8 | - name: Creating new Express Application 9 | actions: 10 | - scripts: 11 | - mkdir $CREATE_DIRECTORY 12 | 13 | # Preset defines the workflow for installing this preset in the current working directory 14 | preset: 15 | - name: 'Copy basic config files' 16 | actions: 17 | - copy: docker-compose.yml 18 | - copy: kool.yml 19 | - copy: app.js 20 | - copy: package.json 21 | 22 | - name: 'Customize your setup' 23 | actions: 24 | # define package manager 25 | - prompt: Which javascript package manager do you want to use? 26 | default: 'npm' 27 | options: 28 | - name: 'npm' 29 | actions: 30 | - merge: scripts/npm-expressjs.yml 31 | dst: kool.yml 32 | - name: 'yarn' 33 | actions: 34 | - merge: scripts/yarn-expressjs.yml 35 | dst: kool.yml 36 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | name: go test 7 | jobs: 8 | test: 9 | strategy: 10 | matrix: 11 | go-version: [1.21.x] 12 | os: [ubuntu-latest, macos-latest] 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - name: Install Go 16 | uses: actions/setup-go@v2 17 | with: 18 | go-version: ${{ matrix.go-version }} 19 | - name: Checkout code 20 | uses: actions/checkout@v4 21 | - name: Caching 22 | uses: actions/cache@v4 23 | with: 24 | path: ~/go/pkg 25 | key: ${{ runner.os }}-kool-${{ hashFiles('**/go.sum') }} 26 | restore-keys: | 27 | ${{ runner.os }}-kool- 28 | - name: Download dependencies 29 | run: | 30 | go mod download 31 | go install 32 | - name: Test 33 | if: matrix.os == 'macos-latest' 34 | run: go test ./... 35 | - name: Test with code coverage 36 | if: matrix.os == 'ubuntu-latest' 37 | run: go test -race -coverprofile=coverage.txt -covermode=atomic ./... 38 | - name: Code Coverage 39 | if: matrix.os == 'ubuntu-latest' 40 | run: bash <(curl -s https://codecov.io/bash) 41 | -------------------------------------------------------------------------------- /core/network/network.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "fmt" 5 | "kool-dev/kool/core/builder" 6 | "kool-dev/kool/core/shell" 7 | ) 8 | 9 | // Handler defines network handler 10 | type Handler interface { 11 | HandleGlobalNetwork(string) error 12 | } 13 | 14 | // DefaultHandler holds docker network command 15 | type DefaultHandler struct { 16 | CheckNetworkCmd builder.Command 17 | CreateNetworkCmd builder.Command 18 | shell shell.Shell 19 | } 20 | 21 | // NewHandler initializes handler 22 | func NewHandler(s shell.Shell) *DefaultHandler { 23 | var checkNetCmd, createNetCmd *builder.DefaultCommand 24 | 25 | checkNetCmd = builder.NewCommand("docker", "network", "ls", "-q", "-f") 26 | createNetCmd = builder.NewCommand("docker", "network", "create", "--attachable") 27 | 28 | return &DefaultHandler{checkNetCmd, createNetCmd, s} 29 | } 30 | 31 | // HandleGlobalNetwork handles global network 32 | func (h *DefaultHandler) HandleGlobalNetwork(networkName string) error { 33 | if networkID, err := h.shell.Exec(h.CheckNetworkCmd, fmt.Sprintf("NAME=^%s$", networkName)); err != nil || networkID != "" { 34 | return err 35 | } 36 | 37 | return h.shell.Interactive(h.CreateNetworkCmd, networkName) 38 | } 39 | -------------------------------------------------------------------------------- /docs/02-Kool-Cloud/17-Custom-Resources.md: -------------------------------------------------------------------------------- 1 | # Custom Resources 2 | 3 | Most real-world web applications will go beyond just computing containers with your code — there's a vast myriad of other resources you will eventually need, like dedicated database services (i.e., RDS), replication, object storage (i.e., S3), CDN (i.e., CloudFront), just to name a few. 4 | 5 | The Kool.dev Cloud team is ready to set it up for you when needed — providing you the Infrastructure-as-Code to live close to your application. While this is not built into our panel for self-service usage yet, you can just [contact us via email with your needs](mailto:contact@kool.dev), and we will work through your request. 6 | 7 | ## Bring Your Own Cloud 8 | 9 | Important to notice - current cloud scenario is flexible enough where you can use Kool.dev Shared cloud, or hire a dedicated stack fully managed by Kool.dev, or even better - if you want total control and access we can set up the Kool.dev Cloud cluster and resources on your own AWS account. [Contact us](mailto:contact@kool.dev) to learn more about this possibility. 10 | 11 | ## Consulting 12 | 13 | If you lack the DevOps expertise to determine your needs, we also have consulting services available to best serve you. 14 | -------------------------------------------------------------------------------- /services/yamler/output.go: -------------------------------------------------------------------------------- 1 | package yamler 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | 9 | "gopkg.in/yaml.v3" 10 | ) 11 | 12 | type OutputWritter interface { 13 | WriteYAML(string, *yaml.Node) error 14 | } 15 | 16 | type DefaultOutputWritter struct{} 17 | 18 | func (o *DefaultOutputWritter) WriteYAML(filePath string, document *yaml.Node) (err error) { 19 | var ( 20 | buff = new(bytes.Buffer) 21 | encoder *yaml.Encoder 22 | file *os.File 23 | ) 24 | 25 | if document.Kind != yaml.DocumentNode { 26 | err = fmt.Errorf("unexpected yaml.Node; expected document (1), but got %d", document.Kind) 27 | return 28 | } 29 | 30 | encoder = yaml.NewEncoder(buff) 31 | encoder.SetIndent(2) 32 | 33 | if err = encoder.Encode(document); err != nil { 34 | return 35 | } 36 | 37 | if err = encoder.Close(); err != nil { 38 | return 39 | } 40 | 41 | if file, err = os.OpenFile(filePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.ModePerm); err != nil { 42 | return 43 | } 44 | 45 | if _, err = io.Copy(file, buff); err != nil { 46 | return 47 | } 48 | 49 | buff.Reset() 50 | buff = nil 51 | 52 | if err = file.Sync(); err != nil { 53 | return 54 | } 55 | 56 | err = file.Close() 57 | return 58 | } 59 | -------------------------------------------------------------------------------- /core/shell/fake_prompt_select_test.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | ) 7 | 8 | func TestFakePromptSelect(t *testing.T) { 9 | f := &FakePromptSelect{} 10 | f.MockAnswer = make(map[string]string) 11 | f.MockAnswer["question"] = "answer" 12 | 13 | answer, err := f.Ask("question", []string{"option"}) 14 | 15 | if err != nil { 16 | t.Errorf("unexpected error on Ask: %v", err) 17 | } 18 | 19 | if answer != "answer" { 20 | t.Errorf("expecting answer 'answer', got %s", answer) 21 | } 22 | 23 | f.MockError = make(map[string]error) 24 | f.MockError["question"] = errors.New("error") 25 | 26 | _, err = f.Ask("question", []string{"option"}) 27 | 28 | if err == nil { 29 | t.Errorf("should throw an error on Ask") 30 | } 31 | 32 | f.MockConfirm = make(map[string]bool) 33 | f.MockConfirm["question"] = true 34 | f.MockConfirmError = make(map[string]error) 35 | f.MockConfirmError["question"] = errors.New("error") 36 | 37 | if confirmed, err := f.Confirm("question"); err == nil || err.Error() != "error" || !confirmed { 38 | t.Errorf("bad return from mocked Confirm") 39 | } else if len(f.CalledConfirm) == 0 || f.CalledConfirm[0].question != "question" { 40 | t.Errorf("bad control of calls to mocked Confirm") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /core/parser/errors_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | ) 7 | 8 | func TestSimilars(t *testing.T) { 9 | err := &ErrPossibleTypo{[]string{"cmd1", "cmd2"}} 10 | 11 | similars := err.Similars() 12 | 13 | if len(similars) != 2 { 14 | t.Error("failed to get the typo error similars") 15 | } 16 | 17 | if similars[0] != "cmd1" || similars[1] != "cmd2" { 18 | t.Error("failed to get the typo error similars") 19 | } 20 | } 21 | 22 | func TestSetSimilars(t *testing.T) { 23 | err := &ErrPossibleTypo{[]string{}} 24 | 25 | err.SetSimilars([]string{"cmd1", "cmd2"}) 26 | 27 | similars := err.Similars() 28 | 29 | if len(similars) != 2 { 30 | t.Error("failed to get the typo error similars") 31 | } 32 | 33 | if similars[0] != "cmd1" || similars[1] != "cmd2" { 34 | t.Error("failed to get the typo error similars") 35 | } 36 | } 37 | 38 | func TestIsPossibleTypoError(t *testing.T) { 39 | err := &ErrPossibleTypo{[]string{}} 40 | 41 | isTypoError := IsPossibleTypoError(err) 42 | 43 | if !isTypoError { 44 | t.Error("failed to assert that error was a typo one") 45 | } 46 | 47 | isTypoError = IsPossibleTypoError(errors.New("error")) 48 | 49 | if isTypoError { 50 | t.Error("failed to assert that error was not a typo one") 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /docs/05-Commands-Reference/kool-cloud-logs.md: -------------------------------------------------------------------------------- 1 | ## kool cloud logs 2 | 3 | See the logs of running service container deployed to Kool.dev Cloud 4 | 5 | ### Synopsis 6 | 7 | After deploying an application to Kool.dev Cloud using 'kool deploy', 8 | you can see the logs from the specified SERVICE container. 9 | Must use a KOOL_API_TOKEN environment variable for authentication. 10 | 11 | ``` 12 | kool cloud logs [OPTIONS] SERVICE 13 | ``` 14 | 15 | ### Options 16 | 17 | ``` 18 | -c, --container string Container target. (default "default") 19 | -f, --follow Follow log output. 20 | -h, --help help for logs 21 | -t, --tail int Number of lines to show from the end of the logs for each container. A value equal to 0 will show all lines. (default 25) 22 | ``` 23 | 24 | ### Options inherited from parent commands 25 | 26 | ``` 27 | --domain string Environment domain name to deploy to 28 | --token string Token to authenticate with Kool.dev Cloud API 29 | --verbose Increases output verbosity 30 | -w, --working_dir string Changes the working directory for the command 31 | ``` 32 | 33 | ### SEE ALSO 34 | 35 | * [kool cloud](kool_cloud) - Interact with Kool.dev Cloud and manage your deployments. 36 | 37 | -------------------------------------------------------------------------------- /services/checker/errors.go: -------------------------------------------------------------------------------- 1 | package checker 2 | 3 | import "errors" 4 | 5 | // ErrDockerNotFound happens when docker is not installed 6 | var ErrDockerNotFound = errors.New("docker doesn't seem to be installed, install it first and retry") 7 | 8 | // IsDockerNotFoundError tells whether the given error is checker.ErrDockerNotFound 9 | func IsDockerNotFoundError(err error) bool { 10 | return err.Error() == ErrDockerNotFound.Error() 11 | } 12 | 13 | // ErrDockerComposeNotFound happens when docker compose V2 is not installed 14 | var ErrDockerComposeNotFound = errors.New("docker compose V2 doesn't seem to be installed, upgrade your docker installation and retry") 15 | 16 | // IsDockerComposeNotFoundError tells whether the given error is checker.ErrDockerComposeNotFound 17 | func IsDockerComposeNotFoundError(err error) bool { 18 | return err.Error() == ErrDockerComposeNotFound.Error() 19 | } 20 | 21 | // ErrDockerNotRunning happens when docker daemon is not running 22 | var ErrDockerNotRunning = errors.New("docker daemon doesn't seem to be running, run it first and retry") 23 | 24 | // IsDockerNotRunningError tells whether the given error is checker.ErrDockerNotRunning 25 | func IsDockerNotRunningError(err error) bool { 26 | return err.Error() == ErrDockerNotRunning.Error() 27 | } 28 | -------------------------------------------------------------------------------- /services/compose/fake_parser_test.go: -------------------------------------------------------------------------------- 1 | package compose 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | ) 7 | 8 | func TestFakeParser(t *testing.T) { 9 | var err error 10 | 11 | f := &FakeParser{} 12 | 13 | f.MockParseError = errors.New("parse error") 14 | err = f.Parse("compose") 15 | 16 | if err == nil { 17 | t.Error("expecting error on Parse, got none") 18 | } else if err.Error() != "parse error" { 19 | t.Errorf("expecting error 'parse error' on Parse, got %v", err) 20 | } 21 | 22 | if val, ok := f.CalledParse["compose"]; !ok || !val { 23 | t.Error("failed calling Parse") 24 | } 25 | 26 | f.SetService("service", "content") 27 | 28 | if val, ok := f.CalledSetService["service"]; !ok || !val { 29 | t.Error("failed calling SetService") 30 | } 31 | 32 | f.SetVolume("volume") 33 | 34 | if val, ok := f.CalledSetVolume["volume"]; !ok || !val { 35 | t.Error("failed calling SetVolume") 36 | } 37 | 38 | f.MockStringError = errors.New("string error") 39 | 40 | _, err = f.String() 41 | 42 | if err == nil { 43 | t.Error("expecting error on String, got none") 44 | } else if err.Error() != "string error" { 45 | t.Errorf("expecting error 'string error' on String, got %v", err) 46 | } 47 | 48 | if !f.CalledString { 49 | t.Error("failed calling String") 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /services/checker/checker.go: -------------------------------------------------------------------------------- 1 | package checker 2 | 3 | import ( 4 | "kool-dev/kool/core/builder" 5 | "kool-dev/kool/core/shell" 6 | "strings" 7 | ) 8 | 9 | // Checker defines the check kool dependencies method 10 | type Checker interface { 11 | Check() error 12 | } 13 | 14 | // DefaultChecker holds commands to be checked. 15 | type DefaultChecker struct { 16 | dockerCmd builder.Command 17 | dockerComposeCmd builder.Command 18 | shell shell.Shell 19 | } 20 | 21 | // NewChecker initializes checker 22 | func NewChecker(s shell.Shell) *DefaultChecker { 23 | return &DefaultChecker{ 24 | builder.NewCommand("docker", "info"), 25 | builder.NewCommand("docker", "compose", "ps"), 26 | s, 27 | } 28 | } 29 | 30 | // Check checks kool dependencies 31 | func (c *DefaultChecker) Check() error { 32 | if err := c.shell.LookPath(c.dockerCmd); err != nil { 33 | return ErrDockerNotFound 34 | } 35 | 36 | if _, err := c.shell.Exec(c.dockerComposeCmd); err != nil { 37 | if strings.Contains(strings.ToLower(err.Error()), "is not a docker command") { 38 | return ErrDockerComposeNotFound 39 | } 40 | // anything else, raise the original error 41 | return err 42 | } 43 | 44 | if _, err := c.shell.Exec(c.dockerCmd); err != nil { 45 | return ErrDockerNotRunning 46 | } 47 | 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /presets/nest+next/config.yml: -------------------------------------------------------------------------------- 1 | # Which tags are related to this preset; used for branching the choices on preset wizard 2 | tags: [ 'Typescript' ] 3 | 4 | name: 'NestJS + NextJS (monorepo)' 5 | 6 | # Create defines the workflow for creating a new Project 7 | # where this preset can then be installed 8 | create: 9 | - name: Creating new NestJS Application 10 | actions: 11 | - scripts: 12 | - docker pull -q kooldev/node:20 13 | - mkdir $CREATE_DIRECTORY 14 | - kool docker kooldev/node:20 npx -y @nestjs/cli new -l Typescript -p npm --skip-git $CREATE_DIRECTORY/backend 15 | - kool docker kooldev/node:20 npx -y create-next-app@latest --ts --use-npm $CREATE_DIRECTORY/frontend 16 | 17 | preset: 18 | - name: 'Copy basic config files' 19 | actions: 20 | - copy: docker-compose.yml 21 | - copy: kool.yml 22 | - merge: nest+next-docker-compose.yml 23 | dst: docker-compose.yml 24 | - merge: nest+next-kool.yml 25 | dst: kool.yml 26 | - recipe: pick-db 27 | - recipe: pick-cache 28 | - copy: env.dist 29 | dst: .env.dist 30 | - copy: gitignore 31 | dst: .gitignore 32 | - scripts: 33 | - kool docker kooldev/node:20 sed -i 's/app.listen(3000)/app.listen(process.env.KOOL_BACKEND_PORT)/' backend/src/main.ts 34 | -------------------------------------------------------------------------------- /commands/kool_upgrade_available_checker.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "kool-dev/kool/services/updater" 5 | 6 | "time" 7 | ) 8 | 9 | // UpdateAwareService holds functions to implement the checker to see if theres a new version available 10 | type UpdateAwareService struct { 11 | KoolService 12 | 13 | updater updater.Updater 14 | skip bool 15 | } 16 | 17 | // CheckNewVersion wraps the service with checker logic 18 | func CheckNewVersion(service KoolService, updater updater.Updater, skip bool) *UpdateAwareService { 19 | return &UpdateAwareService{ 20 | service, 21 | updater, 22 | skip, 23 | } 24 | } 25 | 26 | // Execute runs the check logic and proxies to original service 27 | func (u *UpdateAwareService) Execute(args []string) (err error) { 28 | if u.skip || !u.KoolService.Shell().IsTerminal() { 29 | err = u.KoolService.Execute(args) 30 | return 31 | } 32 | 33 | ch := make(chan bool) 34 | 35 | go u.updater.CheckForUpdates(u.updater.GetCurrentVersion(), ch) 36 | 37 | if err = u.KoolService.Execute(args); err != nil { 38 | return err 39 | } 40 | 41 | select { 42 | case update := <-ch: 43 | if update { 44 | defer u.KoolService.Shell().Warning("There's a new version available! Run kool self-update to upgrade!") 45 | } 46 | case <-time.After(time.Second): 47 | break 48 | } 49 | 50 | return 51 | } 52 | -------------------------------------------------------------------------------- /core/builder/fake_command_test.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | ) 7 | 8 | func TestFakeCommand(t *testing.T) { 9 | f := &FakeCommand{} 10 | 11 | f.AppendArgs("arg1", "arg2") 12 | 13 | if !f.CalledAppendArgs || f.ArgsAppend == nil || f.ArgsAppend[0] != "arg1" || f.ArgsAppend[1] != "arg2" { 14 | t.Errorf("failed to use mocked AppendArgs function on FakeCommand") 15 | } 16 | 17 | _ = f.String() 18 | 19 | if !f.CalledString { 20 | t.Errorf("failed to use mocked String function on FakeCommand") 21 | } 22 | 23 | f.MockCmd = "cmd" 24 | 25 | if cmd := f.Cmd(); !f.CalledCmd || cmd != "cmd" { 26 | t.Errorf("failed to use mocked Cmd function on FakeCommand") 27 | } 28 | 29 | if args := f.Args(); !f.CalledArgs || len(args) != 2 || args[0] != "arg1" || args[1] != "arg2" { 30 | t.Errorf("failed to use mocked Args function on FakeCommand") 31 | } 32 | 33 | f.MockError = errors.New("parse error") 34 | 35 | err := f.Parse("echo x1") 36 | 37 | if !f.CalledParseCommand || err == nil || err.Error() != "parse error" { 38 | t.Errorf("failed to use mocked Parse function on FakeCommand") 39 | } 40 | } 41 | 42 | func TestFakeCopy(t *testing.T) { 43 | f := &FakeCommand{} 44 | 45 | cp := f.Copy() 46 | 47 | if ptr, ok := cp.(*FakeCommand); !ok || ptr != f { 48 | t.Error("unexpected fake copy") 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /core/shell/table_writer.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/jedib0t/go-pretty/v6/table" 7 | ) 8 | 9 | // DefaultTableWriter holds table output writer 10 | type DefaultTableWriter struct { 11 | w table.Writer 12 | } 13 | 14 | // TableWriter holds table output writer logic 15 | type TableWriter interface { 16 | SetWriter(io.Writer) 17 | AppendHeader(...interface{}) 18 | AppendRow(...interface{}) 19 | Render() 20 | SortBy(int) 21 | } 22 | 23 | // NewTableWriter creates a new table writer 24 | func NewTableWriter() TableWriter { 25 | return &DefaultTableWriter{table.NewWriter()} 26 | } 27 | 28 | // SetWriter set table output writer 29 | func (t *DefaultTableWriter) SetWriter(w io.Writer) { 30 | t.w.SetOutputMirror(w) 31 | } 32 | 33 | // AppendHeader append header columns to table 34 | func (t *DefaultTableWriter) AppendHeader(columns ...interface{}) { 35 | t.w.AppendHeader(columns) 36 | } 37 | 38 | // AppendRow append row columns to table 39 | func (t *DefaultTableWriter) AppendRow(columns ...interface{}) { 40 | t.w.AppendRow(columns) 41 | } 42 | 43 | // Render render the table 44 | func (t *DefaultTableWriter) Render() { 45 | t.w.Render() 46 | } 47 | 48 | // SortBy sort table by column 49 | func (t *DefaultTableWriter) SortBy(column int) { 50 | t.w.SortBy([]table.SortBy{{Number: column, Mode: table.Asc}}) 51 | } 52 | -------------------------------------------------------------------------------- /services/compose/fake_parser.go: -------------------------------------------------------------------------------- 1 | package compose 2 | 3 | // FakeParser implements all fake behaviors for using parser in tests. 4 | type FakeParser struct { 5 | CalledParse map[string]bool 6 | CalledSetService map[string]bool 7 | CalledSetVolume map[string]bool 8 | CalledString bool 9 | MockParseError error 10 | MockStringError error 11 | } 12 | 13 | // Parse implements fake Parse behavior 14 | func (f *FakeParser) Parse(content string) (err error) { 15 | if f.CalledParse == nil { 16 | f.CalledParse = make(map[string]bool) 17 | } 18 | 19 | f.CalledParse[content] = true 20 | err = f.MockParseError 21 | return 22 | } 23 | 24 | // SetService implements fake SetService behavior 25 | func (f *FakeParser) SetService(service string, content interface{}) { 26 | if f.CalledSetService == nil { 27 | f.CalledSetService = make(map[string]bool) 28 | } 29 | 30 | f.CalledSetService[service] = true 31 | } 32 | 33 | // SetVolume implements fake SetVolume behavior 34 | func (f *FakeParser) SetVolume(volume string) { 35 | if f.CalledSetVolume == nil { 36 | f.CalledSetVolume = make(map[string]bool) 37 | } 38 | 39 | f.CalledSetVolume[volume] = true 40 | } 41 | 42 | // String implements fake String behavior 43 | func (f *FakeParser) String() (content string, err error) { 44 | f.CalledString = true 45 | err = f.MockStringError 46 | return 47 | } 48 | -------------------------------------------------------------------------------- /core/shell/fake_table_writer_test.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "io" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestFakeTableWriter(t *testing.T) { 10 | f := &FakeTableWriter{} 11 | 12 | f.SetWriter(io.Discard) 13 | 14 | if !f.CalledSetWriter { 15 | t.Errorf("failed to mock method SetWriter on FakeTableWriter") 16 | } 17 | 18 | f.AppendHeader("header") 19 | 20 | if !f.CalledAppendHeader || len(f.Headers) != 1 || len(f.Headers[0]) != 1 || f.Headers[0][0] != "header" { 21 | t.Errorf("failed to mock method AppendHeader on FakeTableWriter") 22 | } 23 | 24 | f.AppendRow("row") 25 | 26 | if !f.CalledAppendRow || len(f.Rows) != 1 || len(f.Rows[0]) != 1 || f.Rows[0][0] != "row" { 27 | t.Errorf("failed to mock method AppendRow on FakeTableWriter") 28 | } 29 | 30 | f.Render() 31 | 32 | expected := `header 33 | row` 34 | 35 | if !f.CalledRender || strings.TrimSpace(expected) != strings.TrimSpace(f.TableOut) { 36 | t.Errorf("failed to mock method Render on FakeTableWriter") 37 | } 38 | } 39 | 40 | func TestSortByFakeTableWriter(t *testing.T) { 41 | f := &FakeTableWriter{} 42 | 43 | f.SetWriter(io.Discard) 44 | 45 | f.AppendRow("zRow") 46 | f.AppendRow("aRow") 47 | 48 | f.SortBy(1) 49 | 50 | if f.Rows[0][0] != "aRow" || f.Rows[1][0] != "zRow" { 51 | t.Errorf("failed to mock method SortBy on FakeTableWriter") 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /presets/adonis/config.yml: -------------------------------------------------------------------------------- 1 | # Which tags are related to this preset; used for branching the choices on preset wizard 2 | tags: [ 'Typescript' ] 3 | 4 | name: 'AdonisJS' 5 | 6 | # Create defines the workflow for creating a new Project where this preset can then be installed 7 | create: 8 | - name: Creating new Adonis Application 9 | actions: 10 | - scripts: 11 | - docker pull -q kooldev/node:20 12 | - kool docker kooldev/node:20 npm init -y adonis-ts-app@latest $CREATE_DIRECTORY 13 | 14 | # Preset defines the workflow for installing this preset in the current working directory 15 | preset: 16 | - name: 'Copy basic config files' 17 | actions: 18 | - copy: docker-compose.yml 19 | - copy: kool.yml 20 | - merge: app/node-adonis.yml 21 | dst: docker-compose.yml 22 | 23 | - name: 'Customize your setup' 24 | actions: 25 | - recipe: pick-db 26 | - recipe: pick-cache 27 | # define package manager 28 | - prompt: Which javascript package manager do you want to use? 29 | default: 'npm' 30 | options: 31 | - name: 'npm' 32 | actions: 33 | - merge: scripts/npm-adonis.yml 34 | dst: kool.yml 35 | - name: 'yarn' 36 | actions: 37 | - merge: scripts/yarn-adonis.yml 38 | dst: kool.yml 39 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile 3 | { 4 | "name": "Kool.dev Container for Go CLI development", 5 | 6 | "build": { 7 | "context": ".", 8 | "dockerfile": "Dockerfile" 9 | }, 10 | 11 | // Features to add to the dev container. More info: https://containers.dev/features. 12 | "features": { 13 | "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}, 14 | "ghcr.io/devcontainers/features/go:1": { 15 | "version": "1.21" 16 | } 17 | }, 18 | 19 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 20 | // "forwardPorts": [ 21 | // ], 22 | 23 | // Uncomment the next line to run commands after the container is created. 24 | "postCreateCommand": [ 25 | // "bash -c \"$(curl -fsSL https://raw.githubusercontent.com/ohmybash/oh-my-bash/master/tools/install.sh)\"" 26 | ], 27 | 28 | // Configure tool-specific properties. 29 | "customizations": { 30 | "vscode": { 31 | "extensions": [ 32 | "ms-vscode.go" 33 | ] 34 | } 35 | }, 36 | 37 | // Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root. 38 | "remoteUser": "kool" 39 | } 40 | -------------------------------------------------------------------------------- /services/yamler/output_test.go: -------------------------------------------------------------------------------- 1 | package yamler 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "testing" 9 | 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | func TestWriteYAML(t *testing.T) { 14 | o := &DefaultOutputWritter{} 15 | f := filepath.Join(t.TempDir(), "test.yml") 16 | 17 | y := new(yaml.Node) 18 | yml := "foo: bar" 19 | 20 | _ = yaml.Unmarshal([]byte(yml), y) 21 | 22 | if err := o.WriteYAML(f, y); err != nil { 23 | t.Errorf("unexpected error writing YAML: %v", err) 24 | } 25 | 26 | fh, _ := os.Open(f) 27 | bs, _ := io.ReadAll(fh) 28 | fh.Close() 29 | got := strings.Trim(string(bs), " \t\n") 30 | 31 | if got != yml { 32 | t.Errorf("bad YML; expected '%s' but got '%s'", yml, got) 33 | } 34 | } 35 | 36 | func TestWriteYAMLIndentation(t *testing.T) { 37 | o := &DefaultOutputWritter{} 38 | f := filepath.Join(t.TempDir(), "test_indent.yml") 39 | 40 | y := new(yaml.Node) 41 | yml := "foo:\n bar: xxx" 42 | 43 | _ = yaml.Unmarshal([]byte(yml), y) 44 | 45 | if err := o.WriteYAML(f, y); err != nil { 46 | t.Errorf("unexpected error writing YAML: %v", err) 47 | } 48 | 49 | fh, _ := os.Open(f) 50 | bs, _ := io.ReadAll(fh) 51 | fh.Close() 52 | got := strings.Trim(string(bs), " \t\n") 53 | 54 | expect := "foo:\n bar: xxx" 55 | 56 | if got != expect { 57 | t.Errorf("bad YML; expected '%s' but got '%s'", expect, got) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /core/automate/actions.go: -------------------------------------------------------------------------------- 1 | package automate 2 | 3 | type ActionType uint 4 | 5 | const ( 6 | TypeUnknown ActionType = iota 7 | TypeCopy 8 | TypeScripts 9 | TypePrompt 10 | TypeRecipe 11 | TypeMerge 12 | ) 13 | 14 | // ActionSet represents a set of single actions or a question 15 | type ActionSet struct { 16 | Name string `yaml:"name"` 17 | Actions []*Action `yaml:"actions"` 18 | } 19 | 20 | // Action is a union kind of type that holds 21 | // one specific action within it; used for parsing 22 | type Action struct { 23 | // ref 24 | Ref string `yaml:"ref"` 25 | 26 | // recipe 27 | Recipe string `yaml:"recipe"` 28 | // merge 29 | Merge string `yaml:"merge"` 30 | // copy 31 | Src string `yaml:"copy"` 32 | Dst string `yaml:"dst"` 33 | // scripts 34 | Scripts []string `yaml:"scripts"` 35 | // prompt 36 | Prompt string `yaml:"prompt"` 37 | Default string `yaml:"default"` 38 | Options []*ActionSet `yaml:"options"` 39 | } 40 | 41 | // Type tells the actual implementation of this action 42 | func (a *Action) Type() ActionType { 43 | if a.Scripts != nil { 44 | return TypeScripts 45 | } 46 | 47 | if a.Recipe != "" { 48 | return TypeRecipe 49 | } 50 | 51 | if a.Src != "" { 52 | return TypeCopy 53 | } 54 | 55 | if a.Prompt != "" { 56 | return TypePrompt 57 | } 58 | 59 | if a.Merge != "" { 60 | return TypeMerge 61 | } 62 | 63 | return TypeUnknown 64 | } 65 | -------------------------------------------------------------------------------- /docs/05-Commands-Reference/kool-cloud.md: -------------------------------------------------------------------------------- 1 | ## kool cloud 2 | 3 | Interact with Kool.dev Cloud and manage your deployments. 4 | 5 | ### Synopsis 6 | 7 | The cloud subcommand encapsulates a set of APIs to interact with Kool.dev Cloud and deploy, access and tail logs from your deployments. 8 | 9 | ### Examples 10 | 11 | ``` 12 | kool cloud deploy 13 | ``` 14 | 15 | ### Options 16 | 17 | ``` 18 | --domain string Environment domain name to deploy to 19 | -h, --help help for cloud 20 | --token string Token to authenticate with Kool.dev Cloud API 21 | ``` 22 | 23 | ### Options inherited from parent commands 24 | 25 | ``` 26 | --verbose Increases output verbosity 27 | -w, --working_dir string Changes the working directory for the command 28 | ``` 29 | 30 | ### SEE ALSO 31 | 32 | * [kool](kool) - Cloud native environments made easy 33 | * [kool cloud deploy](kool_cloud_deploy) - Deploy a local application to a Kool.dev Cloud environment 34 | * [kool cloud destroy](kool_cloud_destroy) - Destroy an environment deployed to Kool.dev Cloud 35 | * [kool cloud exec](kool_cloud_exec) - Execute a command inside a running service container deployed to Kool.dev Cloud 36 | * [kool cloud logs](kool_cloud_logs) - See the logs of running service container deployed to Kool.dev Cloud 37 | * [kool cloud setup](kool_cloud_setup) - Set up local configuration files for deployment 38 | 39 | -------------------------------------------------------------------------------- /kool.yml: -------------------------------------------------------------------------------- 1 | scripts: 2 | # Helper for local development 3 | # compiling and installing locally 4 | dev: 5 | - kool run compile 6 | - kool run install 7 | # Runs go CLI with proper version for kool development (targets host OS passing down GOOS) 8 | go: kool docker --volume=kool_gopath:/go --env='GOOS=$GOOS' golang:1.21 go 9 | # Runs go CLI with Linux, independent of host OS 10 | go:linux: kool docker --volume=kool_gopath:/go golang:1.21 go 11 | # Compiling kool itself. In case you are on MacOS make sure to have your .env 12 | # file properly setting GOOS=darwin so you will be able to use the binary. 13 | compile: 14 | - kool run fmt 15 | - kool run go build -buildvcs=false -o kool-cli 16 | install: mv ./kool-cli /usr/local/bin/kool 17 | fmt: kool run go:linux fmt ./... 18 | lint: kool docker --volume=kool_gopath:/go golangci/golangci-lint:v1.54.1 golangci-lint run -v 19 | test: kool run test:path ./... 20 | test:path: kool run go:linux test -race 21 | test-coverage: kool run go:linux test -v -race -coverprofile=coverage.txt -covermode=atomic ./... 22 | # Generate documentation for kool commands 23 | make-docs: 24 | - rm -f docs/05-Commands-Reference/*.md 25 | - kool run go:linux run docs.go 26 | # build docker image locally 27 | docker:build:rc: docker build --build-arg BUILD_VERSION=0.0.0-rc --pull -t kooldev/kool:rc . 28 | docker:push:rc: docker push kooldev/kool:rc 29 | -------------------------------------------------------------------------------- /commands/restart.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | // KoolRestartFlags holds the flags for the kool restart command 8 | type KoolRestartFlags struct { 9 | Purge bool 10 | Rebuild bool 11 | } 12 | 13 | // NewRestartCommand initializes new kool start command 14 | func NewRestartCommand(stop KoolService, start KoolService) (restartCmd *cobra.Command) { 15 | var flags *KoolRestartFlags = &KoolRestartFlags{false, false} 16 | 17 | restartCmd = &cobra.Command{ 18 | Use: "restart", 19 | Short: "Restart running service containers (the same as 'kool stop' followed by 'kool start')", 20 | RunE: func(cmd *cobra.Command, args []string) error { 21 | if _, ok := stop.(*KoolStop); ok && flags.Purge { 22 | stop.(*KoolStop).Flags.Purge = true 23 | } 24 | if _, ok := start.(*KoolStart); ok && flags.Rebuild { 25 | start.(*KoolStart).Flags.Rebuild = true 26 | } 27 | 28 | return DefaultCommandRunFunction(stop, start)(cmd, args) 29 | }, 30 | 31 | DisableFlagsInUseLine: true, 32 | } 33 | 34 | restartCmd.Flags().BoolVarP(&flags.Purge, "purge", "", false, "Remove all persistent data from volume mounts on containers") 35 | restartCmd.Flags().BoolVarP(&flags.Rebuild, "rebuild", "", false, "Updates and builds service's images") 36 | 37 | return 38 | } 39 | 40 | func AddKoolRestart(root *cobra.Command) { 41 | root.AddCommand(NewRestartCommand(NewKoolStop(), NewKoolStart())) 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: docker 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | docker-build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@master 15 | 16 | - uses: olegtarasov/get-tag@v2.1 17 | id: tagName 18 | 19 | - name: Build image 20 | env: 21 | TAGNAME: ${{ steps.tagName.outputs.tag }} 22 | run: docker build --build-arg BUILD_VERSION=$TAGNAME --pull -t kooldev/kool:$TAGNAME -t kooldev/kool:4scan . 23 | 24 | - name: Test image 25 | env: 26 | TAGNAME: ${{ steps.tagName.outputs.tag }} 27 | run: docker run kooldev/kool:$TAGNAME kool --version 28 | 29 | # - name: Scan image 30 | # uses: anchore/scan-action@v2 31 | # with: 32 | # image: "kooldev/kool:4scan" 33 | # fail-build: true 34 | # severity-cutoff: critical 35 | 36 | - name: Push to hub 37 | env: 38 | TAGNAME: ${{ steps.tagName.outputs.tag }} 39 | DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} 40 | DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} 41 | run: | 42 | TAGNAME_MAJOR=${TAGNAME%%.*} 43 | docker tag kooldev/kool:$TAGNAME kooldev/kool:$TAGNAME_MAJOR 44 | echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin 45 | docker push kooldev/kool:$TAGNAME 46 | docker push kooldev/kool:$TAGNAME_MAJOR 47 | -------------------------------------------------------------------------------- /core/environment/env_storage.go: -------------------------------------------------------------------------------- 1 | package environment 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/fireworkweb/godotenv" 7 | ) 8 | 9 | // DefaultEnvStorage holds data to store environment variables 10 | type DefaultEnvStorage struct{} 11 | 12 | // EnvStorage contract that holds environment variables storage logic 13 | type EnvStorage interface { 14 | Get(string) string 15 | Set(string, string) 16 | Load(string) error 17 | All() []string 18 | IsTrue(string) bool 19 | } 20 | 21 | // NewEnvStorage creates a new Environment Storage instance 22 | func NewEnvStorage() EnvStorage { 23 | return &DefaultEnvStorage{} 24 | } 25 | 26 | // Get get environment variable value 27 | func (es *DefaultEnvStorage) Get(key string) string { 28 | return os.Getenv(key) 29 | } 30 | 31 | // Set set environment variable value 32 | func (es *DefaultEnvStorage) Set(key string, value string) { 33 | os.Setenv(key, value) 34 | } 35 | 36 | // Load load environment file 37 | func (es *DefaultEnvStorage) Load(filename string) error { 38 | return godotenv.Load(filename) 39 | } 40 | 41 | // All get all environment variables 42 | func (es *DefaultEnvStorage) All() []string { 43 | return os.Environ() 44 | } 45 | 46 | // IsTrue checks whether the given environment variable is 47 | // to what would be a boolean value of true (either 1 or "true") 48 | func (es *DefaultEnvStorage) IsTrue(key string) bool { 49 | value := os.Getenv(key) 50 | return value == "1" || value == "true" 51 | } 52 | -------------------------------------------------------------------------------- /core/shell/prompt_select.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/AlecAivazis/survey/v2" 7 | "github.com/AlecAivazis/survey/v2/terminal" 8 | ) 9 | 10 | // PromptSelect contract that holds logic for prompt a select question 11 | type PromptSelect interface { 12 | Ask(string, []string) (string, error) 13 | 14 | Confirm(string, ...any) (bool, error) 15 | } 16 | 17 | // DefaultPromptSelect holds data for prompting a select question 18 | type DefaultPromptSelect struct{} 19 | 20 | // NewPromptSelect creates a new prompt select 21 | func NewPromptSelect() PromptSelect { 22 | return &DefaultPromptSelect{} 23 | } 24 | 25 | // Ask prompt to the user a select question 26 | func (p *DefaultPromptSelect) Ask(question string, options []string) (answer string, err error) { 27 | prompt := &survey.Select{ 28 | Message: question, 29 | Options: options, 30 | } 31 | if err = survey.AskOne(prompt, &answer); err != nil && err == terminal.InterruptErr { 32 | err = ErrUserCancelled 33 | } 34 | return 35 | } 36 | 37 | // Confirm prompts to the user a Yes/No confirm question 38 | func (p *DefaultPromptSelect) Confirm(question string, args ...any) (confirmed bool, err error) { 39 | if args != nil { 40 | question = fmt.Sprintf(question, args...) 41 | } 42 | 43 | var answer string 44 | 45 | if answer, err = p.Ask(question, []string{"Yes", "No"}); err != nil { 46 | return 47 | } 48 | 49 | confirmed = answer == "Yes" 50 | return 51 | } 52 | -------------------------------------------------------------------------------- /core/builder/fake_command.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | // FakeCommand implements the Command interface and is used for mocking on testing scenarios 4 | type FakeCommand struct { 5 | ArgsAppend []string 6 | CalledAppendArgs bool 7 | CalledString bool 8 | CalledCmd bool 9 | CalledArgs bool 10 | CalledParseCommand bool 11 | 12 | MockCmd string 13 | MockExecOut string 14 | MockError error 15 | MockLookPathError error 16 | MockExecError error 17 | MockInteractiveError error 18 | } 19 | 20 | // AppendArgs mocked function for testing 21 | func (f *FakeCommand) AppendArgs(args ...string) { 22 | f.ArgsAppend = append(f.ArgsAppend, args...) 23 | f.CalledAppendArgs = true 24 | } 25 | 26 | // String mocked function for testing 27 | func (f *FakeCommand) String() string { 28 | f.CalledString = true 29 | return "" 30 | } 31 | 32 | // Args returns the command arguments 33 | func (f *FakeCommand) Args() []string { 34 | f.CalledArgs = true 35 | return f.ArgsAppend 36 | } 37 | 38 | // Cmd returns the command executable 39 | func (f *FakeCommand) Cmd() string { 40 | f.CalledCmd = true 41 | return f.MockCmd 42 | } 43 | 44 | // Parse call the ParseCommand function 45 | func (f *FakeCommand) Parse(line string) (err error) { 46 | f.CalledParseCommand = true 47 | err = f.MockError 48 | return 49 | } 50 | 51 | // Copy mocks a copy be returning itself 52 | func (f *FakeCommand) Copy() Command { 53 | return f 54 | } 55 | -------------------------------------------------------------------------------- /docs/10-Docker-Images/1-Introduction.md: -------------------------------------------------------------------------------- 1 | When you start developing in containers, you quickly realize the official Docker images are built for deployment, and not for the special considerations (and nuances) of local development. One of the most common and recurring problems we see are permission issues with mapped volumes, due to host users being different from container users. Kool fixes this problem, and many others, by creating custom Docker images optimized for local development environments. 2 | 3 | A few of the optimizations included in Kool's Docker images: 4 | - UID mapping to host user to solve permission issues 5 | - Alpine base images to remain small and up-to-date 6 | - Configured with sane defaults - for development as well as production 7 | - Environment variables to easily update the most common settings 8 | - Battle-tested - a growing community has been using these images in production for quite a long time now! 9 | 10 | ## Kool-Optimized Docker Images 11 | 12 | - PHP images: https://github.com/kool-dev/docker-php 13 | - Nginx images: https://github.com/kool-dev/docker-nginx 14 | - Node images: https://github.com/kool-dev/docker-node 15 | - Java images: https://github.com/kool-dev/docker-java 16 | - DevOps images: https://github.com/kool-dev/docker-toolkit 17 | 18 | > Disclaimer: Kool Docker images follow our recommended best practices, which are aimed at making your life easier. However, you can use the **Kool CLI** with any Docker images you like - assuming you know what you're doing. 19 | -------------------------------------------------------------------------------- /recipes/create-symfony.yml: -------------------------------------------------------------------------------- 1 | title: "Creating Symfony Application" 2 | 3 | actions: 4 | - prompt: 'Which PHP version do you want to use?' 5 | ref: 'php-version' 6 | default: 'PHP 8.2' 7 | options: 8 | - name: 'PHP 8.3' 9 | actions: 10 | - scripts: 11 | - docker pull -q kooldev/php:8.3 12 | - kool docker kooldev/php:8.3 composer create-project --no-install --prefer-dist symfony/website-skeleton $CREATE_DIRECTORY 13 | - name: 'PHP 8.2' 14 | actions: 15 | - scripts: 16 | - docker pull -q kooldev/php:8.2 17 | - kool docker kooldev/php:8.2 composer create-project --no-install --prefer-dist symfony/website-skeleton $CREATE_DIRECTORY 18 | - name: 'PHP 8.1' 19 | actions: 20 | - scripts: 21 | - docker pull -q kooldev/php:8.1 22 | - kool docker kooldev/php:8.1 composer create-project --no-install --prefer-dist symfony/website-skeleton $CREATE_DIRECTORY 23 | - name: 'PHP 8.0' 24 | actions: 25 | - scripts: 26 | - docker pull -q kooldev/php:8 27 | - kool docker kooldev/php:p:8 composer create-project --no-install --prefer-dist symfony/website-skeleton $CREATE_DIRECTORY 28 | - name: 'PHP 7.4' 29 | actions: 30 | - scripts: 31 | - docker pull -q kooldev/php:7.4 32 | - kool docker kooldev/php:7.4 composer create-project --no-install --prefer-dist symfony/website-skeleton $CREATE_DIRECTORY 33 | -------------------------------------------------------------------------------- /services/updater/fake_updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/blang/semver" 7 | ) 8 | 9 | // FakeUpdater implements all fake behaviors for self-update 10 | type FakeUpdater struct { 11 | CalledGetCurrentVersion, CalledUpdate, 12 | CalledCheckForUpdates, CalledCheckPermission bool 13 | 14 | MockCurrentVersion, MockLatestVersion string 15 | MockErrorUpdate, MockErrorPermission error 16 | MockTimeoutDelay bool 17 | } 18 | 19 | // GetCurrentVersion get mocked current version 20 | func (u *FakeUpdater) GetCurrentVersion() semver.Version { 21 | u.CalledGetCurrentVersion = true 22 | return semver.MustParse(u.MockCurrentVersion) 23 | } 24 | 25 | // Update implements fake update 26 | func (u *FakeUpdater) Update(currentVersion semver.Version) (updatedVersion semver.Version, err error) { 27 | updatedVersion = semver.MustParse(u.MockLatestVersion) 28 | err = u.MockErrorUpdate 29 | u.CalledUpdate = true 30 | return 31 | } 32 | 33 | // CheckForUpdates implements fake available update check 34 | func (u *FakeUpdater) CheckForUpdates(currentVersion semver.Version, ch chan bool) { 35 | u.CalledCheckForUpdates = true 36 | 37 | if u.MockTimeoutDelay { 38 | // causes a timeout 39 | time.Sleep(time.Millisecond * 1100) 40 | } 41 | 42 | ch <- true 43 | } 44 | 45 | // CheckPermission implements fake permission check 46 | func (u *FakeUpdater) CheckPermission() (err error) { 47 | u.CalledCheckPermission = true 48 | err = u.MockErrorPermission 49 | return 50 | } 51 | -------------------------------------------------------------------------------- /recipes/create-laravel.yml: -------------------------------------------------------------------------------- 1 | title: "Creating Laravel Application" 2 | 3 | actions: 4 | - prompt: 'Which PHP version do you want to use?' 5 | ref: 'php-version' 6 | default: 'PHP 8.2' 7 | options: 8 | - name: 'PHP 8.3' 9 | actions: 10 | - scripts: 11 | - docker pull -q kooldev/php:8.3 12 | - kool docker kooldev/php:8.3 composer create-project --no-install --no-scripts --prefer-dist laravel/laravel $CREATE_DIRECTORY 13 | - name: 'PHP 8.2' 14 | actions: 15 | - scripts: 16 | - docker pull -q kooldev/php:8.2 17 | - kool docker kooldev/php:8.2 composer create-project --no-install --no-scripts --prefer-dist laravel/laravel $CREATE_DIRECTORY 18 | - name: 'PHP 8.1' 19 | actions: 20 | - scripts: 21 | - docker pull -q kooldev/php:8.1 22 | - kool docker kooldev/php:8.1 composer create-project --no-install --no-scripts --prefer-dist laravel/laravel $CREATE_DIRECTORY 23 | - name: 'PHP 8.0' 24 | actions: 25 | - scripts: 26 | - docker pull -q kooldev/php:8 27 | - kool docker kooldev/php:8 composer create-project --no-install --no-scripts --prefer-dist laravel/laravel $CREATE_DIRECTORY 28 | - name: 'PHP 7.4' 29 | actions: 30 | - scripts: 31 | - docker pull -q kooldev/php:7.4 32 | - kool docker kooldev/php:7.4 composer create-project --no-install --no-scripts --prefer-dist laravel/laravel $CREATE_DIRECTORY 33 | -------------------------------------------------------------------------------- /core/presets/fake_parser_test.go: -------------------------------------------------------------------------------- 1 | package presets 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | ) 7 | 8 | func TestFakeParser(t *testing.T) { 9 | f := &FakeParser{} 10 | 11 | f.MockExists = true 12 | exists := f.Exists("preset") 13 | 14 | if !f.CalledExists || exists != f.MockExists { 15 | t.Error("failed to use mocked Exists function on FakeParser") 16 | } 17 | 18 | f.MockGetPresets = map[string]string{"preset": "preset"} 19 | presets := f.GetPresets("") 20 | 21 | if !f.CalledGetPresets || len(presets) != 1 || presets["preset"] != "preset" { 22 | t.Error("failed to use mocked GetPresets function on FakeParser") 23 | } 24 | 25 | f.MockGetTags = []string{"php"} 26 | tags := f.GetTags() 27 | 28 | if !f.CalledGetTags || len(tags) != 1 || tags[0] != "php" { 29 | t.Error("failed to use mocked GetTags function on FakeParser") 30 | } 31 | 32 | f.MockInstall = errors.New("Install") 33 | errInstall := f.Install("") 34 | 35 | if !f.CalledInstall || errInstall == nil || errInstall.Error() != "Install" { 36 | t.Error("failed to use mocked Install function on FakeParser") 37 | } 38 | 39 | f.MockCreate = errors.New("Create") 40 | errCreate := f.Create("") 41 | 42 | if !f.CalledCreate || errCreate == nil || errCreate.Error() != "Create" { 43 | t.Error("failed to use mocked Create function on FakeParser") 44 | } 45 | 46 | f.MockAdd = errors.New("Add") 47 | errAdd := f.Add("", nil) 48 | 49 | if !f.CalledAdd || errAdd == nil || errAdd.Error() != "Add" { 50 | t.Error("failed to use mocked Add function on FakeParser") 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /recipes/create-codeigniter.yml: -------------------------------------------------------------------------------- 1 | title: "Creating Laravel Application" 2 | 3 | actions: 4 | - prompt: 'Which PHP version do you want to use?' 5 | ref: 'php-version' 6 | default: 'PHP 8.2' 7 | options: 8 | - name: 'PHP 8.3' 9 | actions: 10 | - scripts: 11 | - docker pull -q kooldev/php:8.3 12 | - kool docker kooldev/php:8.3 composer create-project --no-install --no-scripts --prefer-dist codeigniter4/appstarter $CREATE_DIRECTORY 13 | - name: 'PHP 8.2' 14 | actions: 15 | - scripts: 16 | - docker pull -q kooldev/php:8.2 17 | - kool docker kooldev/php:8.2 composer create-project --no-install --no-scripts --prefer-dist codeigniter4/appstarter $CREATE_DIRECTORY 18 | - name: 'PHP 8.1' 19 | actions: 20 | - scripts: 21 | - docker pull -q kooldev/php:8.1 22 | - kool docker kooldev/php:8.1 composer create-project --no-install --no-scripts --prefer-dist codeigniter4/appstarter $CREATE_DIRECTORY 23 | - name: 'PHP 8.0' 24 | actions: 25 | - scripts: 26 | - docker pull -q kooldev/php:8 27 | - kool docker kooldev/php:p:8 composer create-project --no-install --no-scripts --prefer-dist codeigniter4/appstarter $CREATE_DIRECTORY 28 | - name: 'PHP 7.4' 29 | actions: 30 | - scripts: 31 | - docker pull -q kooldev/php:7.4 32 | - kool docker kooldev/php:7.4 composer create-project --no-install --no-scripts --prefer-dist codeigniter4/appstarter $CREATE_DIRECTORY 33 | -------------------------------------------------------------------------------- /core/shell/prompt_select_test.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestNewPromptSelect(t *testing.T) { 11 | p := NewPromptSelect() 12 | 13 | if _, ok := p.(*DefaultPromptSelect); !ok { 14 | t.Errorf("unexpected PromptSelect on NewPromptSelect") 15 | } 16 | } 17 | 18 | func TestAskPromptSelect(t *testing.T) { 19 | oldStdout := os.Stdout 20 | r, w, _ := os.Pipe() 21 | os.Stdout = w 22 | 23 | p := NewPromptSelect() 24 | 25 | _, _ = p.Ask("testing_question", []string{"testing_option1", "testing_option2"}) 26 | 27 | w.Close() 28 | out, err := io.ReadAll(r) 29 | os.Stdout = oldStdout 30 | 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | 35 | output := string(out) 36 | 37 | if !strings.Contains(output, "testing_question") || !strings.Contains(output, "testing_option1") || !strings.Contains(output, "testing_option2") { 38 | t.Error("failed to render the question and its options") 39 | } 40 | } 41 | 42 | func TestConfirmPromptSelect(t *testing.T) { 43 | oldStdout := os.Stdout 44 | r, w, _ := os.Pipe() 45 | os.Stdout = w 46 | 47 | p := NewPromptSelect() 48 | 49 | _, _ = p.Confirm("testing_question %s", "arg1") 50 | 51 | w.Close() 52 | out, err := io.ReadAll(r) 53 | os.Stdout = oldStdout 54 | 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | 59 | output := string(out) 60 | 61 | if !strings.Contains(output, "testing_question arg1") || !strings.Contains(output, "Yes") || !strings.Contains(output, "No") { 62 | t.Error("failed to render the Confirm prompt and its options") 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /services/cloud/api/deploy_create.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // DeployCreateResponse holds data returned from the deploy endpoint 4 | type DeployCreateResponse struct { 5 | Deploy struct { 6 | ID int `json:"id"` 7 | Project string `json:"project"` 8 | Url string `json:"url"` 9 | Environment struct { 10 | Name string `json:"name"` 11 | Env interface{} `json:"env"` 12 | } `json:"environment"` 13 | Cluster struct { 14 | Region string `json:"region"` 15 | } `json:"cluster"` 16 | } `json:"deploy"` 17 | 18 | Config struct { 19 | ImagePrefix string `json:"image_prefix"` 20 | ImageRepository string `json:"image_repository"` 21 | ImageTag string `json:"image_tag"` 22 | } `json:"stuff"` 23 | 24 | Docker struct { 25 | Login string `json:"login"` 26 | Password string `json:"password"` 27 | } `json:"docker"` 28 | 29 | LogsUrl string `json:"logs_url"` 30 | } 31 | 32 | // DeployCreate consumes the API endpoint to create a new deployment 33 | type DeployCreate struct { 34 | Endpoint 35 | } 36 | 37 | // NewDeployCreate creates a new DeployCreate instance 38 | func NewDeployCreate() (c *DeployCreate) { 39 | c = &DeployCreate{ 40 | Endpoint: NewDefaultEndpoint("POST"), 41 | } 42 | 43 | c.SetPath("deploy/create") 44 | _ = c.PostField("is_local", "1") 45 | 46 | return 47 | } 48 | 49 | // Run calls deploy/create in the Kool Dev API 50 | func (c *DeployCreate) Run() (resp *DeployCreateResponse, err error) { 51 | resp = &DeployCreateResponse{} 52 | 53 | c.SetResponseReceiver(resp) 54 | 55 | err = c.DoCall() 56 | 57 | return 58 | } 59 | -------------------------------------------------------------------------------- /docs/03-Presets/1-Creating-a-Preset.md: -------------------------------------------------------------------------------- 1 | # Creating a new preset 2 | 3 | `kool` has a builtin task-runner feature that allows us to automate steps/repetitive tasks - first of being all necessary setup steps for bootstrapping a new project. We use this feature to enable our presets, accomplishing two objectives: 4 | 5 | - Keeping fast, clean and simple how to setup a new local development environment for popular frameworks and start coding. 6 | - Have this project with all good practices and sane defaults for running in containers - for development and later deployment. 7 | 8 | ## Steps to create a preset 9 | 10 | #### 1. Creating `presets/my-preset/config.yml` 11 | 12 | Create a new folder under `presets` and a `config.yml` file in it: `presets/my-preset/config.yml`. 13 | 14 | The `config.yml` file is where we configure: 15 | 16 | - Steps for creating a new project 17 | - Steps for installing `kool` tailored local Docker environment to existing projects 18 | 19 | Both of the two tasks described above are accomplished via a set of declarative `steps` and `actions` on what we can call Kool Automation language. Some of the actions include: 20 | 21 | - `scripts` - running arbitrary shell script. 22 | - `copy` - copying files from our preset of templates folder right into the local project. 23 | - `merge` - merge YAML files - helpful for building `docker-compose.yml` or `kool.yml` dynamically. 24 | - `recipe`: run a Recipe which is a group of steps/actions ready to reuse 25 | 26 | You can find the full reference on the Kool Automation Langauge here TBD. 27 | 28 | Check out some of our current presets as examples. 29 | -------------------------------------------------------------------------------- /core/parser/fake_parser.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "kool-dev/kool/core/builder" 5 | "strings" 6 | ) 7 | 8 | // FakeParser implements all fake behaviors for using parser in tests. 9 | type FakeParser struct { 10 | CalledAddLookupPath bool 11 | TargetFiles []string 12 | CalledParse bool 13 | CalledParseAvailableScripts bool 14 | MockParsedCommands map[string][]builder.Command 15 | MockParseError map[string]error 16 | MockScripts []string 17 | MockParseAvailableScriptsError error 18 | } 19 | 20 | // AddLookupPath implements fake AddLookupPath behavior 21 | func (f *FakeParser) AddLookupPath(rootPath string) (err error) { 22 | f.CalledAddLookupPath = true 23 | f.TargetFiles = append(f.TargetFiles, "kool.yml") 24 | return 25 | } 26 | 27 | // Parse implements fake Parse behavior 28 | func (f *FakeParser) Parse(script string) (commands []builder.Command, err error) { 29 | f.CalledParse = true 30 | commands = f.MockParsedCommands[script] 31 | err = f.MockParseError[script] 32 | return 33 | } 34 | 35 | // ParseAvailableScripts implements fake ParseAvailableScripts behavior 36 | func (f *FakeParser) ParseAvailableScripts(filter string) (scripts []string, err error) { 37 | f.CalledParseAvailableScripts = true 38 | 39 | if filter == "" { 40 | scripts = f.MockScripts 41 | } else { 42 | for _, script := range f.MockScripts { 43 | if strings.HasPrefix(script, filter) { 44 | scripts = append(scripts, script) 45 | } 46 | } 47 | } 48 | 49 | err = f.MockParseAvailableScriptsError 50 | return 51 | } 52 | -------------------------------------------------------------------------------- /core/presets/fake_parser.go: -------------------------------------------------------------------------------- 1 | package presets 2 | 3 | import "kool-dev/kool/core/shell" 4 | 5 | // FakeParser implements all fake behaviors for using parser in tests. 6 | type FakeParser struct { 7 | CalledExists bool 8 | CalledGetTags bool 9 | CalledGetPresets bool 10 | CalledInstall bool 11 | CalledCreate bool 12 | CalledAdd bool 13 | 14 | MockExists bool 15 | MockGetTags []string 16 | MockGetPresets map[string]string 17 | MockInstall error 18 | MockCreate error 19 | MockAdd error 20 | } 21 | 22 | // Exists check if preset exists 23 | func (f *FakeParser) Exists(preset string) (exists bool) { 24 | f.CalledExists = true 25 | exists = f.MockExists 26 | return 27 | } 28 | 29 | func (f *FakeParser) PrepareExecutor(sh shell.Shell) { 30 | // noop 31 | } 32 | 33 | // GetTags get all presets tags 34 | func (f *FakeParser) GetTags() (languages []string) { 35 | f.CalledGetTags = true 36 | languages = f.MockGetTags 37 | return 38 | } 39 | 40 | // GetPresets get all presets names 41 | func (f *FakeParser) GetPresets(tag string) (presets map[string]string) { 42 | f.CalledGetPresets = true 43 | presets = f.MockGetPresets 44 | return 45 | } 46 | 47 | // Install 48 | func (f *FakeParser) Install(tag string) (err error) { 49 | f.CalledInstall = true 50 | err = f.MockInstall 51 | return 52 | } 53 | 54 | // Create 55 | func (f *FakeParser) Create(tag string) (err error) { 56 | f.CalledCreate = true 57 | err = f.MockCreate 58 | return 59 | } 60 | 61 | // Add 62 | func (f *FakeParser) Add(tag string, sh shell.Shell) (err error) { 63 | f.CalledAdd = true 64 | err = f.MockAdd 65 | return 66 | } 67 | -------------------------------------------------------------------------------- /docs/20-Troubleshooting/hmr-eer-empty-response.md: -------------------------------------------------------------------------------- 1 | ### `kool run npm run hot` or `kool run npm run dev` 2 | 3 | > Also valid for `kool run yarn run hot` or `kool run yarn run dev` 4 | 5 | **Problem:** 6 | 7 | > Browser cannot request "HMR" (hot module replacement) server successfully in Laravel Mix or Laravel Vite. 8 | 9 | > Console errors: **net::ERR_CONNECTION_REFUSED** or **net::ERR_EMPTY_RESPONSE**. 10 | 11 | **Answer:** 12 | 13 | > Publish the HMR's port to the host and change HMR settings to listen on all IPv4 addresses (`0.0.0.0`). 14 | 15 | For the sake of clarity, let's elect port `8080` to publish. 16 | 17 | In your `kool.yml`, apply the following changes: 18 | 19 | ```diff 20 | -npm: kool docker kooldev/node:20 npm 21 | +npm: kool docker -p 8080:8080 kooldev/node:20 npm 22 | ``` 23 | 24 | - Alternatively, if you don't want to publish the port for your general `kool run npm` commands, you may add a new entry. 25 | 26 | **> Laravel Mix** 27 | 28 | In your `webpack.mix.js`, include the following changes to the `mix.options` call. 29 | 30 | ```diff 31 | mix.options({ 32 | + hmrOptions: { 33 | + host: '0.0.0.0', 34 | + port: 8080, 35 | + }, 36 | }); 37 | ``` 38 | 39 | **> Laravel Vite** 40 | 41 | In your `vite.config.js` file, include the following changes to the `defineConfig` call. 42 | 43 | ```diff 44 | export default defineConfig({ 45 | + server: { 46 | + host: '0.0.0.0', 47 | + port: 8080, 48 | + }, 49 | plugins: [ 50 | laravel({ 51 | input: ['resources/css/app.css', 'resources/js/app.js'], 52 | refresh: true, 53 | }), 54 | ], 55 | }); 56 | ``` 57 | -------------------------------------------------------------------------------- /commands/info_test.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "kool-dev/kool/core/builder" 5 | "kool-dev/kool/core/environment" 6 | "kool-dev/kool/core/shell" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | func setupInfoTest(f *KoolInfo) { 14 | f.envStorage.Set("KOOL_FILTER_TESTING", "1") 15 | f.envStorage.Set("KOOL_TESTING", "1") 16 | } 17 | 18 | func fakeKoolInfo() *KoolInfo { 19 | return &KoolInfo{ 20 | *(newDefaultKoolService().Fake()), 21 | environment.NewFakeEnvStorage(), 22 | &builder.FakeCommand{}, 23 | &builder.FakeCommand{}, 24 | } 25 | } 26 | 27 | func TestInfo(t *testing.T) { 28 | f := fakeKoolInfo() 29 | 30 | setupInfoTest(f) 31 | 32 | output, err := execInfoCommand(NewInfoCmd(f), f) 33 | 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | for _, expected := range []string{"KOOL_FILTER_TESTING=1", "KOOL_TESTING=1"} { 39 | if !strings.Contains(output, expected) { 40 | t.Errorf("Expected '%s', got '%s'", expected, output) 41 | } 42 | } 43 | } 44 | 45 | func TestFilteredInfo(t *testing.T) { 46 | f := fakeKoolInfo() 47 | 48 | setupInfoTest(f) 49 | 50 | cmd := NewInfoCmd(f) 51 | cmd.SetArgs([]string{"FILTER"}) 52 | 53 | output, err := execInfoCommand(cmd, f) 54 | 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | 59 | expected := "KOOL_FILTER_TESTING=1" 60 | 61 | if !strings.Contains(output, expected) { 62 | t.Errorf("Expected '%s', got '%s'", expected, output) 63 | } 64 | } 65 | 66 | func execInfoCommand(cmd *cobra.Command, f *KoolInfo) (output string, err error) { 67 | if err = cmd.Execute(); err != nil { 68 | return 69 | } 70 | 71 | output = strings.Join(f.shell.(*shell.FakeShell).OutLines, "\n") 72 | return 73 | } 74 | -------------------------------------------------------------------------------- /core/environment/env.go: -------------------------------------------------------------------------------- 1 | package environment 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strings" 7 | 8 | homedir "github.com/mitchellh/go-homedir" 9 | ) 10 | 11 | var envFiles = []string{".env.local", ".env"} 12 | 13 | // InitEnvironmentVariables handles the reading of .env files and 14 | // setting up important environment variables necessary for kool 15 | // to operate as expected. 16 | func InitEnvironmentVariables(envStorage EnvStorage) { 17 | var ( 18 | homeDir, workDir string 19 | err error 20 | ) 21 | 22 | homeDir, err = homedir.Dir() 23 | if err != nil { 24 | log.Fatal("Could not evaluate HOME directory - ", err) 25 | } 26 | if envStorage.Get("HOME") == "" { 27 | envStorage.Set("HOME", homeDir) 28 | } 29 | 30 | initUid(envStorage) 31 | 32 | if envStorage.Get("PWD") == "" { 33 | workDir, err = os.Getwd() 34 | if err != nil { 35 | log.Fatal("Could not evaluate working directory - ", err) 36 | } 37 | envStorage.Set("PWD", workDir) 38 | } 39 | 40 | for _, envFile := range envFiles { 41 | if _, err = os.Stat(envFile); os.IsNotExist(err) { 42 | continue 43 | } 44 | 45 | err = envStorage.Load(envFile) 46 | if err != nil { 47 | log.Fatal("Failure loading environment file ", envFile, " error: '", err, "'") 48 | } 49 | } 50 | 51 | // Now that we loaded up the files, we will check for 52 | // missing variables that we need to fix 53 | if envStorage.Get("KOOL_NAME") == "" { 54 | pieces := strings.Split(envStorage.Get("PWD"), string(os.PathSeparator)) 55 | envStorage.Set("KOOL_NAME", pieces[len(pieces)-1]) 56 | } 57 | 58 | if envStorage.Get("KOOL_GLOBAL_NETWORK") == "" { 59 | envStorage.Set("KOOL_GLOBAL_NETWORK", "kool_global") 60 | } 61 | 62 | initAsuser(envStorage) 63 | } 64 | -------------------------------------------------------------------------------- /core/environment/fake_env_storage.go: -------------------------------------------------------------------------------- 1 | package environment 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/fireworkweb/godotenv" 7 | ) 8 | 9 | // FakeEnvStorage holds fake environment variables 10 | type FakeEnvStorage struct { 11 | Envs map[string]string 12 | CalledLoad bool 13 | EnvsHistory map[string][]string 14 | } 15 | 16 | // NewFakeEnvStorage creates a new FakeEnvStorage 17 | func NewFakeEnvStorage() *FakeEnvStorage { 18 | return &FakeEnvStorage{ 19 | Envs: make(map[string]string), 20 | EnvsHistory: make(map[string][]string), 21 | } 22 | } 23 | 24 | // Get get environment variable value (fake behavior) 25 | func (f *FakeEnvStorage) Get(key string) string { 26 | return f.Envs[key] 27 | } 28 | 29 | // Set set environment variable value (fake behavior) 30 | func (f *FakeEnvStorage) Set(key string, value string) { 31 | f.Envs[key] = value 32 | f.EnvsHistory[key] = append(f.EnvsHistory[key], value) 33 | } 34 | 35 | // Load load environment file (fake behavior) 36 | func (f *FakeEnvStorage) Load(filename string) error { 37 | f.CalledLoad = true 38 | envs, _ := godotenv.Read(filename) 39 | for k, v := range envs { 40 | if _, exists := f.Envs[k]; !exists { 41 | f.Envs[k] = v 42 | } 43 | } 44 | return nil 45 | } 46 | 47 | // All get all environment variables 48 | func (f *FakeEnvStorage) All() (envs []string) { 49 | for key, value := range f.Envs { 50 | envs = append(envs, fmt.Sprintf("%s=%s", key, value)) 51 | } 52 | 53 | return 54 | } 55 | 56 | // IsTrue checks whether the given environment variable is 57 | // to what would be a boolean value of true (fake behavior) 58 | func (f *FakeEnvStorage) IsTrue(key string) bool { 59 | value := f.Envs[key] 60 | return value == "1" || value == "true" 61 | } 62 | -------------------------------------------------------------------------------- /core/parser/errors.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // ErrMultipleDefinedScript happens when the script asked for is 10 | // found within more than one kool.yml files targeted. 11 | var ErrMultipleDefinedScript = fmt.Errorf("script was found in more than one kool.yml file") 12 | 13 | // IsMultipleDefinedScriptError tells whether the given error is parser.ErrMultipleDefinedScript 14 | func IsMultipleDefinedScriptError(err error) bool { 15 | return err.Error() == ErrMultipleDefinedScript.Error() 16 | } 17 | 18 | // ErrKoolYmlNotFound means there was no kool.yml file in the targeted folders 19 | var ErrKoolYmlNotFound = errors.New("could not find any kool.yml file") 20 | 21 | // ErrPossibleTypo implements error interface and can be used 22 | // to determine specific situations of not-found scripts but 23 | // where similar names exist, indicating a possible typo 24 | type ErrPossibleTypo struct { 25 | similars []string 26 | } 27 | 28 | // Error formats a default string with the similar names found 29 | func (e *ErrPossibleTypo) Error() string { 30 | if len(e.similars) == 1 { 31 | return fmt.Sprintf("did you mean '%s'?", e.similars[0]) 32 | } 33 | 34 | return fmt.Sprintf("did you mean one of ['%s']?", strings.Join(e.similars, "', '")) 35 | } 36 | 37 | // Similars get the possible similar scripts 38 | func (e *ErrPossibleTypo) Similars() []string { 39 | return e.similars 40 | } 41 | 42 | // SetSimilars set the possible similar scripts 43 | func (e *ErrPossibleTypo) SetSimilars(similars []string) { 44 | e.similars = similars 45 | } 46 | 47 | // IsPossibleTypoError tells whether the given error is parser.ErrPossibleTypo 48 | func IsPossibleTypoError(err error) (assert bool) { 49 | _, assert = err.(*ErrPossibleTypo) 50 | return 51 | } 52 | -------------------------------------------------------------------------------- /core/environment/env_test.go: -------------------------------------------------------------------------------- 1 | package environment 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | "testing" 8 | 9 | homedir "github.com/mitchellh/go-homedir" 10 | ) 11 | 12 | func TestInitEnvironmentVariables(t *testing.T) { 13 | f := NewFakeEnvStorage() 14 | 15 | originalEnvFiles := envFiles 16 | defer func() { envFiles = originalEnvFiles }() 17 | 18 | testEnvFile := filepath.Join(t.TempDir(), ".env.test") 19 | envFiles = []string{testEnvFile} 20 | 21 | if err := os.WriteFile(testEnvFile, []byte("FOO=bar\n"), os.ModePerm); err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | InitEnvironmentVariables(f) 26 | 27 | homeDir, _ := homedir.Dir() 28 | 29 | if envHomeDir := f.Envs["HOME"]; envHomeDir != homeDir { 30 | t.Errorf("expecting $HOME value '%s', got '%s'", homeDir, envHomeDir) 31 | } 32 | 33 | UID := uid() 34 | 35 | if envUID := f.Envs["UID"]; envUID != UID { 36 | t.Errorf("expecting $UID value '%s', got '%s'", UID, envUID) 37 | } 38 | 39 | workDir, _ := os.Getwd() 40 | 41 | if envWorkDir := f.Envs["PWD"]; envWorkDir != workDir { 42 | t.Errorf("expecting $PWD value '%s', got '%s'", workDir, envWorkDir) 43 | } 44 | 45 | if !f.CalledLoad { 46 | t.Error("did not call Load on EnvSotrage") 47 | } 48 | 49 | if foo := f.Envs["FOO"]; foo != "bar" { 50 | t.Errorf("expected FOO to be bar: %v", foo) 51 | } 52 | 53 | pieces := strings.Split(workDir, string(os.PathSeparator)) 54 | koolName := pieces[len(pieces)-1] 55 | 56 | if envKoolName := f.Envs["KOOL_NAME"]; envKoolName != koolName { 57 | t.Errorf("expecting $KOOL_NAME value '%s', got '%s'", koolName, envKoolName) 58 | } 59 | 60 | if envKoolNet := f.Envs["KOOL_GLOBAL_NETWORK"]; envKoolNet != "kool_global" { 61 | t.Errorf("expecting $KOOL_GLOBAL_NETWORK value 'kool_global', got '%s'", envKoolNet) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /docs/05-Commands-Reference/kool-completion.md: -------------------------------------------------------------------------------- 1 | ## kool completion 2 | 3 | Generate shell completion configuration script 4 | 5 | ### Synopsis 6 | 7 | Autocompletion: 8 | 9 | If you want to use kool autocompletion in your Unix shell, follow the appropriate instructions below. 10 | 11 | After running one of the below commands, remember to start a new shell for autocompletion to take effect. 12 | 13 | #### Bash 14 | 15 | Temporarily enable autocompletion for your current session only: 16 | 17 | $ source <(kool completion bash) 18 | 19 | Permanently enable autocompletion for all sessions: 20 | 21 | Linux: 22 | 23 | $ kool completion bash > /etc/bash_completion.d/kool 24 | 25 | macOS: 26 | 27 | $ kool completion bash > /usr/local/etc/bash_completion.d/kool 28 | 29 | #### Zsh 30 | 31 | If Zsh tab completion is not already initialized on your machine, run the following command to turn it on. 32 | 33 | $ echo "autoload -U compinit; compinit" >> ~/.zshrc 34 | 35 | Permanently enable autocompletion for all sessions: 36 | 37 | $ kool completion zsh > "${fpath[1]}/_kool" 38 | 39 | #### Fish 40 | 41 | Temporarily enable autocompletion for your current session only: 42 | 43 | $ kool completion fish | source 44 | 45 | Permanently enable autocompletion for all sessions: 46 | 47 | $ kool completion fish > ~/.config/fish/completions/kool.fish 48 | 49 | 50 | ``` 51 | kool completion [bash|zsh|fish|powershell] 52 | ``` 53 | 54 | ### Options 55 | 56 | ``` 57 | -h, --help help for completion 58 | ``` 59 | 60 | ### Options inherited from parent commands 61 | 62 | ``` 63 | --verbose Increases output verbosity 64 | -w, --working_dir string Changes the working directory for the command 65 | ``` 66 | 67 | ### SEE ALSO 68 | 69 | * [kool](kool) - Cloud native environments made easy 70 | 71 | -------------------------------------------------------------------------------- /docs/02-Kool-Cloud/15-Deploy-Lifecycle-Hooks.md: -------------------------------------------------------------------------------- 1 | # Hooks 2 | 3 | You have the ability to define hooks to run before or after every deploy you make. 4 | 5 | These hooks will run using the very same image that is being deployed. This is usually needed for common routines, such as running database migrations, sending alerts of updates to some other service, etc. 6 | 7 | # Before and After 8 | 9 | To illustrate the options you have in the `kool.cloud.yml` file: 10 | 11 | ```yaml 12 | services: 13 | app: 14 | # ... 15 | 16 | # The 'before' hook is a special section where we can define commands to be executed 17 | # right before a new deployment happens. 18 | # ATTENTION: current limitation - can only have a 'before' hook after a first deploy has created the environment. 19 | before: 20 | - [ sh, run-database-migrations.sh, arg1, arg2 ] 21 | 22 | # The 'after' hook is a special section where we can define procedures to be executed 23 | # right after a new deployment finishes. 24 | after: 25 | - [ sh, run-cache-version-update.sh, arg1, arg2 ] 26 | ``` 27 | 28 | ## Failures on lifecycle hooks 29 | 30 | Please notice that these lifecycle hooks are required for the new deploy to be successful—this means that **if any of them fail**—either `before` or `after` newly deployed container versions are running—**the whole deploy is going to be rolled back**. As you can imagine, this poses a challenge, especially on database migrations since they can be problematic and not backwards compatible with the previously running container version. 31 | 32 | ## Limitations 33 | 34 | The `before` hook can only be used after a first deploy has succeded for your environment - that is a limitation that should be lifted in the future, but currently can halt your first deploy if it includes a `before` hook. 35 | -------------------------------------------------------------------------------- /docs/02-Kool-Cloud/10-Persistent-Storage.md: -------------------------------------------------------------------------------- 1 | Containers were built to be ephemeral—and that is how we like them and how Kubernetes and all other container orchestrators usually work the best with them as well. 2 | 3 | But at times, we know that traditional web applications may not be ready to switch to network-based object storage instead of local disk storage. 4 | 5 | Kool.dev Cloud does offer you the ability to create persisted paths within your deployed containers. 6 | 7 | ```yaml 8 | services: 9 | app: 10 | # ... 11 | 12 | # Tells your app's root folder so all other paths can be relative (optional). 13 | root: /app 14 | 15 | # Containers are ephemeral, that means their filesystem do not persist across restarts. 16 | # If you want to persist stuff into the disk across deployments, you can do so by defining persistent paths here. 17 | persists: 18 | # Total size of the volume you want to attach to the running container. 19 | # This can be increased later, but it may take a while to apply the change. 20 | size: 10Gi 21 | # Paths to persist - within that single volume, you can have one or more paths 22 | # that are going to be mounted every time your containers are running. Note that 23 | # such mounts will be there for before/after hooks as well as daemon containers. 24 | paths: 25 | # The path within the container. Must be either aboslute or relative to the 'root' config. 26 | - path: /app/some/path/persisted 27 | # Tells the Deploy API to sync the folder from your built image to the persisted storage. 28 | # This is very helpful to start off with a known folder structure. 29 | sync: true 30 | # Tells what user and group should own the persisted folder (only used when sync: true) 31 | chown: user:group 32 | ``` -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | The Kool project accepts contributions via GitHub pull requests. This document outlines the process to help get your contribution accepted. 4 | 5 | At this point, the workflow is focused on supporting the core team with shipping new features and writing documentation. 6 | 7 | There are issues with a [`good first issue`](https://github.com/kool-dev/kool/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) label. Feel free to browse open issues, enter discussions, or get straight to work! 8 | 9 | ## Reporting a Security Issue 10 | 11 | As stated on [SECURITY.md](SECURITY.md), in case of a security issue or concern, please consider reporting it privately at first to [contact@kool.dev](mailto:contact@kool.dev). 12 | 13 | ### Rich Content Issues and PRs 14 | 15 | If applicable, please consider adding screenshots to help explain your issues or solutions. Recordings can also be very helpful to facilitate communication. 16 | 17 | - Check out [ASCII Cinema](https://asciinema.org/) for recording your terminal only. 18 | - For recording your whole screen, check out [ShareX Opensource Screen Capture](https://getsharex.com), or similar tools, and upload your recordings somewhere to share. 19 | 20 | ## Local Development Directions 21 | 22 | ### Lint, Formatting and Tests 23 | 24 | Before submitting a PR, make sure to run `fmt` and linters. 25 | 26 | ```bash 27 | kool run fmt 28 | kool run lint 29 | kool run test 30 | ``` 31 | 32 | ### Updating Command Signature Documentation 33 | 34 | The Cobra library offers a simple solution for generating markdown documentation for the usage of its commands. In order to update the generated markdown files, you must do the following: 35 | 36 | ```bash 37 | kool run make-docs 38 | git add . 39 | git commit -m "Updated commands docs (auto generated)" 40 | ``` 41 | -------------------------------------------------------------------------------- /docs/03-Presets/0-About-Presets.md: -------------------------------------------------------------------------------- 1 | # Kool Presets: Streamlining Your Development Experience 2 | 3 | Kool Presets are curated configurations tailored to popular stacks and frameworks, designed to jumpstart your projects with efficiency and ease. Whether you're working with Node.js, PHP, JavaScript, or other technologies, Kool Presets provide battle-tested configurations, saving you from the hassle of reinventing the wheel for each new project. 4 | 5 | These presets encapsulate best practices, allowing you to focus on your code rather than spending time on intricate setup processes. From popular backend frameworks like Laravel and NestJS to frontend powerhouses like Next.js and NuxtJS, Kool Presets cover a spectrum of technologies, offering you a head start in building robust and scalable applications. 6 | 7 | By leveraging Kool Presets, you not only reduce the learning curve associated with configuring different environments but also ensure consistency across your projects. With Kool, enjoy a unified and efficient development experience, whether you're a solo developer seeking a quick setup or a team collaborating on complex projects. Explore the available presets and elevate your development workflow with Kool Presets today. 8 | 9 | # Popular Presets 10 | 11 | ## Node 12 | 13 | - [NestJS](docs/03-Presets/NestJS.md) 14 | - [AdonisJs](docs/03-Presets/AdonisJs.md) 15 | - [Express.js](/docs/03-Presets/ExpressJS.md) 16 | 17 | ## PHP 18 | 19 | - [Laravel](/docs/03-Presets/Laravel.md) 20 | - [Laravel Octane](/docs/03-Presets/Laravel+Octane.md) 21 | - [Symfony](/docs/03-Presets/Symfony.md) 22 | - [CodeIgniter](/docs/03-Presets/CodeIgniter.md) 23 | 24 | ## Javascript 25 | 26 | - [Next.js](/docs/03-Presets/NextJS.md) 27 | - [NuxtJS](/docs/03-Presets/NuxtJS.md) 28 | 29 | ## Others 30 | 31 | - [Hugo](/docs/03-Presets/Hugo.md) 32 | - [WordPress](/docs/03-Presets/WordPress.md) 33 | -------------------------------------------------------------------------------- /commands/cloud_deploy_destroy.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "kool-dev/kool/core/environment" 6 | "kool-dev/kool/services/cloud/api" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // KoolDeployDestroy holds handlers and functions for using Deploy API 12 | type KoolDeployDestroy struct { 13 | DefaultKoolService 14 | 15 | env environment.EnvStorage 16 | apiDestroy api.DeployDestroy 17 | } 18 | 19 | // NewDeployDestroyCommand initializes new kool deploy Cobra command 20 | func NewDeployDestroyCommand(destroy *KoolDeployDestroy) *cobra.Command { 21 | return &cobra.Command{ 22 | Use: "destroy", 23 | Short: "Destroy an environment deployed to Kool.dev Cloud", 24 | Args: cobra.NoArgs, 25 | RunE: DefaultCommandRunFunction(destroy), 26 | 27 | DisableFlagsInUseLine: true, 28 | } 29 | } 30 | 31 | // NewKoolDeployDestroy creates a new pointer with default KoolDeployDestroy service dependencies 32 | func NewKoolDeployDestroy() *KoolDeployDestroy { 33 | return &KoolDeployDestroy{ 34 | *newDefaultKoolService(), 35 | environment.NewEnvStorage(), 36 | *api.NewDeployDestroy(), 37 | } 38 | } 39 | 40 | // Execute runs the deploy destroy logic - integrating with Deploy API 41 | func (d *KoolDeployDestroy) Execute(args []string) (err error) { 42 | var ( 43 | domain string 44 | resp *api.DeployDestroyResponse 45 | ) 46 | 47 | if url := d.env.Get("KOOL_API_URL"); url != "" { 48 | api.SetBaseURL(url) 49 | } 50 | 51 | if domain = d.env.Get("KOOL_DEPLOY_DOMAIN"); domain == "" { 52 | err = fmt.Errorf("missing deploy domain (env KOOL_DEPLOY_DOMAIN)") 53 | return 54 | } 55 | 56 | d.apiDestroy.Query().Set("domain", domain) 57 | 58 | if resp, err = d.apiDestroy.Call(); err != nil { 59 | return 60 | } 61 | 62 | d.Shell().Success(fmt.Sprintf("Environment (ID: %d) scheduled for deleting.", resp.Environment.ID)) 63 | 64 | return 65 | } 66 | -------------------------------------------------------------------------------- /core/shell/fake_table_writer.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "sort" 7 | "strings" 8 | ) 9 | 10 | // FakeTableWriter mock table writer for testing 11 | type FakeTableWriter struct { 12 | CalledSetWriter, CalledAppendHeader, CalledAppendRow, CalledRender bool 13 | Headers, Rows [][]interface{} 14 | TableOut string 15 | } 16 | 17 | // SetWriter fake SetWriter behavior 18 | func (f *FakeTableWriter) SetWriter(w io.Writer) { 19 | f.CalledSetWriter = true 20 | } 21 | 22 | // AppendHeader fake AppendHeader behavior 23 | func (f *FakeTableWriter) AppendHeader(columns ...interface{}) { 24 | f.CalledAppendHeader = true 25 | f.Headers = append(f.Headers, columns) 26 | } 27 | 28 | // AppendRow fake AppendRow behavior 29 | func (f *FakeTableWriter) AppendRow(columns ...interface{}) { 30 | f.CalledAppendRow = true 31 | f.Rows = append(f.Rows, columns) 32 | } 33 | 34 | // Render fake Render behavior 35 | func (f *FakeTableWriter) Render() { 36 | f.CalledRender = true 37 | 38 | for _, columns := range f.Headers { 39 | columnsStr := make([]string, len(columns)) 40 | 41 | for i := range columns { 42 | columnsStr[i] = fmt.Sprintf("%v", columns[i]) 43 | } 44 | 45 | f.TableOut = f.TableOut + fmt.Sprintln(strings.Join(columnsStr, " | ")) 46 | } 47 | 48 | for _, columns := range f.Rows { 49 | columnsStr := make([]string, len(columns)) 50 | 51 | for i := range columns { 52 | columnsStr[i] = fmt.Sprintf("%v", columns[i]) 53 | } 54 | 55 | f.TableOut = f.TableOut + fmt.Sprintln(strings.Join(columnsStr, " | ")) 56 | } 57 | } 58 | 59 | // SortBy fake SortBy behavior 60 | func (f *FakeTableWriter) SortBy(column int) { 61 | sort.SliceStable(f.Rows, func(i, j int) bool { 62 | return f.Rows[i][column-1].(string) < f.Rows[j][column-1].(string) 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /core/shell/table_writer_test.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestNewTableWriter(t *testing.T) { 11 | tableWriter := NewTableWriter() 12 | 13 | if _, ok := tableWriter.(*DefaultTableWriter); !ok { 14 | t.Errorf("NewTableWriter() did not return a *DefaultTableWriter") 15 | } 16 | } 17 | func TestTableWriter(t *testing.T) { 18 | tableWriter := NewTableWriter() 19 | 20 | b := bytes.NewBufferString("") 21 | tableWriter.SetWriter(b) 22 | 23 | tableWriter.AppendHeader("header") 24 | 25 | tableWriter.AppendRow("row") 26 | 27 | tableWriter.Render() 28 | 29 | var ( 30 | out []byte 31 | err error 32 | ) 33 | 34 | if out, err = io.ReadAll(b); err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | output := strings.TrimSpace(string(out)) 39 | expected := ` 40 | +--------+ 41 | | HEADER | 42 | +--------+ 43 | | row | 44 | +--------+ 45 | ` 46 | expected = strings.TrimSpace(expected) 47 | 48 | if expected != output { 49 | t.Errorf("expecting output '%s', got '%s'", expected, output) 50 | } 51 | } 52 | 53 | func TestSortByTableWriter(t *testing.T) { 54 | tableWriter := NewTableWriter() 55 | 56 | b := bytes.NewBufferString("") 57 | tableWriter.SetWriter(b) 58 | 59 | tableWriter.AppendHeader("header") 60 | 61 | tableWriter.AppendRow("zRow") 62 | tableWriter.AppendRow("aRow") 63 | 64 | tableWriter.SortBy(1) 65 | 66 | tableWriter.Render() 67 | 68 | var ( 69 | out []byte 70 | err error 71 | ) 72 | 73 | if out, err = io.ReadAll(b); err != nil { 74 | t.Fatal(err) 75 | } 76 | 77 | output := strings.TrimSpace(string(out)) 78 | expected := ` 79 | +--------+ 80 | | HEADER | 81 | +--------+ 82 | | aRow | 83 | | zRow | 84 | +--------+ 85 | ` 86 | expected = strings.TrimSpace(expected) 87 | 88 | if expected != output { 89 | t.Errorf("expecting output '%s', got '%s'", expected, output) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /services/cloud/deployer.go: -------------------------------------------------------------------------------- 1 | package cloud 2 | 3 | import ( 4 | "kool-dev/kool/core/environment" 5 | "kool-dev/kool/core/shell" 6 | "kool-dev/kool/services/cloud/api" 7 | ) 8 | 9 | // Deployer service handles the deployment process 10 | type Deployer struct { 11 | env environment.EnvStorage 12 | out shell.Shell 13 | } 14 | 15 | // NewDeployer creates a new handler for using the 16 | // Kool Dev API for deploying your application. 17 | func NewDeployer() *Deployer { 18 | return &Deployer{ 19 | env: environment.NewEnvStorage(), 20 | out: shell.NewShell(), 21 | } 22 | } 23 | 24 | // SendFile integrates with the API to send the tarball 25 | func (d *Deployer) CreateDeploy(tarballPath string) (resp *api.DeployCreateResponse, err error) { 26 | var create = api.NewDeployCreate() 27 | 28 | if err = create.PostFile("deploy", tarballPath, "deploy.tgz"); err != nil { 29 | return 30 | } 31 | 32 | _ = create.PostField("cluster", d.env.Get("KOOL_CLOUD_CLUSTER")) 33 | _ = create.PostField("domain", d.env.Get("KOOL_DEPLOY_DOMAIN")) 34 | _ = create.PostField("domain_extras", d.env.Get("KOOL_DEPLOY_DOMAIN_EXTRAS")) 35 | _ = create.PostField("www_redirect", d.env.Get("KOOL_DEPLOY_WWW_REDIRECT")) 36 | 37 | resp, err = create.Run() 38 | 39 | return 40 | } 41 | 42 | func (d *Deployer) StartDeploy(created *api.DeployCreateResponse) (started *api.DeployStartResponse, err error) { 43 | var start = api.NewDeployStart(created) 44 | 45 | started, err = start.Run() 46 | return 47 | } 48 | 49 | func (d *Deployer) PingDeploy(created *api.DeployCreateResponse) (pinged *api.DeployPingResponse, err error) { 50 | var ping = api.NewDeployPing(created) 51 | 52 | pinged, err = ping.Run() 53 | return 54 | } 55 | 56 | func (d *Deployer) BuildError(created *api.DeployCreateResponse, gotErr error) (err error) { 57 | var buildErr = api.NewDeployError(created, gotErr) 58 | 59 | err = buildErr.Run() 60 | return 61 | } 62 | -------------------------------------------------------------------------------- /services/yamler/merger_test.go: -------------------------------------------------------------------------------- 1 | package yamler 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | func testMerger(t *testing.T, foo, bar, expected string) { 11 | var y1, y2 = new(yaml.Node), new(yaml.Node) 12 | 13 | _ = yaml.Unmarshal([]byte(foo), y1) 14 | _ = yaml.Unmarshal([]byte(bar), y2) 15 | 16 | m := &DefaultMerger{} 17 | 18 | if err := m.Merge(y2, y1); err != nil { 19 | t.Errorf("unexpected error merging yaml: %v", err) 20 | } 21 | 22 | bs, _ := yaml.Marshal(y1) 23 | merged := strings.Trim(string(bs), " \t\n") 24 | if merged != expected { 25 | t.Errorf("unexpected merge result: expected '%s' but got '%s'", expected, merged) 26 | } 27 | } 28 | 29 | func TestMergerMap(t *testing.T) { 30 | foo := "foo: yyy" 31 | bar := "bar: xxx" 32 | expected := foo + "\n" + bar 33 | 34 | testMerger(t, foo, bar, expected) 35 | } 36 | 37 | func TestMergerMapOverride(t *testing.T) { 38 | foo := "foo: yyy" 39 | bar := "bar: xxx\nfoo: zzz" 40 | expected := "foo: zzz\nbar: xxx" 41 | 42 | testMerger(t, foo, bar, expected) 43 | } 44 | 45 | func TestMergerComments(t *testing.T) { 46 | comment := "# comment\nfoo: test" 47 | bar := "bar: xxx" 48 | expected := comment + "\n" + bar 49 | 50 | testMerger(t, comment, bar, expected) 51 | } 52 | 53 | func TestMergerListAppend(t *testing.T) { 54 | foo := "foo: [yyy, zzz]" 55 | bar := "foo: [xxx]" 56 | expected := "foo: [yyy, zzz, xxx]" 57 | 58 | testMerger(t, foo, bar, expected) 59 | } 60 | 61 | func TestMergerMapNested(t *testing.T) { 62 | foo := "foo:" 63 | bar := "foo:\n bar: xxx" 64 | expected := "foo:\n bar: xxx" 65 | 66 | testMerger(t, foo, bar, expected) 67 | } 68 | 69 | func TestMergerMapNestedOverride(t *testing.T) { 70 | foo := "foo:\n bar: yyy\n zuk: ppp" 71 | bar := "foo:\n bar: xxx" 72 | expected := "foo:\n bar: xxx\n zuk: ppp" 73 | 74 | testMerger(t, foo, bar, expected) 75 | } 76 | -------------------------------------------------------------------------------- /services/cloud/api/errors.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // ErrBadAPIServer represents some issue in the API side 10 | var ErrBadAPIServer error 11 | 12 | // ErrMissingToken is the lack of the access token 13 | var ErrMissingToken error 14 | 15 | // ErrDeployFailed is returned when checking the status of a failed deploy 16 | var ErrDeployFailed error 17 | 18 | // ErrUnauthorized unauthorized; please check your KOOL_API_TOKEN 19 | var ErrUnauthorized error 20 | 21 | // failed validating deploy payload 22 | var ErrPayloadValidation error 23 | 24 | // ErrBadResponseStatus unexpected return status 25 | var ErrBadResponseStatus error 26 | 27 | // ErrUnexpectedResponse bad API response; please ask for support 28 | var ErrUnexpectedResponse error 29 | 30 | // ErrAPI reprents a default error returned from the API 31 | type ErrAPI struct { 32 | Status int 33 | 34 | Message string `json:"message"` 35 | Errors map[string]interface{} `json:"errors"` 36 | } 37 | 38 | // Error returns the string representation for the error 39 | func (e *ErrAPI) Error() string { 40 | if e.Errors != nil { 41 | s := []string{} 42 | for k, e := range e.Errors { 43 | s = append(s, fmt.Sprintf("\t%s > %v", k, e.([]interface{})[0])) 44 | } 45 | return fmt.Sprintf("\n%d - %s\n\n%s\n", e.Status, e.Message, strings.Join(s, "\n")) 46 | } 47 | return fmt.Sprintf("\n%d - %s\n", e.Status, e.Message) 48 | } 49 | 50 | func init() { 51 | ErrBadAPIServer = errors.New("bad API server response") 52 | ErrDeployFailed = errors.New("deploy process has failed") 53 | ErrUnauthorized = errors.New("unauthorized; please check your KOOL_API_TOKEN") 54 | ErrPayloadValidation = errors.New("failed validating deploy payload") 55 | ErrBadResponseStatus = errors.New("unexpected return status") 56 | ErrUnexpectedResponse = errors.New("bad API response; please ask for support") 57 | ErrMissingToken = errors.New("missing KOOL_API_TOKEN") 58 | } 59 | -------------------------------------------------------------------------------- /docs/05-Commands-Reference/0-kool.md: -------------------------------------------------------------------------------- 1 | ## kool 2 | 3 | Cloud native environments made easy 4 | 5 | ### Synopsis 6 | 7 | From development to production, a robust and easy-to-use developer tool 8 | that makes Docker container adoption quick and easy for building and deploying cloud native 9 | applications. 10 | 11 | Complete documentation is available at https://kool.dev/docs 12 | 13 | ``` 14 | kool 15 | ``` 16 | 17 | ### Options 18 | 19 | ``` 20 | -h, --help help for kool 21 | --verbose Increases output verbosity 22 | -w, --working_dir string Changes the working directory for the command 23 | ``` 24 | 25 | ### SEE ALSO 26 | 27 | * [kool cloud](kool-cloud) - Interact with Kool.dev Cloud and manage your deployments. 28 | * [kool create](kool-create) - Create a new project using a preset 29 | * [kool docker](kool-docker) - Create a new container (a powered up 'docker run') 30 | * [kool exec](kool-exec) - Execute a command inside a running service container 31 | * [kool info](kool-info) - Print out information about the local environment 32 | * [kool logs](kool-logs) - Display log output from running service containers 33 | * [kool preset](kool-preset) - Install configuration files customized for Kool in the current directory 34 | * [kool recipe](kool-recipe) - Adds configuration for some recipe in the current work directory. 35 | * [kool restart](kool-restart) - Restart running service containers (the same as 'kool stop' followed by 'kool start') 36 | * [kool run](kool-run) - Execute a script defined in kool.yml 37 | * [kool self-update](kool-self-update) - Update kool to the latest version 38 | * [kool share](kool-share) - Live share your local environment on the Internet using an HTTP tunnel 39 | * [kool start](kool-start) - Start service containers defined in docker-compose.yml 40 | * [kool status](kool-status) - Show the status of all service containers 41 | * [kool stop](kool-stop) - Stop and destroy running service containers 42 | 43 | -------------------------------------------------------------------------------- /core/network/network_test.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "kool-dev/kool/core/builder" 5 | "kool-dev/kool/core/shell" 6 | "testing" 7 | ) 8 | 9 | func TestDefaultHandler(t *testing.T) { 10 | var c Handler = NewHandler(&shell.FakeShell{}) 11 | 12 | if _, assert := c.(*DefaultHandler); !assert { 13 | t.Errorf("NewHandler() did not return a *DefaultHandler") 14 | } 15 | } 16 | 17 | func TestGlobalNetworkExists(t *testing.T) { 18 | var h Handler 19 | 20 | checkNetCmd := &builder.FakeCommand{MockExecOut: "NetworkID"} 21 | checkNetCmd.MockCmd = "check" 22 | 23 | createNetCmd := &builder.FakeCommand{} 24 | createNetCmd.MockCmd = "create" 25 | 26 | s := &shell.FakeShell{} 27 | h = &DefaultHandler{checkNetCmd, createNetCmd, s} 28 | 29 | err := h.HandleGlobalNetwork("global_network") 30 | 31 | if val, ok := h.(*DefaultHandler).shell.(*shell.FakeShell).CalledExec["check"]; !val || !ok { 32 | t.Errorf("HandleGlobalNetwork() did not check if network exists.") 33 | } 34 | 35 | if val, ok := h.(*DefaultHandler).shell.(*shell.FakeShell).CalledInteractive["create"]; val && ok { 36 | t.Errorf("HandleGlobalNetwork() should not try to create the global network if it already exists.") 37 | } 38 | 39 | if err != nil { 40 | t.Errorf("Expected no errors, got %v", err) 41 | } 42 | } 43 | 44 | func TestGlobalNetworkNotExists(t *testing.T) { 45 | var h Handler 46 | 47 | checkNetCmd := &builder.FakeCommand{} 48 | checkNetCmd.MockCmd = "check" 49 | 50 | createNetCmd := &builder.FakeCommand{} 51 | createNetCmd.MockCmd = "create" 52 | 53 | s := &shell.FakeShell{} 54 | h = &DefaultHandler{checkNetCmd, createNetCmd, s} 55 | 56 | err := h.HandleGlobalNetwork("global_network") 57 | 58 | if err != nil { 59 | t.Errorf("Expected no errors, got %v", err) 60 | } 61 | 62 | if val, ok := h.(*DefaultHandler).shell.(*shell.FakeShell).CalledInteractive["create"]; !val || !ok { 63 | t.Errorf("HandleGlobalNetwork() is not trying to create the global network when it not exists.") 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: '$RESOLVED_VERSION' 2 | tag-template: '$RESOLVED_VERSION' 3 | autolabeler: 4 | - label: 'release-drafter:added' 5 | body: 6 | - '/Feature\s*\|\s*Yes/' 7 | - label: 'release-drafter:changed' 8 | body: 9 | - '/Improvement\s*\|\s*Yes/' 10 | - '/Refactor\s*\|\s*Yes/' 11 | - label: 'release-drafter:fixed' 12 | body: 13 | - '/Bug Fix\s*\|\s*Yes/' 14 | - label: 'release-drafter:documentation' 15 | body: 16 | - '/Documentation\s*\|\s*Yes/' 17 | - label: 'release-drafter:removed' 18 | body: 19 | - '/Removed\s*\|\s*Yes/' 20 | - label: 'release-drafter:major' 21 | body: 22 | - '/Removed\s*\|\s*Yes/' 23 | - '/Break Change\s*\|\s*Yes/' 24 | - label: 'release-drafter:minor' 25 | body: 26 | - '/Feature\s*\|\s*Yes/' 27 | - '/Improvement\s*\|\s*Yes/' 28 | - '/Refactor\s*\|\s*Yes/' 29 | - label: 'release-drafter:patch' 30 | body: 31 | - '/Bug Fix\s*\|\s*Yes/' 32 | - '/Documentation\s*\|\s*Yes/' 33 | categories: 34 | - title: 'Added' 35 | labels: 36 | - 'feature' 37 | - 'new' 38 | - 'release-drafter:added' 39 | - title: 'Changed' 40 | labels: 41 | - 'enhancement' 42 | - 'chore' 43 | - 'refactor' 44 | - 'release-drafter:changed' 45 | - title: 'Fixed' 46 | labels: 47 | - 'fix' 48 | - 'bugfix' 49 | - 'bug' 50 | - 'release-drafter:fixed' 51 | - title: 'Documentation' 52 | labels: 53 | - 'docs' 54 | - 'documentation' 55 | - 'release-drafter:documentation' 56 | - title: 'Removed' 57 | labels: 58 | - 'remove' 59 | - 'drop' 60 | - 'release-drafter:removed' 61 | 62 | exclude-labels: 63 | - 'release-drafter:skip' 64 | 65 | version-resolver: 66 | major: 67 | labels: 68 | - 'release-drafter:major' 69 | minor: 70 | labels: 71 | - 'release-drafter:minor' 72 | patch: 73 | labels: 74 | - 'release-drafter:patch' 75 | default: patch 76 | 77 | template: | 78 | $CHANGES 79 | -------------------------------------------------------------------------------- /commands/cloud_deploy_destroy_test.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "kool-dev/kool/core/environment" 6 | "kool-dev/kool/core/shell" 7 | "kool-dev/kool/services/cloud/api" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestNewDeployDestroyCommand(t *testing.T) { 13 | destroy := NewKoolDeployDestroy() 14 | cmd := NewDeployDestroyCommand(destroy) 15 | if cmd.Use != "destroy" { 16 | t.Errorf("bad command use: %s", cmd.Use) 17 | } 18 | 19 | if _, ok := destroy.env.(*environment.DefaultEnvStorage); !ok { 20 | t.Error("unexpected default env on destroy") 21 | } 22 | } 23 | 24 | func TestDeployDestroyExec(t *testing.T) { 25 | destroy := &KoolDeployDestroy{ 26 | *(newDefaultKoolService().Fake()), 27 | environment.NewFakeEnvStorage(), 28 | *api.NewDeployDestroy(), 29 | } 30 | 31 | destroy.env.Set("KOOL_API_TOKEN", "fake token") 32 | destroy.env.Set("KOOL_API_URL", "fake-url") 33 | 34 | args := []string{} 35 | 36 | if err := destroy.Execute(args); !strings.Contains(err.Error(), "missing deploy domain") { 37 | t.Errorf("unexpected error - expected missing deploy domain, got: %v", err) 38 | } 39 | 40 | destroy.env.Set("KOOL_DEPLOY_DOMAIN", "domain.com") 41 | 42 | destroy.apiDestroy.Endpoint.(*api.DefaultEndpoint).Fake() 43 | destroy.apiDestroy.Endpoint.(*api.DefaultEndpoint).MockErr(errors.New("failed call")) 44 | 45 | if err := destroy.Execute(args); !strings.Contains(err.Error(), "failed call") { 46 | t.Errorf("unexpected error - expected failed call, got: %v", err) 47 | } 48 | 49 | destroy.apiDestroy.Endpoint.(*api.DefaultEndpoint).MockErr(nil) 50 | destroy.apiDestroy.Endpoint.(*api.DefaultEndpoint).MockResp(&api.DeployDestroyResponse{ 51 | Environment: struct { 52 | ID int `json:"id"` 53 | }{ 54 | ID: 100, 55 | }, 56 | }) 57 | 58 | if err := destroy.Execute(args); err != nil { 59 | t.Errorf("unexpected error, got: %v", err) 60 | } 61 | 62 | if !strings.Contains(destroy.shell.(*shell.FakeShell).SuccessOutput[0].(string), "ID: 100") { 63 | t.Errorf("did not get success message") 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /docs/02-Kool-Cloud/05-Environment-Variables.md: -------------------------------------------------------------------------------- 1 | Most applications and frameworks will rely on environment variables to configure important aspects of their functions, specially for providing credentials and other secrets your app needs to work and access other resources. 2 | 3 | Kool.dev Cloud supports a few different ways you can define your environment variables for a deploying container, so pick the one that best suits you. 4 | 5 | ### Using `kool.deploy.env` file for deploy 6 | 7 | `kool.deploy.env` should be a `.env` formatted file. You can point to it like this: 8 | 9 | ```yaml 10 | services: 11 | app: 12 | # ... 13 | environment: kool.deploy.env 14 | ``` 15 | 16 | Upon deployment, all of the variables within that file will be parsed, placeholders replaced—if you have any—and then **each variable will become a real environment variable in the running container**. 17 | 18 | This option is usually best suited for automated CI routines since you work your way to have a different `kool.deploy.env` file for each of your deploying environments (i.e., staging and production). 19 | 20 | ### Using a plain YAML object for environment variables 21 | 22 | ```yaml 23 | services: 24 | app: 25 | # ... 26 | environment: 27 | FOO: bar 28 | ``` 29 | 30 | You can simply use a YAML map of values that will become your environment variables in the running deployed container. This is handy sometimes when you have simple and not sensitive variables you want to add to a container for deploy. 31 | 32 | ### Build a `.env` file inside the running container 33 | 34 | If your application does rely on and requires a `.env` file existing in the running container, you may achieve so by using the `env:` entry: 35 | 36 | ```yaml 37 | services: 38 | app: 39 | # ... 40 | 41 | # 'env' is a different option that allows you to build a file inside your running container. 42 | env: 43 | source: kool.deploy.env 44 | target: .env 45 | ``` 46 | 47 | This is useful for apps that require the .env file, but you do not wish to have that built into your Docker image itself. 48 | --------------------------------------------------------------------------------