bard-new 0.1.0

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.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +42 -0
  3. data/.gitignore +4 -0
  4. data/CLAUDE.md +55 -0
  5. data/Gemfile +10 -0
  6. data/Gemfile.lock +179 -0
  7. data/README.md +107 -0
  8. data/Rakefile +8 -0
  9. data/bard-new.gemspec +25 -0
  10. data/features/new.feature +12 -0
  11. data/features/provision.feature +10 -0
  12. data/features/step_definitions/bard_new_steps.rb +64 -0
  13. data/features/support/bard-coverage +16 -0
  14. data/features/support/env.rb +22 -0
  15. data/features/support/new_server.rb +136 -0
  16. data/features/support/provision_server.rb +282 -0
  17. data/lib/bard/new/cli/new.rb +102 -0
  18. data/lib/bard/new/cli/provision.rb +32 -0
  19. data/lib/bard/new/provision/app.rb +9 -0
  20. data/lib/bard/new/provision/apt.rb +15 -0
  21. data/lib/bard/new/provision/authorizedkeys.rb +23 -0
  22. data/lib/bard/new/provision/base.rb +17 -0
  23. data/lib/bard/new/provision/data.rb +23 -0
  24. data/lib/bard/new/provision/deploy.rb +9 -0
  25. data/lib/bard/new/provision/http.rb +15 -0
  26. data/lib/bard/new/provision/logrotation.rb +27 -0
  27. data/lib/bard/new/provision/masterkey.rb +17 -0
  28. data/lib/bard/new/provision/mysql.rb +21 -0
  29. data/lib/bard/new/provision/nginx.rb +31 -0
  30. data/lib/bard/new/provision/repo.rb +71 -0
  31. data/lib/bard/new/provision/rvm.rb +20 -0
  32. data/lib/bard/new/provision/ssh.rb +79 -0
  33. data/lib/bard/new/provision/swapfile.rb +21 -0
  34. data/lib/bard/new/provision/user.rb +43 -0
  35. data/lib/bard/new/rails_template.rb +213 -0
  36. data/lib/bard/new/version.rb +5 -0
  37. data/lib/bard/plugins/new.rb +2 -0
  38. data/spec/acceptance/docker/Dockerfile.new +68 -0
  39. data/spec/acceptance/docker/Dockerfile.provision +41 -0
  40. data/spec/acceptance/docker/entrypoint-new.sh +3 -0
  41. data/spec/acceptance/docker/test_key +27 -0
  42. data/spec/acceptance/docker/test_key.pub +1 -0
  43. data/spec/bard/new/cli/new_spec.rb +85 -0
  44. data/spec/bard/new/cli/provision_spec.rb +40 -0
  45. data/spec/bard/new/provision/app_spec.rb +33 -0
  46. data/spec/bard/new/provision/apt_spec.rb +39 -0
  47. data/spec/bard/new/provision/authorizedkeys_spec.rb +40 -0
  48. data/spec/bard/new/provision/base_spec.rb +34 -0
  49. data/spec/bard/new/provision/data_spec.rb +54 -0
  50. data/spec/bard/new/provision/deploy_spec.rb +33 -0
  51. data/spec/bard/new/provision/http_spec.rb +57 -0
  52. data/spec/bard/new/provision/logrotation_spec.rb +34 -0
  53. data/spec/bard/new/provision/masterkey_spec.rb +62 -0
  54. data/spec/bard/new/provision/mysql_spec.rb +55 -0
  55. data/spec/bard/new/provision/nginx_spec.rb +81 -0
  56. data/spec/bard/new/provision/repo_spec.rb +208 -0
  57. data/spec/bard/new/provision/rvm_spec.rb +49 -0
  58. data/spec/bard/new/provision/ssh_spec.rb +242 -0
  59. data/spec/bard/new/provision/swapfile_spec.rb +33 -0
  60. data/spec/bard/new/provision/user_spec.rb +103 -0
  61. data/spec/spec_helper.rb +19 -0
  62. metadata +214 -0
