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,81 @@
1
+ require "spec_helper"
2
+ require "bard/new/provision/base"
3
+ require "bard/new/provision/nginx"
4
+
5
+ describe Bard::Provision::Nginx do
6
+ let(:target) { double("target", project_name: "test_app") }
7
+ let(:config) { double("config", project_name: "test_app", :[] => target) }
8
+ let(:ssh_url) { "user@example.com" }
9
+ let(:provision_server) { double("provision_server") }
10
+ let(:nginx) { Bard::Provision::Nginx.new(config, ssh_url) }
11
+
12
+ before do
13
+ allow(nginx).to receive(:target).and_return(target)
14
+ allow(nginx).to receive(:provision_server).and_return(provision_server)
15
+ allow(provision_server).to receive_message_chain(:ssh_uri, :host).and_return("192.168.1.100")
16
+ allow(nginx).to receive(:print)
17
+ allow(nginx).to receive(:puts)
18
+ allow(nginx).to receive(:system)
19
+ end
20
+
21
+ describe "#call" do
22
+ context "when HTTP is not responding" do
23
+ it "installs nginx" do
24
+ allow(nginx).to receive(:http_responding?).and_return(false)
25
+ allow(nginx).to receive(:app_configured?).and_return(true)
26
+
27
+ expect(provision_server).to receive(:run!).with(/apt-get install -y nginx/, home: true)
28
+
29
+ nginx.call
30
+ end
31
+ end
32
+
33
+ context "when app is not configured" do
34
+ it "creates nginx config" do
35
+ allow(nginx).to receive(:http_responding?).and_return(true)
36
+ allow(nginx).to receive(:app_configured?).and_return(false)
37
+
38
+ expect(provision_server).to receive(:run!).with("bard setup")
39
+
40
+ nginx.call
41
+ end
42
+ end
43
+
44
+ context "when everything is already set up" do
45
+ it "skips installation and configuration" do
46
+ allow(nginx).to receive(:http_responding?).and_return(true)
47
+ allow(nginx).to receive(:app_configured?).and_return(true)
48
+
49
+ expect(provision_server).not_to receive(:run!)
50
+
51
+ nginx.call
52
+ end
53
+ end
54
+
55
+ it "prints status messages" do
56
+ allow(nginx).to receive(:http_responding?).and_return(true)
57
+ allow(nginx).to receive(:app_configured?).and_return(true)
58
+
59
+ expect(nginx).to receive(:print).with("Nginx:")
60
+ expect(nginx).to receive(:puts).with(" ✓")
61
+
62
+ nginx.call
63
+ end
64
+ end
65
+
66
+ describe "#http_responding?" do
67
+ it "checks if port 80 is responding on the remote" do
68
+ expect(provision_server).to receive(:run).with("nc -zv localhost 80 2>/dev/null", home: true, quiet: true)
69
+
70
+ nginx.http_responding?
71
+ end
72
+ end
73
+
74
+ describe "#app_configured?" do
75
+ it "checks if nginx config exists for the app" do
76
+ expect(provision_server).to receive(:run).with("[ -f /etc/nginx/sites-enabled/test_app ]", quiet: true)
77
+
78
+ nginx.app_configured?
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,208 @@
1
+ require "spec_helper"
2
+ require "bard/new/provision/base"
3
+ require "bard/new/provision/repo"
4
+
5
+ describe Bard::Provision::Repo do
6
+ let(:ssh_uri) { double("ssh_uri", user: "deploy", host: "example.com") }
7
+ let(:target) { double("target", ssh_uri: ssh_uri, project_name: "test_project") }
8
+ let(:config) { double("config", project_name: "test_project", :[] => target) }
9
+ let(:ssh_url) { "deploy@example.com" }
10
+ let(:provision_server) { double("provision_server") }
11
+ let(:github_api) { double("github_api") }
12
+ let(:repo_provisioner) { Bard::Provision::Repo.new(config, ssh_url) }
13
+
14
+ before do
15
+ allow(repo_provisioner).to receive(:target).and_return(target)
16
+ allow(repo_provisioner).to receive(:provision_server).and_return(provision_server)
17
+ allow(repo_provisioner).to receive(:print)
18
+ allow(repo_provisioner).to receive(:puts)
19
+ allow(Bard::Github).to receive(:new).and_return(github_api)
20
+ end
21
+
22
+ describe "#call" do
23
+ context "when repository is already cloned" do
24
+ context "when not on latest master" do
25
+ it "updates to latest master" do
26
+ allow(repo_provisioner).to receive(:already_cloned?).and_return(true)
27
+ allow(repo_provisioner).to receive(:on_latest_master?).and_return(false)
28
+
29
+ expect(repo_provisioner).to receive(:update_to_latest_master!)
30
+ expect(github_api).not_to receive(:add_deploy_key)
31
+
32
+ repo_provisioner.call
33
+ end
34
+
35
+ it "prints status message when updating to latest master" do
36
+ allow(repo_provisioner).to receive(:already_cloned?).and_return(true)
37
+ allow(repo_provisioner).to receive(:on_latest_master?).and_return(false)
38
+ allow(repo_provisioner).to receive(:update_to_latest_master!)
39
+
40
+ expect(repo_provisioner).to receive(:print).with("Repo:")
41
+ expect(repo_provisioner).to receive(:print).with(" Updating to latest master,")
42
+ expect(repo_provisioner).to receive(:puts).with(" ✓")
43
+
44
+ repo_provisioner.call
45
+ end
46
+ end
47
+
48
+ context "when already on latest master" do
49
+ it "skips update" do
50
+ allow(repo_provisioner).to receive(:already_cloned?).and_return(true)
51
+ allow(repo_provisioner).to receive(:on_latest_master?).and_return(true)
52
+
53
+ expect(repo_provisioner).not_to receive(:update_to_latest_master!)
54
+ expect(github_api).not_to receive(:add_deploy_key)
55
+
56
+ repo_provisioner.call
57
+ end
58
+
59
+ it "only prints repo header and checkbox" do
60
+ allow(repo_provisioner).to receive(:already_cloned?).and_return(true)
61
+ allow(repo_provisioner).to receive(:on_latest_master?).and_return(true)
62
+
63
+ expect(repo_provisioner).to receive(:print).with("Repo:")
64
+ expect(repo_provisioner).not_to receive(:print).with(" Updating to latest master,")
65
+ expect(repo_provisioner).to receive(:puts).with(" ✓")
66
+
67
+ repo_provisioner.call
68
+ end
69
+ end
70
+ end
71
+
72
+ context "when repository is not cloned but can be cloned" do
73
+ it "clones the repository directly" do
74
+ allow(repo_provisioner).to receive(:already_cloned?).and_return(false)
75
+ allow(repo_provisioner).to receive(:can_clone_project?).and_return(true)
76
+
77
+ expect(provision_server).to receive(:run!).with("git clone git@github.com:botandrosedesign/test_project", home: true)
78
+ expect(github_api).not_to receive(:add_deploy_key)
79
+
80
+ repo_provisioner.call
81
+ end
82
+ end
83
+
84
+ context "when repository cannot be cloned and SSH keypair exists" do
85
+ it "adds deploy key and clones repository" do
86
+ allow(repo_provisioner).to receive(:already_cloned?).and_return(false)
87
+ allow(repo_provisioner).to receive(:can_clone_project?).and_return(false)
88
+ allow(repo_provisioner).to receive(:ssh_keypair?).and_return(true)
89
+ allow(provision_server).to receive(:run).with("cat ~/.ssh/id_rsa.pub", home: true).and_return("ssh-rsa AAAAB3...")
90
+
91
+ expect(github_api).to receive(:add_deploy_key).with(title: "deploy@example.com", key: "ssh-rsa AAAAB3...")
92
+ expect(provision_server).to receive(:run!).with("git clone git@github.com:botandrosedesign/test_project", home: true)
93
+
94
+ repo_provisioner.call
95
+ end
96
+ end
97
+
98
+ context "when repository cannot be cloned and no SSH keypair exists" do
99
+ it "generates keypair, adds deploy key, and clones repository" do
100
+ allow(repo_provisioner).to receive(:already_cloned?).and_return(false)
101
+ allow(repo_provisioner).to receive(:can_clone_project?).and_return(false)
102
+ allow(repo_provisioner).to receive(:ssh_keypair?).and_return(false)
103
+ allow(provision_server).to receive(:run).with("cat ~/.ssh/id_rsa.pub", home: true).and_return("ssh-rsa AAAAB3...")
104
+
105
+ expect(provision_server).to receive(:run!).with('ssh-keygen -t rsa -b 2048 -f ~/.ssh/id_rsa -q -N ""', home: true)
106
+ expect(github_api).to receive(:add_deploy_key).with(title: "deploy@example.com", key: "ssh-rsa AAAAB3...")
107
+ expect(provision_server).to receive(:run!).with("git clone git@github.com:botandrosedesign/test_project", home: true)
108
+
109
+ repo_provisioner.call
110
+ end
111
+
112
+ it "prints status messages during setup" do
113
+ allow(repo_provisioner).to receive(:already_cloned?).and_return(false)
114
+ allow(repo_provisioner).to receive(:can_clone_project?).and_return(false)
115
+ allow(repo_provisioner).to receive(:ssh_keypair?).and_return(false)
116
+ allow(provision_server).to receive(:run).and_return("ssh-rsa AAAAB3...")
117
+ allow(provision_server).to receive(:run!)
118
+ allow(github_api).to receive(:add_deploy_key)
119
+
120
+ expect(repo_provisioner).to receive(:print).with("Repo:")
121
+ expect(repo_provisioner).to receive(:print).with(" Generating keypair in ~/.ssh,")
122
+ expect(repo_provisioner).to receive(:print).with(" Add public key to GitHub repo deploy keys,")
123
+ expect(repo_provisioner).to receive(:print).with(" Cloning repo,")
124
+ expect(repo_provisioner).to receive(:puts).with(" ✓")
125
+
126
+ repo_provisioner.call
127
+ end
128
+ end
129
+
130
+ it "always prints success message" do
131
+ allow(repo_provisioner).to receive(:already_cloned?).and_return(true)
132
+ allow(repo_provisioner).to receive(:on_latest_master?).and_return(true)
133
+
134
+ expect(repo_provisioner).to receive(:print).with("Repo:")
135
+ expect(repo_provisioner).to receive(:puts).with(" ✓")
136
+
137
+ repo_provisioner.call
138
+ end
139
+ end
140
+
141
+ describe "private methods" do
142
+ describe "#ssh_keypair?" do
143
+ it "checks if SSH public key exists" do
144
+ expect(provision_server).to receive(:run).with("[ -f ~/.ssh/id_rsa.pub ]", home: true, quiet: true)
145
+
146
+ repo_provisioner.send(:ssh_keypair?)
147
+ end
148
+ end
149
+
150
+ describe "#already_cloned?" do
151
+ it "checks if git directory exists" do
152
+ expect(provision_server).to receive(:run).with("[ -d ~/test_project/.git ]", home: true, quiet: true)
153
+
154
+ repo_provisioner.send(:already_cloned?)
155
+ end
156
+ end
157
+
158
+ describe "#can_clone_project?" do
159
+ it "tests if repository can be cloned" do
160
+ expected_commands = [
161
+ "needle=$(ssh-keyscan -t ed25519 github.com 2>/dev/null | cut -d \" \" -f 2-3)",
162
+ "grep -q \"$needle\" ~/.ssh/known_hosts || ssh-keyscan -H github.com >> ~/.ssh/known_hosts 2>/dev/null",
163
+ "git ls-remote git@github.com:botandrosedesign/test_project"
164
+ ].join("; ")
165
+
166
+ expect(provision_server).to receive(:run).with(expected_commands, home: true, quiet: true)
167
+
168
+ repo_provisioner.send(:can_clone_project?)
169
+ end
170
+ end
171
+
172
+ describe "#project_name" do
173
+ it "returns the config's project name" do
174
+ expect(repo_provisioner.send(:project_name)).to eq("test_project")
175
+ end
176
+ end
177
+
178
+ describe "#on_latest_master?" do
179
+ it "checks if current HEAD matches origin/master after fetching" do
180
+ expected_command = "cd ~/test_project && git fetch origin && [ $(git rev-parse HEAD) = $(git rev-parse origin/master) ]"
181
+ expect(provision_server).to receive(:run).with(expected_command, home: true, quiet: true)
182
+
183
+ repo_provisioner.send(:on_latest_master?)
184
+ end
185
+
186
+ it "returns true when on latest master" do
187
+ allow(provision_server).to receive(:run).and_return(true)
188
+
189
+ expect(repo_provisioner.send(:on_latest_master?)).to be true
190
+ end
191
+
192
+ it "returns false when not on latest master" do
193
+ allow(provision_server).to receive(:run).and_return(false)
194
+
195
+ expect(repo_provisioner.send(:on_latest_master?)).to be false
196
+ end
197
+ end
198
+
199
+ describe "#update_to_latest_master!" do
200
+ it "checks out master and resets to origin/master" do
201
+ expected_command = "cd ~/test_project && git checkout master && git reset --hard origin/master"
202
+ expect(provision_server).to receive(:run!).with(expected_command, home: true)
203
+
204
+ repo_provisioner.send(:update_to_latest_master!)
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,49 @@
1
+ require "spec_helper"
2
+ require "bard/new/provision/base"
3
+ require "bard/new/provision/rvm"
4
+
5
+ describe Bard::Provision::RVM do
6
+ let(:config) { { production: double("production") } }
7
+ let(:ssh_url) { "user@example.com" }
8
+ let(:provision_server) { double("provision_server") }
9
+ let(:rvm) { Bard::Provision::RVM.new(config, ssh_url) }
10
+
11
+ before do
12
+ allow(rvm).to receive(:provision_server).and_return(provision_server)
13
+ allow(rvm).to receive(:print)
14
+ allow(rvm).to receive(:puts)
15
+ allow(File).to receive(:read).with(".ruby-version").and_return("3.2.0\n")
16
+ end
17
+
18
+ describe "#call" do
19
+ context "when RVM is not installed" do
20
+ it "installs RVM and Ruby" do
21
+ allow(provision_server).to receive(:run).with("[ -d ~/.rvm ]", quiet: true).and_return(false)
22
+
23
+ expect(provision_server).to receive(:run!).with(/sed -i.*bashrc/)
24
+ expect(provision_server).to receive(:run!).with("rvm install 3.2.0")
25
+
26
+ rvm.call
27
+ end
28
+ end
29
+
30
+ context "when RVM is already installed" do
31
+ it "skips installation" do
32
+ allow(provision_server).to receive(:run).with("[ -d ~/.rvm ]", quiet: true).and_return(true)
33
+
34
+ expect(provision_server).not_to receive(:run!)
35
+
36
+ rvm.call
37
+ end
38
+ end
39
+
40
+ it "prints status messages" do
41
+ allow(provision_server).to receive(:run).and_return(true)
42
+
43
+ expect(rvm).to receive(:print).with("RVM:")
44
+ expect(rvm).to receive(:puts).with(" ✓")
45
+
46
+ rvm.call
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,242 @@
1
+ require "spec_helper"
2
+ require "bard/new/provision/base"
3
+ require "bard/new/provision/ssh"
4
+
5
+ describe Bard::Provision::SSH do
6
+ let(:ssh_uri) { double("ssh_uri", host: "example.com", port: 2222) }
7
+ let(:provision_ssh_uri) { double("provision_ssh_uri", host: "example.com", port: nil) }
8
+ let(:target) { double("target", ssh_uri: ssh_uri) }
9
+ let(:config) { { production: target } }
10
+ let(:ssh_url) { "user@example.com" }
11
+ let(:provision_server) { double("provision_server", ssh_uri: provision_ssh_uri) }
12
+ let(:ssh_provisioner) { Bard::Provision::SSH.new(config, ssh_url) }
13
+
14
+ before do
15
+ allow(ssh_provisioner).to receive(:target).and_return(target)
16
+ allow(ssh_provisioner).to receive(:provision_server).and_return(provision_server)
17
+ allow(ssh_provisioner).to receive(:print)
18
+ allow(ssh_provisioner).to receive(:puts)
19
+ allow(ssh_provisioner).to receive(:system)
20
+ end
21
+
22
+ describe "#call" do
23
+ context "when SSH is already available on target port" do
24
+ it "skips port reconfiguration but checks known hosts" do
25
+ allow(ssh_provisioner).to receive(:password_auth_enabled?).and_return(false)
26
+ allow(ssh_provisioner).to receive(:ssh_available?).with(provision_ssh_uri, port: 2222).and_return(true)
27
+ allow(ssh_provisioner).to receive(:ssh_known_host?).with(provision_ssh_uri).and_return(true)
28
+
29
+ expect(provision_server).not_to receive(:run!)
30
+
31
+ ssh_provisioner.call
32
+ end
33
+
34
+ it "adds to known hosts if not present" do
35
+ allow(ssh_provisioner).to receive(:password_auth_enabled?).and_return(false)
36
+ allow(ssh_provisioner).to receive(:ssh_available?).with(provision_ssh_uri, port: 2222).and_return(true)
37
+ allow(ssh_provisioner).to receive(:ssh_known_host?).with(provision_ssh_uri).and_return(false)
38
+
39
+ expect(ssh_provisioner).to receive(:add_ssh_known_host!).with(provision_ssh_uri)
40
+
41
+ ssh_provisioner.call
42
+ end
43
+ end
44
+
45
+ context "when SSH is not available on target port but available on default port" do
46
+ it "reconfigures SSH port and adds to known hosts" do
47
+ allow(ssh_provisioner).to receive(:password_auth_enabled?).and_return(false)
48
+ allow(ssh_provisioner).to receive(:ssh_available?).with(provision_ssh_uri, port: 2222).and_return(false, true)
49
+ allow(ssh_provisioner).to receive(:ssh_available?).with(provision_ssh_uri).and_return(true)
50
+ allow(ssh_provisioner).to receive(:ssh_known_host?).with(provision_ssh_uri).and_return(false)
51
+ allow(ssh_provisioner).to receive(:sleep)
52
+
53
+ expect(ssh_provisioner).to receive(:add_ssh_known_host!).with(provision_ssh_uri).twice
54
+ expect(provision_server).to receive(:run!).with(
55
+ 'echo "Port 2222" | sudo tee /etc/ssh/sshd_config.d/port_2222.conf && sudo service ssh restart',
56
+ home: true
57
+ )
58
+
59
+ ssh_provisioner.call
60
+ end
61
+
62
+ it "raises if new port is not responding after reconfiguration" do
63
+ allow(ssh_provisioner).to receive(:password_auth_enabled?).and_return(false)
64
+ allow(ssh_provisioner).to receive(:ssh_available?).with(provision_ssh_uri, port: 2222).and_return(false)
65
+ allow(ssh_provisioner).to receive(:ssh_available?).with(provision_ssh_uri).and_return(true)
66
+ allow(ssh_provisioner).to receive(:ssh_known_host?).with(provision_ssh_uri).and_return(true)
67
+ allow(provision_server).to receive(:run!)
68
+ allow(ssh_provisioner).to receive(:sleep)
69
+
70
+ expect { ssh_provisioner.call }.to raise_error(/reconfigured SSH to port 2222 but it's not responding/)
71
+ end
72
+
73
+ it "prints status messages during reconfiguration" do
74
+ allow(ssh_provisioner).to receive(:password_auth_enabled?).and_return(false)
75
+ allow(ssh_provisioner).to receive(:ssh_available?).with(provision_ssh_uri, port: 2222).and_return(false, true)
76
+ allow(ssh_provisioner).to receive(:ssh_available?).with(provision_ssh_uri).and_return(true)
77
+ allow(ssh_provisioner).to receive(:ssh_known_host?).and_return(false)
78
+ allow(ssh_provisioner).to receive(:add_ssh_known_host!)
79
+ allow(provision_server).to receive(:run!)
80
+ allow(ssh_provisioner).to receive(:sleep)
81
+
82
+ expect(ssh_provisioner).to receive(:print).with("SSH:")
83
+ expect(ssh_provisioner).to receive(:print).with(" Adding known host,")
84
+ expect(ssh_provisioner).to receive(:print).with(" Reconfiguring port to 2222,")
85
+ expect(ssh_provisioner).to receive(:print).with(" Adding known host,")
86
+ expect(ssh_provisioner).to receive(:puts).with(" ✓")
87
+
88
+ ssh_provisioner.call
89
+ end
90
+ end
91
+
92
+ context "when SSH is not available on either port" do
93
+ it "raises an error" do
94
+ allow(ssh_provisioner).to receive(:password_auth_enabled?).and_return(false)
95
+ allow(ssh_provisioner).to receive(:ssh_available?).with(provision_ssh_uri, port: 2222).and_return(false)
96
+ allow(ssh_provisioner).to receive(:ssh_available?).with(provision_ssh_uri).and_return(false)
97
+
98
+ expect { ssh_provisioner.call }.to raise_error("can't find SSH on port 2222 or 22")
99
+ end
100
+ end
101
+
102
+ context "when password authentication is enabled" do
103
+ it "disables password authentication" do
104
+ allow(ssh_provisioner).to receive(:password_auth_enabled?).and_return(true)
105
+ allow(ssh_provisioner).to receive(:ssh_available?).with(provision_ssh_uri, port: 2222).and_return(true)
106
+ allow(ssh_provisioner).to receive(:ssh_known_host?).and_return(true)
107
+
108
+ expect(ssh_provisioner).to receive(:disable_password_auth!)
109
+
110
+ ssh_provisioner.call
111
+ end
112
+
113
+ it "prints status message when disabling password authentication" do
114
+ allow(ssh_provisioner).to receive(:password_auth_enabled?).and_return(true)
115
+ allow(ssh_provisioner).to receive(:disable_password_auth!)
116
+ allow(ssh_provisioner).to receive(:ssh_available?).with(provision_ssh_uri, port: 2222).and_return(true)
117
+ allow(ssh_provisioner).to receive(:ssh_known_host?).and_return(true)
118
+
119
+ expect(ssh_provisioner).to receive(:print).with("SSH:")
120
+ expect(ssh_provisioner).to receive(:print).with(" Disabling password authentication,")
121
+ expect(ssh_provisioner).to receive(:puts).with(" ✓")
122
+
123
+ ssh_provisioner.call
124
+ end
125
+ end
126
+
127
+ context "when password authentication is already disabled" do
128
+ it "skips disabling password authentication" do
129
+ allow(ssh_provisioner).to receive(:password_auth_enabled?).and_return(false)
130
+ allow(ssh_provisioner).to receive(:ssh_available?).with(provision_ssh_uri, port: 2222).and_return(true)
131
+ allow(ssh_provisioner).to receive(:ssh_known_host?).and_return(true)
132
+
133
+ expect(ssh_provisioner).not_to receive(:disable_password_auth!)
134
+
135
+ ssh_provisioner.call
136
+ end
137
+ end
138
+
139
+ it "updates the SSH URL with the target port" do
140
+ allow(ssh_provisioner).to receive(:password_auth_enabled?).and_return(false)
141
+ allow(ssh_provisioner).to receive(:ssh_available?).with(provision_ssh_uri, port: 2222).and_return(true)
142
+ allow(ssh_provisioner).to receive(:ssh_known_host?).and_return(true)
143
+
144
+ ssh_provisioner.call
145
+ end
146
+ end
147
+
148
+ describe "private methods" do
149
+ describe "#target_port" do
150
+ it "returns the target's SSH port" do
151
+ expect(ssh_provisioner.send(:target_port)).to eq(2222)
152
+ end
153
+
154
+ it "defaults to 22 if no port is specified" do
155
+ allow(ssh_uri).to receive(:port).and_return(nil)
156
+ expect(ssh_provisioner.send(:target_port)).to eq(22)
157
+ end
158
+ end
159
+
160
+ describe "#ssh_available?" do
161
+ it "tests SSH connectivity on specified port" do
162
+ expect(ssh_provisioner).to receive(:system).with("nc -zv example.com 2222 2>/dev/null")
163
+
164
+ ssh_provisioner.send(:ssh_available?, provision_ssh_uri, port: 2222)
165
+ end
166
+
167
+ it "uses SSH URI port if no port specified" do
168
+ allow(provision_ssh_uri).to receive(:port).and_return(2222)
169
+ expect(ssh_provisioner).to receive(:system).with("nc -zv example.com 2222 2>/dev/null")
170
+
171
+ ssh_provisioner.send(:ssh_available?, provision_ssh_uri)
172
+ end
173
+
174
+ it "defaults to port 22 if no port in URI or parameter" do
175
+ expect(ssh_provisioner).to receive(:system).with("nc -zv example.com 22 2>/dev/null")
176
+
177
+ ssh_provisioner.send(:ssh_available?, provision_ssh_uri)
178
+ end
179
+ end
180
+
181
+ describe "#ssh_known_host?" do
182
+ it "checks if host is in known_hosts file" do
183
+ expected_command = 'grep -q "$(ssh-keyscan -t ed25519 -p22 example.com 2>/dev/null | cut -d \' \' -f 2-3)" ~/.ssh/known_hosts'
184
+ expect(ssh_provisioner).to receive(:system).with(expected_command)
185
+
186
+ ssh_provisioner.send(:ssh_known_host?, provision_ssh_uri)
187
+ end
188
+ end
189
+
190
+ describe "#add_ssh_known_host!" do
191
+ it "adds host to known_hosts file" do
192
+ expected_command = "ssh-keyscan -p22 -H example.com >> ~/.ssh/known_hosts 2>/dev/null"
193
+ expect(ssh_provisioner).to receive(:system).with(expected_command)
194
+
195
+ ssh_provisioner.send(:add_ssh_known_host!, provision_ssh_uri)
196
+ end
197
+ end
198
+
199
+ describe "#password_auth_enabled?" do
200
+ it "returns true when password authentication is enabled" do
201
+ expect(provision_server).to receive(:run!).with(
202
+ %q{grep -E '^\s*PasswordAuthentication\s+yes' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf 2>/dev/null || true},
203
+ home: true,
204
+ capture: true
205
+ ).and_return("PasswordAuthentication yes\n")
206
+
207
+ expect(ssh_provisioner.send(:password_auth_enabled?)).to be true
208
+ end
209
+
210
+ it "returns false when password authentication is disabled" do
211
+ expect(provision_server).to receive(:run!).with(
212
+ %q{grep -E '^\s*PasswordAuthentication\s+yes' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf 2>/dev/null || true},
213
+ home: true,
214
+ capture: true
215
+ ).and_return("")
216
+
217
+ expect(ssh_provisioner.send(:password_auth_enabled?)).to be false
218
+ end
219
+
220
+ it "returns false when grep returns nil" do
221
+ expect(provision_server).to receive(:run!).with(
222
+ %q{grep -E '^\s*PasswordAuthentication\s+yes' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf 2>/dev/null || true},
223
+ home: true,
224
+ capture: true
225
+ ).and_return(nil)
226
+
227
+ expect(ssh_provisioner.send(:password_auth_enabled?)).to be false
228
+ end
229
+ end
230
+
231
+ describe "#disable_password_auth!" do
232
+ it "creates sshd config file to disable password authentication and restarts ssh" do
233
+ expect(provision_server).to receive(:run!).with(
234
+ %q{echo "PasswordAuthentication no" | sudo tee /etc/ssh/sshd_config.d/disable_password_auth.conf && sudo service ssh restart},
235
+ home: true
236
+ )
237
+
238
+ ssh_provisioner.send(:disable_password_auth!)
239
+ end
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,33 @@
1
+ require "spec_helper"
2
+ require "bard/new/provision/base"
3
+ require "bard/new/provision/swapfile"
4
+
5
+ describe Bard::Provision::Swapfile do
6
+ let(:config) { { production: double("production") } }
7
+ let(:ssh_url) { "user@example.com" }
8
+ let(:provision_server) { double("provision_server") }
9
+ let(:swapfile) { Bard::Provision::Swapfile.new(config, ssh_url) }
10
+
11
+ before do
12
+ allow(swapfile).to receive(:provision_server).and_return(provision_server)
13
+ allow(swapfile).to receive(:print)
14
+ allow(swapfile).to receive(:puts)
15
+ end
16
+
17
+ describe "#call" do
18
+ it "sets up swapfile on the server" do
19
+ expect(provision_server).to receive(:run!).with(/if \[ ! -f \/swapfile \]/, home: true).ordered
20
+ expect(provision_server).to receive(:run!).with("sudo swapon --show | grep -q /swapfile", home: true).ordered
21
+
22
+ swapfile.call
23
+ end
24
+
25
+ it "prints status messages" do
26
+ allow(provision_server).to receive(:run!)
27
+ expect(swapfile).to receive(:print).with("Swapfile:")
28
+ expect(swapfile).to receive(:puts).with(" ✓")
29
+
30
+ swapfile.call
31
+ end
32
+ end
33
+ end