kamal-easy 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1a04dcf9e1378966de7e532f431dad98804e5e3b9e7c03b71a79c43e8903e5d8
4
+ data.tar.gz: a57516ce3a7fd5fe6a86fa6768629017eaa9f74f20fecd7f1722a9fabd10c583
5
+ SHA512:
6
+ metadata.gz: 21aafcdc9097152fde8bc371d02d54cbadec09f57f30b44ba024533f9cd2e9fae66ea4aed103fbb0ed957600d7a8ee511baea6a3236383c5d5f15b3992606e46
7
+ data.tar.gz: 23c094ba49f1046f04a7ece78e9fd8e3d1e360cba4f5268c92a578c1b7d0afe79a9e3c44888d887fa7cac1fb74247900349baf570a9daebb3bc7d34c5399f761
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # Kamal Easy
2
+
3
+ **Unified deployment wrapper for Kamal.**
4
+ Simplifies multi-environment deployments (UAT/Production) and provides unified commands for logs and access to the remote console.
5
+
6
+ ## 🚀 Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'kamal-easy', git: 'https://github.com/overnet/kamal-easy.git'
12
+ # OR locally during development:
13
+ # gem 'kamal-easy', path: '../gems/kamal-easy'
14
+ ```
15
+
16
+ And then execute:
17
+
18
+ ```bash
19
+ bundle install
20
+ ```
21
+
22
+ ## ⚙️ Configuration
23
+
24
+ Run the installer to generate the configuration file:
25
+
26
+ ```bash
27
+ bundle exec kamal-easy install
28
+ ```
29
+
30
+ This will create `config/kamal-easy.yml`. Configure it for your project:
31
+
32
+ ```yaml
33
+ # config/kamal-easy.yml
34
+ environments:
35
+ uat:
36
+ env_file: .env.uat
37
+ credentials_file: config/credentials/uat.yml.enc
38
+ staging:
39
+ env_file: .env.staging
40
+ credentials_file: config/credentials/staging.yml.enc
41
+ production:
42
+ env_file: .env.production
43
+ credentials_file: config/credentials/production.yml.enc
44
+
45
+ components:
46
+ backend:
47
+ path: .
48
+ kamal_cmd: "bundle exec kamal"
49
+ container_name_pattern: "your-app-backend-api" # For console access
50
+ mandatory_files:
51
+ - config/deploy.yml
52
+ - Dockerfile
53
+ frontend:
54
+ path: ../your-app-frontend
55
+ kamal_cmd: "kamal"
56
+ mandatory_files:
57
+ - config/deploy.yml
58
+ ```
59
+
60
+ ### Mandatory Files
61
+ - **`.env.uat` / `.env.staging` / `.env.production`**: Must exist in the component directory and contain necessary secrets (e.g., `RAILS_MASTER_KEY`).
62
+ - **`config/deploy.yml`**: Standard Kamal configuration must be present in each component directory.
63
+ - **`Dockerfile`**: Required for building images.
64
+
65
+ ## 🛠️ Usage
66
+
67
+ ### 1. Deployment (`kamal-easy deploy`)
68
+ Deploy specific components or the entire stack.
69
+
70
+ **Flags**:
71
+ - `--uat`: Deploy to UAT environment
72
+ - `--staging`: Deploy to Staging environment
73
+ - `--prod`: Deploy to Production environment
74
+ - `--all`: Deploy backend, frontend, and restart DB
75
+ - `--backend`: Deploy only backend
76
+ - `--frontend`: Deploy only frontend
77
+ - `--db`: Restart database accessory
78
+ - `--prune` / `-p`: Prune Docker system (images/containers) before deploy
79
+
80
+ ```bash
81
+ # Deploy EVERYTHING (Backend + Frontend + DB)
82
+ bundle exec kamal-easy deploy --all --prod
83
+
84
+ # Deploy Specific Component
85
+ bundle exec kamal-easy deploy --backend --uat
86
+ bundle exec kamal-easy deploy --frontend --staging
87
+ bundle exec kamal-easy deploy --db --prod
88
+ ```
89
+
90
+ ### 2. Logs (`kamal-easy logs`)
91
+ Stream logs from the remote container.
92
+
93
+ **Aliases**:
94
+ - `--follow` -> `-f`
95
+ - `--lines` -> `-n`
96
+ - `--grep` -> `-g`
97
+
98
+ ```bash
99
+ # Follow live logs (UAT)
100
+ bundle exec kamal-easy logs --uat -f
101
+
102
+ # View last 500 lines
103
+ bundle exec kamal-easy logs --prod -n 500
104
+
105
+ # Grep for errors
106
+ bundle exec kamal-easy logs --prod -g "Error"
107
+ ```
108
+
109
+ ### 3. Remote Console (`kamal-easy console`)
110
+ Securely access the remote Rails console.
111
+
112
+ **Aliases**:
113
+ - `console` -> `c`, `rails_console`
114
+
115
+ ```bash
116
+ # Connect to UAT Console
117
+ bundle exec kamal-easy c --uat
118
+
119
+ # Connect to Production
120
+ bundle exec kamal-easy rails_console --prod
121
+ ```
122
+
123
+ ### 4. Cleanup (`kamal-easy prune`)
124
+ Manage disk usage by removing old versions.
125
+
126
+ ```bash
127
+ # Keep last 3 versions (Default)
128
+ bundle exec kamal-easy prune --uat
129
+
130
+ # Keep last 5 versions
131
+ bundle exec kamal-easy prune --uat --retain 5
132
+
133
+ # Hard Cleanup (Aggressive docker system prune)
134
+ # WARNING: Deletes all stopped containers and unused images
135
+ bundle exec kamal-easy prune --uat --hard
136
+ ```
137
+
138
+ ## 🏗️ Deployment Architecture (Reference)
139
+ This gem assumes a setup where:
140
+ 1. **Backend & Frontend** are in sibling directories.
141
+ 2. **Zero Downtime** is handled via Kamal Proxy / Traefik (internal port binding).
142
+ 3. **Credentials** are managed via `RAILS_MASTER_KEY`.
data/bin/kamal-easy ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "kamal_easy"
4
+ require "kamal_easy/cli"
5
+
6
+ KamalEasy::CLI.start(ARGV)
@@ -0,0 +1,27 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "kamal_easy/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "kamal-easy"
8
+ spec.version = KamalEasy::VERSION
9
+ spec.authors = ["Ruslan Vikhor"]
10
+ spec.email = ["ruslan@overnet.com"]
11
+ spec.summary = "Unified deployment wrapper for Kamal"
12
+ spec.description = "Simplifies Kamal deployments with multi-environment support (UAT/Prod) and unified commands for logs and console."
13
+ spec.homepage = "https://github.com/overnet/kamal-easy"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.3.0"
16
+
17
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
18
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) || f.match(%r{\.gem$}) }
19
+ end
20
+ spec.bindir = "bin"
21
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_dependency "thor", "~> 1.2"
25
+ # Relaxed dependency to avoid conflicts with Rails apps running dotenv 3.x
26
+ spec.add_dependency "dotenv", ">= 2.8", "< 4.0"
27
+ end
@@ -0,0 +1,211 @@
1
+ require "thor"
2
+ require "dotenv"
3
+ require "kamal_easy/config"
4
+
5
+ module KamalEasy
6
+ class CLI < Thor
7
+ check_unknown_options!
8
+
9
+ class_option :uat, type: :boolean, desc: "Run command in UAT environment", default: false
10
+ class_option :staging, type: :boolean, desc: "Run command in Staging environment", default: false
11
+ class_option :prod, type: :boolean, desc: "Run command in Production environment", default: false
12
+ class_option :production, type: :boolean, desc: "Alias for --prod", default: false
13
+
14
+ desc "install", "Generate configuration file"
15
+ def install
16
+ config_path = "config/kamal-easy.yml"
17
+ if File.exist?(config_path)
18
+ puts "⚠️ #{config_path} already exists."
19
+ else
20
+ template = <<~YAML
21
+ environments:
22
+ uat:
23
+ env_file: .env.uat
24
+ credentials_file: config/credentials/uat.yml.enc
25
+ staging:
26
+ env_file: .env.staging
27
+ credentials_file: config/credentials/staging.yml.enc
28
+ production:
29
+ env_file: .env.production
30
+ credentials_file: config/credentials/production.yml.enc
31
+
32
+ components:
33
+ backend:
34
+ path: .
35
+ kamal_cmd: "bundle exec kamal"
36
+ container_name_pattern: "your-app-backend-api"
37
+ mandatory_files:
38
+ - config/deploy.yml
39
+ - Dockerfile
40
+ frontend:
41
+ path: ../your-app-frontend
42
+ kamal_cmd: "kamal"
43
+ mandatory_files:
44
+ - config/deploy.yml
45
+ YAML
46
+ File.write(config_path, template)
47
+ puts "✅ Created #{config_path}"
48
+ end
49
+ end
50
+
51
+ desc "deploy [COMPONENT]", "Deploy specific component or everything"
52
+ method_option :all, type: :boolean, desc: "Deploy all components"
53
+ method_option :backend, type: :boolean, desc: "Deploy backend"
54
+ method_option :frontend, type: :boolean, desc: "Deploy frontend"
55
+ method_option :db, type: :boolean, desc: "Restart database"
56
+ method_option :prune, aliases: "-p", type: :boolean, desc: "Prune Docker before deploy"
57
+ def deploy
58
+ config = KamalEasy::Config.load
59
+ env_key, env_vars = load_environment(config)
60
+
61
+ prune_docker(config, env_vars) if options[:prune]
62
+
63
+ if options[:all]
64
+ deploy_backend(config, env_vars)
65
+ deploy_frontend(config, env_vars)
66
+ restart_db(config, env_vars)
67
+ else
68
+ deploy_backend(config, env_vars) if options[:backend]
69
+ deploy_frontend(config, env_vars) if options[:frontend]
70
+ restart_db(config, env_vars) if options[:db]
71
+ end
72
+
73
+ if !options[:all] && !options[:backend] && !options[:frontend] && !options[:db]
74
+ help("deploy")
75
+ exit(1)
76
+ end
77
+ end
78
+
79
+ desc "logs", "View remote logs"
80
+ method_option :follow, aliases: "-f", type: :boolean, desc: "Follow logs", default: false
81
+ method_option :lines, aliases: "-n", type: :numeric, desc: "Number of lines", default: 100
82
+ method_option :grep, aliases: "-g", type: :string, desc: "Filter pattern"
83
+ def logs
84
+ config = KamalEasy::Config.load
85
+ env_key, env_vars = load_environment(config)
86
+
87
+ backend_config = config.components["backend"]
88
+ abort "❌ Backend component not configured" unless backend_config
89
+
90
+ cmd = "#{backend_config['kamal_cmd']} app logs"
91
+ cmd += " --lines #{options[:lines]}"
92
+ cmd += " --follow" if options[:follow]
93
+ cmd += " --grep '#{options[:grep]}'" if options[:grep]
94
+
95
+ puts "📋 Fetching logs from #{env_key.upcase}..."
96
+ exec_in_dir(backend_config["path"], env_vars, cmd)
97
+ end
98
+
99
+ desc "console", "Access remote Rails console"
100
+ map ["c", "rails_console"] => :console
101
+ def console
102
+ config = KamalEasy::Config.load
103
+ env_key, env_vars = load_environment(config)
104
+
105
+ backend_config = config.components["backend"]
106
+ abort "❌ Backend component not configured" unless backend_config
107
+
108
+ container_name = backend_config["container_name_pattern"]
109
+
110
+ cmd = "#{backend_config['kamal_cmd']} server exec -i 'docker exec -it $(docker ps -q -f name=#{container_name} | head -n1) bin/rails console'"
111
+
112
+ puts "🔌 Connecting to #{env_key.upcase} Rails Console..."
113
+ exec_in_dir(backend_config["path"], env_vars, cmd)
114
+ end
115
+
116
+ desc "prune", "Prune old images and containers"
117
+ method_option :retain, aliases: "-r", type: :numeric, default: 3, desc: "Number of versions to keep"
118
+ method_option :hard, type: :boolean, default: false, desc: "Run aggressive docker system prune"
119
+ def prune
120
+ config = KamalEasy::Config.load
121
+ env_key, env_vars = load_environment(config)
122
+ backend_config = config.components["backend"]
123
+
124
+ puts "🧹 Pruning old versions (keeping last #{options[:retain]})..."
125
+
126
+ # Kamal native prune (removes old containers/images managed by Kamal)
127
+ cmd = "#{backend_config['kamal_cmd']} prune --retain #{options[:retain]}"
128
+ exec_in_dir(backend_config["path"], env_vars, cmd)
129
+
130
+ if options[:hard]
131
+ puts "🧹 Running hard Docker cleanup..."
132
+ exec_in_dir(backend_config["path"], env_vars, "#{backend_config['kamal_cmd']} server exec 'docker system prune -a -f --volumes'")
133
+ end
134
+ end
135
+
136
+ private
137
+
138
+ def load_environment(config)
139
+ is_prod = options[:prod] || options[:production]
140
+ is_uat = options[:uat]
141
+ is_staging = options[:staging]
142
+
143
+ if [is_prod, is_uat, is_staging].count(true) > 1
144
+ abort "❌ Error: Cannot target multiple environments simultaneously."
145
+ end
146
+
147
+ unless is_prod || is_uat || is_staging
148
+ puts "ℹ️ No environment flag provided. Using current process environment..."
149
+ return [nil, {}]
150
+ end
151
+
152
+ env_key = if is_prod
153
+ "production"
154
+ elsif is_staging
155
+ "staging"
156
+ else
157
+ "uat"
158
+ end
159
+
160
+ env_config = config.env_config(env_key)
161
+ env_file = env_config["env_file"]
162
+
163
+ unless File.exist?(env_file)
164
+ abort "❌ Error: Environment file #{env_file} not found."
165
+ end
166
+
167
+ puts "📖 Loading configuration from #{env_file}..."
168
+ [env_key, Dotenv.parse(env_file)]
169
+ end
170
+
171
+ def deploy_backend(config, env_vars)
172
+ puts "🚀 Deploying Backend..."
173
+ comp = config.components["backend"]
174
+ validate_mandatory_files(comp, comp["path"])
175
+ exec_in_dir(comp["path"], env_vars, "#{comp['kamal_cmd']} deploy")
176
+ end
177
+
178
+ def deploy_frontend(config, env_vars)
179
+ puts "🚀 Deploying Frontend..."
180
+ comp = config.components["frontend"]
181
+ validate_mandatory_files(comp, comp["path"])
182
+ exec_in_dir(comp["path"], env_vars, "#{comp['kamal_cmd']} deploy")
183
+ end
184
+
185
+ def restart_db(config, env_vars)
186
+ puts "🔄 Restarting Database..."
187
+ comp = config.components["backend"]
188
+ exec_in_dir(comp["path"], env_vars, "#{comp['kamal_cmd']} accessory reboot db")
189
+ end
190
+
191
+ def validate_mandatory_files(component_config, path)
192
+ return unless component_config["mandatory_files"]
193
+
194
+ Dir.chdir(path) do
195
+ component_config["mandatory_files"].each do |file|
196
+ unless File.exist?(file)
197
+ abort "❌ Error: Mandatory file '#{file}' missing in #{path}"
198
+ end
199
+ end
200
+ end
201
+ end
202
+
203
+ def exec_in_dir(dir, env_vars, cmd)
204
+ Dir.chdir(dir) do
205
+ unless system(env_vars, cmd)
206
+ abort "❌ Command failed: #{cmd}"
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,33 @@
1
+ require "yaml"
2
+ require "thor"
3
+
4
+ module KamalEasy
5
+ class Config
6
+ CONFIG_FILE = "config/kamal-easy.yml"
7
+
8
+ def self.load
9
+ unless File.exist?(CONFIG_FILE)
10
+ abort "❌ Error: Configuration file #{CONFIG_FILE} not found. Run `kamal-easy install`."
11
+ end
12
+ new(YAML.load_file(CONFIG_FILE))
13
+ end
14
+
15
+ def initialize(data)
16
+ @data = data
17
+ end
18
+
19
+ def environments
20
+ @data["environments"] || {}
21
+ end
22
+
23
+ def components
24
+ @data["components"] || {}
25
+ end
26
+
27
+ def env_config(env_name)
28
+ config = environments[env_name.to_s]
29
+ abort "❌ Error: Environment '#{env_name}' not defined in #{CONFIG_FILE}" unless config
30
+ config
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,3 @@
1
+ module KamalEasy
2
+ VERSION = "0.0.1"
3
+ end
data/lib/kamal_easy.rb ADDED
@@ -0,0 +1,6 @@
1
+ require "kamal_easy/version"
2
+
3
+ module KamalEasy
4
+ class Error < StandardError; end
5
+ # Your code goes here...
6
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kamal-easy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Ruslan Vikhor
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-02-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: thor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: dotenv
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '2.8'
34
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: '4.0'
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '2.8'
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: '4.0'
47
+ description: Simplifies Kamal deployments with multi-environment support (UAT/Prod)
48
+ and unified commands for logs and console.
49
+ email:
50
+ - ruslan@overnet.com
51
+ executables:
52
+ - kamal-easy
53
+ extensions: []
54
+ extra_rdoc_files: []
55
+ files:
56
+ - Gemfile
57
+ - README.md
58
+ - bin/kamal-easy
59
+ - kamal-easy.gemspec
60
+ - lib/kamal_easy.rb
61
+ - lib/kamal_easy/cli.rb
62
+ - lib/kamal_easy/config.rb
63
+ - lib/kamal_easy/version.rb
64
+ homepage: https://github.com/overnet/kamal-easy
65
+ licenses:
66
+ - MIT
67
+ metadata: {}
68
+ post_install_message:
69
+ rdoc_options: []
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: 3.3.0
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ requirements: []
83
+ rubygems_version: 3.5.3
84
+ signing_key:
85
+ specification_version: 4
86
+ summary: Unified deployment wrapper for Kamal
87
+ test_files: []