@@ -0,0 +1,136 @@
1
+ require "fileutils"
2
+ require "open3"
3
+ require "docker-api"
4
+
5
+ module NewServerWorld
6
+ class << self
7
+ attr_accessor :server_available, :image_built
8
+ end
9
+
10
+ class PrerequisiteError < StandardError; end
11
+
12
+ def ensure_new_server_available
13
+ return if NewServerWorld.server_available
14
+
15
+ unless system("command -v podman >/dev/null 2>&1")
16
+ raise PrerequisiteError, "podman is not installed"
17
+ end
18
+
19
+ configure_new_container_socket
20
+ build_new_test_image
21
+ FileUtils.chmod(0o600, new_ssh_key_path)
22
+
23
+ NewServerWorld.server_available = true
24
+ end
25
+
26
+ def configure_new_container_socket
27
+ if ENV["DOCKER_HOST"]
28
+ Docker.url = ENV["DOCKER_HOST"]
29
+ return
30
+ end
31
+
32
+ socket_path = "/run/user/#{Process.uid}/podman/podman.sock"
33
+ unless File.exist?(socket_path)
34
+ system("systemctl --user start podman.socket 2>/dev/null")
35
+ sleep 2
36
+ end
37
+
38
+ unless File.exist?(socket_path)
39
+ raise PrerequisiteError, "Podman socket not available"
40
+ end
41
+
42
+ ENV["DOCKER_HOST"] = "unix://#{socket_path}"
43
+ Docker.url = ENV["DOCKER_HOST"]
44
+ end
45
+
46
+ def build_new_test_image
47
+ return if NewServerWorld.image_built
48
+
49
+ if new_image_exists?("bard-test-new")
50
+ NewServerWorld.image_built = true
51
+ return
52
+ end
53
+
54
+ dockerfile = File.join(ROOT, "spec/acceptance/docker/Dockerfile.new")
55
+ unless system("podman build -t bard-test-new -f #{dockerfile} #{ROOT} 2>&1")
56
+ raise PrerequisiteError, "Failed to build bard-test-new image"
57
+ end
58
+
59
+ NewServerWorld.image_built = true
60
+ end
61
+
62
+ def new_image_exists?(name)
63
+ Docker::Image.get(name)
64
+ true
65
+ rescue Docker::Error::NotFoundError
66
+ false
67
+ end
68
+
69
+ def start_new_server
70
+ ensure_new_server_available
71
+
72
+ @new_container = Docker::Container.create(
73
+ "Image" => "localhost/bard-test-new:latest",
74
+ "ExposedPorts" => { "22/tcp" => {} },
75
+ "HostConfig" => {
76
+ "PortBindings" => { "22/tcp" => [{ "HostPort" => "" }] },
77
+ "PublishAllPorts" => true
78
+ }
79
+ )
80
+ @new_container.start
81
+ @new_container.refresh!
82
+
83
+ @new_ssh_port = @new_container.info["NetworkSettings"]["Ports"]["22/tcp"].first["HostPort"].to_i
84
+
85
+ wait_for_new_ssh
86
+ end
87
+
88
+ def wait_for_new_ssh
89
+ 30.times do
90
+ return if system(
91
+ "ssh", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null",
92
+ "-o", "ConnectTimeout=1", "-p", @new_ssh_port.to_s, "-i", new_ssh_key_path,
93
+ "deploy@localhost", "true",
94
+ out: File::NULL, err: File::NULL
95
+ )
96
+ sleep 0.5
97
+ end
98
+ raise PrerequisiteError, "SSH not ready"
99
+ end
100
+
101
+ def run_new_ssh(command)
102
+ escaped = command.gsub("'", "'\"'\"'")
103
+ Open3.capture2e(
104
+ "ssh", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null",
105
+ "-p", @new_ssh_port.to_s, "-i", new_ssh_key_path,
106
+ "deploy@localhost", "bash -lc '#{escaped}'"
107
+ )
108
+ end
109
+
110
+ def run_bard_remote(command)
111
+ @stdout, @status = run_new_ssh("mkdir -p /tmp/bardwork/current && cd /tmp/bardwork/current && bard #{command}")
112
+ end
113
+
114
+ def new_ssh_key_path
115
+ File.join(ROOT, "spec/acceptance/docker/test_key")
116
+ end
117
+
118
+ def stop_new_server
119
+ return unless @new_container
120
+ @new_container.stop rescue nil
121
+ @new_container.delete(force: true) rescue nil
122
+ ensure
123
+ @new_container = nil
124
+ @new_ssh_port = nil
125
+ end
126
+ end
127
+
128
+ World(NewServerWorld)
129
+
130
+ Before("@new") do
131
+ start_new_server
132
+ end
133
+
134
+ After("@new") do
135
+ stop_new_server
136
+ end
@@ -0,0 +1,282 @@
1
+ require "fileutils"
2
+ require "open3"
3
+ require "tmpdir"
4
+ require "docker-api"
5
+
6
+ module ProvisionServerWorld
7
+ class << self
8
+ attr_accessor :server_available, :image_built
9
+ end
10
+
11
+ class PrerequisiteError < StandardError; end
12
+
13
+ def ensure_provision_server_available
14
+ return if ProvisionServerWorld.server_available
15
+
16
+ unless system("command -v podman >/dev/null 2>&1")
17
+ raise PrerequisiteError, "podman is not installed"
18
+ end
19
+
20
+ configure_provision_socket
21
+ build_provision_image
22
+ FileUtils.chmod(0o600, provision_ssh_key_path)
23
+
24
+ ProvisionServerWorld.server_available = true
25
+ end
26
+
27
+ def configure_provision_socket
28
+ if ENV["DOCKER_HOST"]
29
+ Docker.url = ENV["DOCKER_HOST"]
30
+ return
31
+ end
32
+
33
+ socket_path = "/run/user/#{Process.uid}/podman/podman.sock"
34
+ unless File.exist?(socket_path)
35
+ system("systemctl --user start podman.socket 2>/dev/null")
36
+ sleep 2
37
+ end
38
+
39
+ unless File.exist?(socket_path)
40
+ raise PrerequisiteError, "Podman socket not available"
41
+ end
42
+
43
+ ENV["DOCKER_HOST"] = "unix://#{socket_path}"
44
+ Docker.url = ENV["DOCKER_HOST"]
45
+ end
46
+
47
+ def build_provision_image
48
+ return if ProvisionServerWorld.image_built
49
+
50
+ if provision_image_exists?("bard-test-provision")
51
+ ProvisionServerWorld.image_built = true
52
+ return
53
+ end
54
+
55
+ docker_dir = File.join(ROOT, "spec/acceptance/docker")
56
+ unless system("podman build -t bard-test-provision -f #{docker_dir}/Dockerfile.provision #{docker_dir} 2>&1")
57
+ raise PrerequisiteError, "Failed to build provision test image"
58
+ end
59
+
60
+ ProvisionServerWorld.image_built = true
61
+ end
62
+
63
+ def provision_image_exists?(name)
64
+ Docker::Image.get(name)
65
+ true
66
+ rescue Docker::Error::NotFoundError
67
+ false
68
+ end
69
+
70
+ def start_provision_server
71
+ ensure_provision_server_available
72
+
73
+ @container = Docker::Container.create(
74
+ "Image" => "localhost/bard-test-provision:latest",
75
+ "ExposedPorts" => { "22/tcp" => {}, "80/tcp" => {} },
76
+ "HostConfig" => {
77
+ "PortBindings" => {
78
+ "22/tcp" => [{ "HostPort" => "" }],
79
+ "80/tcp" => [{ "HostPort" => "" }],
80
+ },
81
+ "PublishAllPorts" => true,
82
+ "Privileged" => true,
83
+ }
84
+ )
85
+ @container.start
86
+ @container.refresh!
87
+
88
+ @ssh_port = @container.info["NetworkSettings"]["Ports"]["22/tcp"].first["HostPort"].to_i
89
+ @http_port = @container.info["NetworkSettings"]["Ports"]["80/tcp"].first["HostPort"].to_i
90
+ @container_ip = "127.0.0.1"
91
+
92
+ wait_for_systemd
93
+ setup_local_test_directory
94
+ end
95
+
96
+ def wait_for_systemd
97
+ last_output = ""
98
+ 60.times do
99
+ stdout, status = Open3.capture2e(
100
+ "ssh", "-4", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null",
101
+ "-o", "ConnectTimeout=2", "-p", @ssh_port.to_s, "-i", provision_ssh_key_path,
102
+ "root@#{@container_ip}", "systemctl is-system-running 2>/dev/null || true"
103
+ )
104
+ last_output = stdout.strip.split("\n").last.to_s
105
+ if last_output =~ /running|degraded/
106
+ # Remove nologin so non-root users can SSH (systemd-user-sessions may not run in container)
107
+ Open3.capture2e(
108
+ "ssh", "-4", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null",
109
+ "-p", @ssh_port.to_s, "-i", provision_ssh_key_path,
110
+ "root@#{@container_ip}", "rm -f /run/nologin"
111
+ )
112
+ return
113
+ end
114
+ sleep 1
115
+ end
116
+ raise PrerequisiteError, "systemd not ready after 60s, last output: #{last_output}"
117
+ end
118
+
119
+ def setup_local_test_directory
120
+ @test_parent = Dir.mktmpdir("bard_provision")
121
+ @test_dir = File.join(@test_parent, "testproject")
122
+ FileUtils.mkdir_p(@test_dir)
123
+
124
+ Dir.chdir(@test_dir) do
125
+ system("git init", out: File::NULL, err: File::NULL)
126
+ system("git config user.email 'test@example.com'", out: File::NULL, err: File::NULL)
127
+ system("git config user.name 'Test User'", out: File::NULL, err: File::NULL)
128
+
129
+ File.write("bard.rb", <<~RUBY)
130
+ target :production do
131
+ ssh "www@#{@container_ip}:#{@ssh_port}",
132
+ path: "testproject",
133
+ ssh_key: "#{provision_ssh_key_path}"
134
+ url "http://testproject.localhost"
135
+ ping false
136
+ end
137
+ RUBY
138
+
139
+ FileUtils.mkdir_p("config")
140
+ File.write("config/master.key", "fake_master_key_for_testing")
141
+
142
+ File.write(".ruby-version", "ruby-3.3.4")
143
+
144
+ system("git add -A && git commit -m 'initial'", out: File::NULL, err: File::NULL)
145
+ end
146
+ end
147
+
148
+ def setup_test_project
149
+ # Create test project
150
+ run_provision_ssh_as("www", <<~'SH')
151
+ mkdir -p ~/testproject/bin ~/testproject/db ~/testproject/public ~/testproject/config ~/testproject/log
152
+ SH
153
+
154
+ run_provision_ssh_as("www", <<~SH)
155
+ cat > ~/testproject/Gemfile << 'GEMFILE'
156
+ source "https://rubygems.org"
157
+ gem "bard", github: "botandrose/bard", branch: "v2.0"
158
+ gem "foreman-export-systemd_user"
159
+ GEMFILE
160
+ SH
161
+
162
+ run_provision_ssh_as("www", <<~SH)
163
+ cat > ~/testproject/Procfile << 'PROCFILE'
164
+ web: python3 -m http.server 3000 -d public
165
+ PROCFILE
166
+ SH
167
+
168
+ run_provision_ssh_as("www", <<~SH)
169
+ cat > ~/testproject/bin/setup << 'SCRIPT'
170
+ #!/bin/bash
171
+ bundle install --quiet
172
+ bin/rake bootstrap
173
+ SCRIPT
174
+ chmod +x ~/testproject/bin/setup
175
+ SH
176
+
177
+ run_provision_ssh_as("www", <<~SH)
178
+ cat > ~/testproject/bin/rake << 'SCRIPT'
179
+ #!/bin/bash
180
+ case "$1" in
181
+ bootstrap)
182
+ if [ "$RAILS_ENV" = "production" ]; then
183
+ app=$(basename $(pwd))
184
+ bundle exec foreman export systemd-user --app $app
185
+ systemctl --user restart $app.target
186
+ fi
187
+ ;;
188
+ db:dump)
189
+ echo "test data" | gzip > db/data.sql.gz
190
+ ;;
191
+ db:load)
192
+ gunzip -c db/data.sql.gz > /dev/null
193
+ echo "Data loaded"
194
+ ;;
195
+ esac
196
+ SCRIPT
197
+ chmod +x ~/testproject/bin/rake
198
+ SH
199
+
200
+ run_provision_ssh_as("www", <<~SH)
201
+ cat > ~/testproject/bard.rb << 'BARDCONFIG'
202
+ target :production do
203
+ ssh "www@#{@container_ip}:#{@ssh_port}",
204
+ path: "testproject"
205
+ url "http://testproject.localhost"
206
+ ping false
207
+ end
208
+ BARDCONFIG
209
+ SH
210
+
211
+ run_provision_ssh_as("www", "echo 'ruby-3.3.4' > ~/testproject/.ruby-version")
212
+
213
+ run_provision_ssh_as("www", "echo 'hello from testproject' > ~/testproject/public/index.html")
214
+
215
+ # Initialize git repo
216
+ run_provision_ssh_as("www", <<~SH)
217
+ cd ~/testproject && \
218
+ git config --global user.email "test@example.com" && \
219
+ git config --global user.name "Test User" && \
220
+ git config --global init.defaultBranch master && \
221
+ git init && git add -A && git commit -m "Initial commit"
222
+ SH
223
+
224
+ # Set up a bare remote so Repo step's on_latest_master? works (fetch origin)
225
+ run_provision_ssh_as("www", <<~SH)
226
+ git clone --bare ~/testproject ~/repos/testproject.git && \
227
+ cd ~/testproject && git remote add origin ~/repos/testproject.git
228
+ SH
229
+ end
230
+
231
+ def run_provision_phase1
232
+ Dir.chdir(@test_dir) do
233
+ bard_coverage = File.join(ROOT, "features/support/bard-coverage")
234
+ @stdout, @status = Open3.capture2e("#{bard_coverage} provision root@#{@container_ip}:#{@ssh_port} --steps=SSH User AuthorizedKeys Apt")
235
+ end
236
+ end
237
+
238
+ def run_provision_phase2
239
+ Dir.chdir(@test_dir) do
240
+ bard_coverage = File.join(ROOT, "features/support/bard-coverage")
241
+ @stdout, @status = Open3.capture2e("#{bard_coverage} provision www@#{@container_ip}:#{@ssh_port} --steps=Repo MasterKey RVM App Nginx Deploy HTTP LogRotation")
242
+ end
243
+ end
244
+
245
+ def run_provision_ssh_as(user, command)
246
+ stdout, status = Open3.capture2e(
247
+ "ssh", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null",
248
+ "-p", @ssh_port.to_s, "-i", provision_ssh_key_path,
249
+ "#{user}@#{@container_ip}", command
250
+ )
251
+ unless status.success?
252
+ raise PrerequisiteError, "SSH command failed (#{user}): #{command}\nOutput: #{stdout}"
253
+ end
254
+ stdout
255
+ end
256
+
257
+ def provision_ssh_key_path
258
+ File.join(ROOT, "spec/acceptance/docker/test_key")
259
+ end
260
+
261
+ def stop_provision_server
262
+ return unless @container
263
+ @container.stop rescue nil
264
+ @container.delete(force: true) rescue nil
265
+ ensure
266
+ @container = nil
267
+ @ssh_port = nil
268
+ FileUtils.rm_rf(@test_parent) if @test_parent
269
+ @test_dir = nil
270
+ @test_parent = nil
271
+ end
272
+ end
273
+
274
+ World(ProvisionServerWorld)
275
+
276
+ Before("@provision") do
277
+ start_provision_server
278
+ end
279
+
280
+ After("@provision") do
281
+ stop_provision_server
282
+ end
@@ -0,0 +1,102 @@
1
+ require "bard/plugins/github"
2
+
3
+ class Bard::CLI
4
+ NEW_RAILS_REQUIREMENT = "~> 8.1.0"
5
+
6
+ desc "new <project-name>", "creates new bard app named <project-name>"
7
+ method_option :skip_github, type: :boolean, default: false
8
+ method_option :skip_stage, type: :boolean, default: false
9
+ def new(project_name)
10
+ @new_project_name = project_name
11
+ new_validate
12
+ new_create_project
13
+ new_push_to_github unless options[:skip_github]
14
+ new_stage unless options[:skip_stage]
15
+ puts green("Project #{@new_project_name} created!")
16
+ puts "Please cd ../#{@new_project_name}"
17
+ end
18
+
19
+ no_commands do
20
+ def new_validate
21
+ if @new_project_name !~ /^[a-z][a-z0-9]*\Z/
22
+ puts red("!!! ") + "Invalid project name: #{yellow(@new_project_name)}."
23
+ puts "The first character must be a lowercase letter, and all following characters must be a lowercase letter or number."
24
+ exit 1
25
+ end
26
+ end
27
+
28
+ def new_create_project
29
+ run! new_build_create_project_script
30
+ end
31
+
32
+ def new_build_create_project_script
33
+ new_build_bash_env do
34
+ new_build_rvm_setup +
35
+ new_build_gem_install("rails", NEW_RAILS_REQUIREMENT) +
36
+ new_build_rails_new
37
+ end
38
+ end
39
+
40
+ def new_build_bash_env
41
+ script = yield
42
+ <<~SH
43
+ env -i bash -lc '
44
+ export HOME=~
45
+ source ~/.rvm/scripts/rvm
46
+ #{script}
47
+ '
48
+ SH
49
+ end
50
+
51
+ def new_build_rvm_setup
52
+ <<~SH
53
+ cd ..
54
+ rvm use --create #{new_ruby_version}@#{@new_project_name}
55
+ SH
56
+ end
57
+
58
+ def new_build_gem_install(gem_name, version_requirement)
59
+ <<~SH
60
+ gem install #{gem_name} -v "#{version_requirement}" --no-document || exit 1
61
+ GEM_VERSION=$(gem list #{gem_name} --exact -v "#{version_requirement}" | grep -oP "#{gem_name} \\(\\K[0-9.]+" | head -1)
62
+ SH
63
+ end
64
+
65
+ def new_build_rails_new
66
+ <<~SH
67
+ rails _${GEM_VERSION}_ new #{@new_project_name} --skip-git --skip-kamal --skip-test -m #{new_template_path}
68
+ SH
69
+ end
70
+
71
+ def new_push_to_github
72
+ api = Bard::Github.new(@new_project_name)
73
+ api.create_repo
74
+ run! <<~SH
75
+ cd ../#{@new_project_name}
76
+ git init -b master
77
+ git add -A
78
+ git commit -m"initial commit."
79
+ git remote add origin git@github.com:botandrosedesign/#{@new_project_name}
80
+ git push -u origin master
81
+ SH
82
+ api.add_master_key File.read("../#{@new_project_name}/config/master.key")
83
+ api.add_master_branch_protection
84
+ api.patch(nil, allow_auto_merge: true)
85
+ end
86
+
87
+ def new_stage
88
+ run! <<~SH
89
+ cd ../#{@new_project_name}
90
+ bard deploy --clone
91
+ SH
92
+ end
93
+
94
+ def new_ruby_version
95
+ "ruby-4.0.2"
96
+ end
97
+
98
+ def new_template_path
99
+ File.expand_path("../rails_template.rb", __dir__)
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,32 @@
1
+ require "bard/new/provision/base"
2
+ require "bard/plugins/ssh"
3
+
4
+ class Bard::CLI
5
+ PROVISION_STEPS = %w[
6
+ SSH
7
+ User
8
+ AuthorizedKeys
9
+ Swapfile
10
+ Apt
11
+ MySQL
12
+ Repo
13
+ MasterKey
14
+ RVM
15
+ App
16
+ Nginx
17
+ Deploy
18
+ HTTP
19
+ LogRotation
20
+ Data
21
+ ]
22
+
23
+ desc "provision [ssh_url] --steps=all", "takes an optional ssh url to a raw ubuntu 24.04 install, and readies it in the shape of :production"
24
+ option :steps, type: :array, default: PROVISION_STEPS
25
+ def provision(ssh_url = config[:production].ssh&.to_s)
26
+ ssh_url = ssh_url.dup
27
+ options[:steps].each do |step|
28
+ require "bard/new/provision/#{step.downcase}"
29
+ Bard::Provision.const_get(step).call(config, ssh_url)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,9 @@
1
+ # run bin/setup
2
+
3
+ class Bard::Provision::App < Bard::Provision
4
+ def call
5
+ print "App:"
6
+ provision_server.run! "bin/setup"
7
+ puts " ✓"
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ # apt sanity
2
+
3
+ class Bard::Provision::Apt < Bard::Provision
4
+ def call
5
+ print "Apt:"
6
+ provision_server.run! [
7
+ %(echo "\\$nrconf{restart} = \\"a\\";" | sudo tee /etc/needrestart/conf.d/90-autorestart.conf),
8
+ "sudo apt-get update -y",
9
+ "sudo apt-get upgrade -y",
10
+ "sudo apt-get install -y curl build-essential libsodium-dev",
11
+ ].join("; "), home: true
12
+
13
+ puts " ✓"
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ # install public keys if missing
2
+
3
+ class Bard::Provision::AuthorizedKeys < Bard::Provision
4
+ def call
5
+ print "Authorized Keys:"
6
+
7
+ KEYS.each do |search_text, full_key|
8
+ file = "~/.ssh/authorized_keys"
9
+ provision_server.run! <<~SH, home: true
10
+ if ! grep -F -q "#{search_text}" #{file}; then
11
+ echo "#{full_key}" >> #{file}
12
+ fi
13
+ SH
14
+ end
15
+
16
+ puts " ✓"
17
+ end
18
+
19
+ KEYS = {
20
+ "micah@haku" => "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDAH235mtpxPQucd0bIgdufo1bR3By2+a+NPHiZS1P7SpI73evN9+hY7ri+gLscPLRWeoy1ig/TbyfN1AqJmfqIaskZdYOdcEQdOum4AwDMY5L6OAq2o5NER047RqDxE6Pjm2nfRVVw2Dz38eeco+ouchCI+sY5pJL/wEZItrCpPjKvwo56uln1rL6Smd4Kh98ZBKTGL8xKs95rNmFdBCCq4eUE28JDgkJAiLDZ/4u2LNrgEr7/brkUieZjaZ4BacBi8EQvyvMWmZ0g2MoG+Ptxn/3K2nd1QqdhfINqHBVCi8UbkP08B0Msif/7Dycuxd7DU9cVZ3RgnhLtbIsQ8HaYVj5yCKB6CuX3lv3H4YKBghBC/TnJD5Nq5xcSYTW0BKKrusCb/OoOk5AUV+BGM1+R70fno8reVEBUlZDkWapHxmqgNnf1byL7Aol/L5SWgyfSLT6b5FjI6g/U+dhaecYY9T9g/GWo+JiwZktc094O0ujlQHoibMY2M0csVfvO9Oc= micah@haku",
21
+ "gubs@Theia.local" => "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEApJh0E8ZlaLbMUWGvryAhEBRnnI519ZKz586vdQTuIPlDb9xhe5m3Ys8Fk9LKqJUQNxBV6qCGOXNgNdWySkk2ChmmgDnPfr7/31ZuOAASFbUY0PtaDXUsMVvs1Uu2VhtRU9gSduGonEHG7iBpAuBI23CxU4yPS6o3pv7L9xwnmULes5F9S4/nDvPig15h9byInyHOLDV0XjHFS+2OlSWO/xC8uqH5CdlxXFAmPQ0R69qmILl0rcTPyNMLJGcJGUzb/LMRJX/RDyTpZeJPjH4V+zksQ/4YQ3LWvLrlZL6QLuM285ve4mQa3vBY4WMqNlp4Ig3ZCFOpMKmpvTn7pFUmKw== gubs@Theia.local",
22
+ }
23
+ end
@@ -0,0 +1,17 @@
1
+ module Bard
2
+ class Provision < Struct.new(:config, :ssh_url)
3
+ def self.call(...) = new(...).call
4
+
5
+ private
6
+
7
+ def target
8
+ config[:production]
9
+ end
10
+
11
+ def provision_server
12
+ options = {}
13
+ options[:ssh_key] = target.ssh.ssh_key if target.ssh&.ssh_key
14
+ target.dup.tap { |t| t.ssh(ssh_url, **options) }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,23 @@
1
+ # copy data from production
2
+
3
+ class Bard::Provision::Data < Bard::Provision
4
+ def call
5
+ print "Data:"
6
+
7
+ print " Dumping #{target.key} database to file"
8
+ target.run! "bin/rake db:dump"
9
+
10
+ print " Transfering file from #{target.key},"
11
+ target.copy_file "db/data.sql.gz", to: provision_server, verbose: false
12
+
13
+ print " Loading file into database,"
14
+ provision_server.run! "bin/rake db:load"
15
+
16
+ config.data.each do |path|
17
+ print " Synchronizing files in #{path},"
18
+ target.copy_dir path, to: provision_server, verbose: false
19
+ end
20
+
21
+ puts " ✓"
22
+ end
23
+ end
@@ -0,0 +1,9 @@
1
+ # run bard deploy
2
+
3
+ class Bard::Provision::Deploy < Bard::Provision
4
+ def call
5
+ print "Deploy:"
6
+ provision_server.run! "bin/setup"
7
+ puts " ✓"
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ require "uri"
2
+
3
+ # test for existence
4
+
5
+ class Bard::Provision::HTTP < Bard::Provision
6
+ def call
7
+ print "HTTP:"
8
+ target_host = URI.parse(target.url).host
9
+ if system "curl -sf --resolve #{target_host}:80:#{provision_server.ssh_uri.host} http://#{target_host} -o /dev/null"
10
+ puts " ✓"
11
+ else
12
+ puts " !!! not serving a rails app from #{provision_server.ssh_uri.host}"
13
+ end
14
+ end
15
+ end