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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +42 -0
- data/.gitignore +4 -0
- data/CLAUDE.md +55 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +179 -0
- data/README.md +107 -0
- data/Rakefile +8 -0
- data/bard-new.gemspec +25 -0
- data/features/new.feature +12 -0
- data/features/provision.feature +10 -0
- data/features/step_definitions/bard_new_steps.rb +64 -0
- data/features/support/bard-coverage +16 -0
- data/features/support/env.rb +22 -0
- data/features/support/new_server.rb +136 -0
- data/features/support/provision_server.rb +282 -0
- data/lib/bard/new/cli/new.rb +102 -0
- data/lib/bard/new/cli/provision.rb +32 -0
- data/lib/bard/new/provision/app.rb +9 -0
- data/lib/bard/new/provision/apt.rb +15 -0
- data/lib/bard/new/provision/authorizedkeys.rb +23 -0
- data/lib/bard/new/provision/base.rb +17 -0
- data/lib/bard/new/provision/data.rb +23 -0
- data/lib/bard/new/provision/deploy.rb +9 -0
- data/lib/bard/new/provision/http.rb +15 -0
- data/lib/bard/new/provision/logrotation.rb +27 -0
- data/lib/bard/new/provision/masterkey.rb +17 -0
- data/lib/bard/new/provision/mysql.rb +21 -0
- data/lib/bard/new/provision/nginx.rb +31 -0
- data/lib/bard/new/provision/repo.rb +71 -0
- data/lib/bard/new/provision/rvm.rb +20 -0
- data/lib/bard/new/provision/ssh.rb +79 -0
- data/lib/bard/new/provision/swapfile.rb +21 -0
- data/lib/bard/new/provision/user.rb +43 -0
- data/lib/bard/new/rails_template.rb +213 -0
- data/lib/bard/new/version.rb +5 -0
- data/lib/bard/plugins/new.rb +2 -0
- data/spec/acceptance/docker/Dockerfile.new +68 -0
- data/spec/acceptance/docker/Dockerfile.provision +41 -0
- data/spec/acceptance/docker/entrypoint-new.sh +3 -0
- data/spec/acceptance/docker/test_key +27 -0
- data/spec/acceptance/docker/test_key.pub +1 -0
- data/spec/bard/new/cli/new_spec.rb +85 -0
- data/spec/bard/new/cli/provision_spec.rb +40 -0
- data/spec/bard/new/provision/app_spec.rb +33 -0
- data/spec/bard/new/provision/apt_spec.rb +39 -0
- data/spec/bard/new/provision/authorizedkeys_spec.rb +40 -0
- data/spec/bard/new/provision/base_spec.rb +34 -0
- data/spec/bard/new/provision/data_spec.rb +54 -0
- data/spec/bard/new/provision/deploy_spec.rb +33 -0
- data/spec/bard/new/provision/http_spec.rb +57 -0
- data/spec/bard/new/provision/logrotation_spec.rb +34 -0
- data/spec/bard/new/provision/masterkey_spec.rb +62 -0
- data/spec/bard/new/provision/mysql_spec.rb +55 -0
- data/spec/bard/new/provision/nginx_spec.rb +81 -0
- data/spec/bard/new/provision/repo_spec.rb +208 -0
- data/spec/bard/new/provision/rvm_spec.rb +49 -0
- data/spec/bard/new/provision/ssh_spec.rb +242 -0
- data/spec/bard/new/provision/swapfile_spec.rb +33 -0
- data/spec/bard/new/provision/user_spec.rb +103 -0
- data/spec/spec_helper.rb +19 -0
- 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,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,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
|