bard 1.6.0 → 1.7.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 +4 -4
- data/Gemfile +1 -1
- data/bard.gemspec +1 -0
- data/features/podman_testcontainers.feature +16 -0
- data/features/step_definitions/podman_steps.rb +23 -0
- data/features/support/podman.rb +153 -0
- data/lib/bard/config.rb +36 -27
- data/lib/bard/version.rb +1 -1
- data/spec/bard/config_spec.rb +20 -13
- metadata +22 -4
- data/spec/acceptance/podman_testcontainers_spec.rb +0 -140
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a11cf0d4ee1ff435809dbe0e48beb1dcff72f2a4191fa39c40ef8932bbf64915
|
|
4
|
+
data.tar.gz: 4861d2109f0cb0d438dd281425865ac8c0496019c8529560464fab55c05b5da7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 53d527f268f68f7b5a61cae324e583f83c26246bd69f4717a37e10c2e1ae44ca53d7d3b194a60454a82c1471ba8588f1182177dd7994b41c193739889a0d5a36
|
|
7
|
+
data.tar.gz: 669b2ae00f5c9983fed2747e1426f48710c5da142b3d43cda7bb0c7c3c160dc9ee52385f606ea65dbf087803733461bf4ce71d15b3bb8ed5373737555dd441a8
|
data/Gemfile
CHANGED
data/bard.gemspec
CHANGED
|
@@ -26,5 +26,6 @@ Gem::Specification.new do |spec|
|
|
|
26
26
|
spec.add_development_dependency "rake"
|
|
27
27
|
spec.add_development_dependency "rspec"
|
|
28
28
|
spec.add_development_dependency "debug"
|
|
29
|
+
spec.add_development_dependency "cucumber"
|
|
29
30
|
spec.add_development_dependency "testcontainers"
|
|
30
31
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
@podman
|
|
2
|
+
Feature: bard run against a podman TestContainers host
|
|
3
|
+
Background:
|
|
4
|
+
Given a podman testcontainer is ready for bard
|
|
5
|
+
|
|
6
|
+
Scenario: Running ls via bard run
|
|
7
|
+
Given a remote file "test-file.txt" exists in the test container
|
|
8
|
+
When I run bard "ls" against the test container
|
|
9
|
+
Then the bard command should succeed
|
|
10
|
+
And the bard output should include "test-file.txt"
|
|
11
|
+
|
|
12
|
+
Scenario: Running commands in isolated containers
|
|
13
|
+
Given a remote file "another-file.txt" containing "content" exists in the test container
|
|
14
|
+
When I run bard "cat another-file.txt" against the test container
|
|
15
|
+
Then the bard command should succeed
|
|
16
|
+
And the bard output should include "content"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Given /^a podman testcontainer is ready for bard$/ do
|
|
2
|
+
raise "Podman testcontainer failed to start" unless @podman_container && @podman_ssh_port
|
|
3
|
+
end
|
|
4
|
+
|
|
5
|
+
Given /^a remote file "([^\"]+)" exists in the test container$/ do |filename|
|
|
6
|
+
run_ssh("touch testproject/#{filename}").should be_true
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
Given /^a remote file "([^\"]+)" containing "([^\"]+)" exists in the test container$/ do |filename, content|
|
|
10
|
+
run_ssh("echo #{Shellwords.escape(content)} > testproject/#{filename}").should be_true
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
When /^I run bard "([^\"]+)" against the test container$/ do |command|
|
|
14
|
+
run_bard_against_container(command)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
Then /^the bard command should succeed$/ do
|
|
18
|
+
@status.success?.should be_true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
Then /^the bard output should include "([^\"]+)"$/ do |expected|
|
|
22
|
+
@stdout.should include(expected)
|
|
23
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "open3"
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
require "testcontainers"
|
|
6
|
+
|
|
7
|
+
module PodmanWorld
|
|
8
|
+
class << self
|
|
9
|
+
attr_accessor :podman_available, :podman_image_built
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class PrerequisiteError < StandardError; end
|
|
13
|
+
|
|
14
|
+
def ensure_podman_available
|
|
15
|
+
return if @podman_available || PodmanWorld.podman_available
|
|
16
|
+
|
|
17
|
+
raise PrerequisiteError, "podman is not installed or not on PATH" unless system("command -v podman >/dev/null 2>&1")
|
|
18
|
+
|
|
19
|
+
configure_podman_socket
|
|
20
|
+
ensure_bard_test_image
|
|
21
|
+
FileUtils.chmod(0o600, podman_ssh_key_path)
|
|
22
|
+
|
|
23
|
+
PodmanWorld.podman_available = true
|
|
24
|
+
@podman_available = true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def configure_podman_socket
|
|
28
|
+
return if ENV["DOCKER_HOST"]
|
|
29
|
+
|
|
30
|
+
podman_socket = "/run/user/#{Process.uid}/podman/podman.sock"
|
|
31
|
+
unless File.exist?(podman_socket)
|
|
32
|
+
system("systemctl --user start podman.socket 2>/dev/null || podman system service --time=0 unix://#{podman_socket} &")
|
|
33
|
+
sleep 2
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
raise PrerequisiteError, "Podman socket not available at #{podman_socket}" unless File.exist?(podman_socket)
|
|
37
|
+
|
|
38
|
+
ENV["DOCKER_HOST"] = "unix://#{podman_socket}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def ensure_bard_test_image
|
|
42
|
+
return if @podman_image_built || PodmanWorld.podman_image_built
|
|
43
|
+
|
|
44
|
+
raise PrerequisiteError, "Unable to pull ubuntu:22.04 image" unless system("podman pull ubuntu:22.04 >/dev/null 2>&1")
|
|
45
|
+
|
|
46
|
+
docker_dir = File.join(ROOT, "spec/acceptance/docker")
|
|
47
|
+
dockerfile = File.join(docker_dir, "Dockerfile")
|
|
48
|
+
unless system("podman build -t bard-test-server -f #{dockerfile} #{docker_dir} 2>&1")
|
|
49
|
+
raise PrerequisiteError, "Failed to build bard test image"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
PodmanWorld.podman_image_built = true
|
|
53
|
+
@podman_image_built = true
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def start_podman_container
|
|
57
|
+
ensure_podman_available
|
|
58
|
+
|
|
59
|
+
@podman_container = Testcontainers::DockerContainer
|
|
60
|
+
.new("localhost/bard-test-server:latest")
|
|
61
|
+
.with_exposed_port(22)
|
|
62
|
+
.with_name("bard-test-#{SecureRandom.hex(4)}")
|
|
63
|
+
.start
|
|
64
|
+
|
|
65
|
+
@podman_ssh_port = @podman_container.mapped_port(22)
|
|
66
|
+
wait_for_ssh
|
|
67
|
+
run_ssh("mkdir -p testproject")
|
|
68
|
+
write_bard_config
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def wait_for_ssh
|
|
72
|
+
30.times do
|
|
73
|
+
return if run_ssh("echo ready", quiet: true)
|
|
74
|
+
sleep 0.5
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
raise PrerequisiteError, "SSH in podman container did not become ready"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def write_bard_config
|
|
81
|
+
FileUtils.mkdir_p(File.join(ROOT, "tmp"))
|
|
82
|
+
@bard_config_path = File.join(ROOT, "tmp", "test_bard_#{SecureRandom.hex(4)}.rb")
|
|
83
|
+
|
|
84
|
+
File.write(@bard_config_path, <<~RUBY)
|
|
85
|
+
server :production do
|
|
86
|
+
ssh "deploy@localhost:#{@podman_ssh_port}"
|
|
87
|
+
path "testproject"
|
|
88
|
+
ssh_key "#{podman_ssh_key_path}"
|
|
89
|
+
ping false
|
|
90
|
+
end
|
|
91
|
+
RUBY
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def run_ssh(command, quiet: false)
|
|
95
|
+
escaped = Shellwords.escape(command)
|
|
96
|
+
ssh_command = [
|
|
97
|
+
"ssh",
|
|
98
|
+
"-o", "StrictHostKeyChecking=no",
|
|
99
|
+
"-o", "ConnectTimeout=1",
|
|
100
|
+
"-p", @podman_ssh_port.to_s,
|
|
101
|
+
"-i", podman_ssh_key_path,
|
|
102
|
+
"deploy@localhost",
|
|
103
|
+
"--",
|
|
104
|
+
"bash",
|
|
105
|
+
"-lc",
|
|
106
|
+
escaped
|
|
107
|
+
].join(" ")
|
|
108
|
+
|
|
109
|
+
quiet ? system("#{ssh_command} >/dev/null 2>&1") : system(ssh_command)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def run_bard_against_container(command)
|
|
113
|
+
Dir.chdir(File.join(ROOT, "tmp")) do
|
|
114
|
+
FileUtils.cp(@bard_config_path, "bard.rb")
|
|
115
|
+
@stdout, @status = Open3.capture2e(@env || {}, "bard run #{command}")
|
|
116
|
+
@stderr = ""
|
|
117
|
+
FileUtils.rm_f("bard.rb")
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def podman_ssh_key_path
|
|
122
|
+
@podman_ssh_key_path ||= File.expand_path(File.join(ROOT, "spec/acceptance/docker/test_key"))
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def stop_podman_container
|
|
126
|
+
FileUtils.rm_f(@bard_config_path) if @bard_config_path
|
|
127
|
+
return unless @podman_container
|
|
128
|
+
|
|
129
|
+
@podman_container.stop
|
|
130
|
+
@podman_container.remove
|
|
131
|
+
rescue StandardError => e
|
|
132
|
+
warn "Failed to cleanup podman container: #{e.message}"
|
|
133
|
+
ensure
|
|
134
|
+
@podman_container = nil
|
|
135
|
+
@podman_ssh_port = nil
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
World(PodmanWorld)
|
|
140
|
+
|
|
141
|
+
Before("@podman") do
|
|
142
|
+
@env ||= {}
|
|
143
|
+
|
|
144
|
+
begin
|
|
145
|
+
start_podman_container
|
|
146
|
+
rescue PodmanWorld::PrerequisiteError => e
|
|
147
|
+
pending(e.message)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
After("@podman") do
|
|
152
|
+
stop_podman_container
|
|
153
|
+
end
|
data/lib/bard/config.rb
CHANGED
|
@@ -2,6 +2,12 @@ require "bard/server"
|
|
|
2
2
|
|
|
3
3
|
module Bard
|
|
4
4
|
class Config
|
|
5
|
+
def self.current(working_directory: Dir.getwd)
|
|
6
|
+
project_name = File.basename(working_directory)
|
|
7
|
+
path = File.join(working_directory, "bard.rb")
|
|
8
|
+
new(project_name, path: path)
|
|
9
|
+
end
|
|
10
|
+
|
|
5
11
|
def initialize project_name, path: nil, source: nil
|
|
6
12
|
@project_name = project_name
|
|
7
13
|
@servers = {
|
|
@@ -69,30 +75,23 @@ module Bard
|
|
|
69
75
|
if block_given?
|
|
70
76
|
@backup = BackupConfig.new(&block)
|
|
71
77
|
elsif value == false
|
|
72
|
-
@backup =
|
|
73
|
-
elsif value
|
|
74
|
-
|
|
75
|
-
@backup = BackupConfig.new { bard }
|
|
76
|
-
elsif value.nil?
|
|
77
|
-
# Getter
|
|
78
|
-
return @backup if defined?(@backup)
|
|
79
|
-
@backup = BackupConfig.new { bard } # Default
|
|
78
|
+
@backup = BackupConfig.new { disabled }
|
|
79
|
+
elsif value.nil? # Getter
|
|
80
|
+
@backup ||= BackupConfig.new { bard }
|
|
80
81
|
else
|
|
81
|
-
raise ArgumentError, "backup accepts a block
|
|
82
|
+
raise ArgumentError, "backup accepts false or a block"
|
|
82
83
|
end
|
|
83
84
|
end
|
|
84
85
|
|
|
85
86
|
# short-hand for michael
|
|
86
87
|
|
|
87
|
-
def github_pages url
|
|
88
|
+
def github_pages url
|
|
88
89
|
urls = []
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
urls << "www.#{hostname}"
|
|
95
|
-
end
|
|
90
|
+
uri = url.start_with?("http") ? URI.parse(url) : URI.parse("https://#{url}")
|
|
91
|
+
hostname = uri.hostname.sub(/^www\./, '')
|
|
92
|
+
urls = [hostname]
|
|
93
|
+
if hostname.count(".") < 2
|
|
94
|
+
urls << "www.#{hostname}"
|
|
96
95
|
end
|
|
97
96
|
|
|
98
97
|
server :production do
|
|
@@ -106,31 +105,41 @@ module Bard
|
|
|
106
105
|
end
|
|
107
106
|
|
|
108
107
|
class BackupConfig
|
|
109
|
-
attr_reader :
|
|
108
|
+
attr_reader :destinations
|
|
110
109
|
|
|
111
110
|
def initialize(&block)
|
|
112
|
-
@bard_enabled = false
|
|
113
111
|
@destinations = []
|
|
114
112
|
instance_eval(&block) if block_given?
|
|
115
113
|
end
|
|
116
114
|
|
|
117
115
|
def bard
|
|
118
|
-
@
|
|
116
|
+
@bard = true
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def bard?
|
|
120
|
+
!!@bard
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def disabled
|
|
124
|
+
@disabled = true
|
|
119
125
|
end
|
|
120
126
|
|
|
121
|
-
def
|
|
127
|
+
def disabled?
|
|
128
|
+
!!@disabled
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def enabled?
|
|
132
|
+
!disabled?
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def s3(name, **kwargs)
|
|
122
136
|
@destinations << {
|
|
123
137
|
name: name,
|
|
124
138
|
type: :s3,
|
|
125
|
-
|
|
126
|
-
path: path
|
|
139
|
+
**kwargs,
|
|
127
140
|
}
|
|
128
141
|
end
|
|
129
142
|
|
|
130
|
-
def bard?
|
|
131
|
-
@bard_enabled
|
|
132
|
-
end
|
|
133
|
-
|
|
134
143
|
def self_managed?
|
|
135
144
|
@destinations.any?
|
|
136
145
|
end
|
data/lib/bard/version.rb
CHANGED
data/spec/bard/config_spec.rb
CHANGED
|
@@ -78,7 +78,7 @@ describe Bard::Config do
|
|
|
78
78
|
|
|
79
79
|
describe "#backup" do
|
|
80
80
|
it "returns the backup setting" do
|
|
81
|
-
expect(subject.backup).to
|
|
81
|
+
expect(subject.backup).to be_disabled
|
|
82
82
|
end
|
|
83
83
|
end
|
|
84
84
|
end
|
|
@@ -97,7 +97,7 @@ describe Bard::Config do
|
|
|
97
97
|
end
|
|
98
98
|
|
|
99
99
|
describe "#backup with s3 directive" do
|
|
100
|
-
subject { described_class.new("test", source: "backup { s3 :primary,
|
|
100
|
+
subject { described_class.new("test", source: "backup { s3 :primary, path: 'bucket/path' }") }
|
|
101
101
|
|
|
102
102
|
it "returns a BackupConfig with s3 destination" do
|
|
103
103
|
backup = subject.backup
|
|
@@ -109,13 +109,12 @@ describe Bard::Config do
|
|
|
109
109
|
dest = backup.destinations.first
|
|
110
110
|
expect(dest[:name]).to eq :primary
|
|
111
111
|
expect(dest[:type]).to eq :s3
|
|
112
|
-
expect(dest[:credentials]).to eq :backup
|
|
113
112
|
expect(dest[:path]).to eq 'bucket/path'
|
|
114
113
|
end
|
|
115
114
|
end
|
|
116
115
|
|
|
117
116
|
describe "#backup with both bard and s3 directives" do
|
|
118
|
-
subject { described_class.new("test", source: "backup { bard; s3 :custom,
|
|
117
|
+
subject { described_class.new("test", source: "backup { bard; s3 :custom, path: 'bucket/path' }") }
|
|
119
118
|
|
|
120
119
|
it "returns a BackupConfig with bard and s3 destination" do
|
|
121
120
|
backup = subject.backup
|
|
@@ -130,8 +129,8 @@ describe Bard::Config do
|
|
|
130
129
|
subject do
|
|
131
130
|
described_class.new("test", source: <<~SOURCE)
|
|
132
131
|
backup do
|
|
133
|
-
s3 :primary,
|
|
134
|
-
s3 :secondary,
|
|
132
|
+
s3 :primary, path: 'bucket1/path'
|
|
133
|
+
s3 :secondary, path: 'bucket2/path'
|
|
135
134
|
end
|
|
136
135
|
SOURCE
|
|
137
136
|
end
|
|
@@ -147,15 +146,23 @@ describe Bard::Config do
|
|
|
147
146
|
expect(backup.destinations[1][:name]).to eq :secondary
|
|
148
147
|
end
|
|
149
148
|
end
|
|
149
|
+
end
|
|
150
150
|
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
context "with github_pages directive" do
|
|
152
|
+
subject { described_class.new("test", source: "github_pages 'example.com'") }
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
expect(backup).to
|
|
157
|
-
|
|
158
|
-
|
|
154
|
+
describe "#backup" do
|
|
155
|
+
it "sets backup to false" do
|
|
156
|
+
expect(subject.backup).to be_disabled
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
describe "#server" do
|
|
161
|
+
it "creates a production server with github_pages enabled" do
|
|
162
|
+
production = subject[:production]
|
|
163
|
+
expect(production).not_to be_nil
|
|
164
|
+
expect(production.github_pages).to eq true
|
|
165
|
+
expect(production.ssh).to eq false
|
|
159
166
|
end
|
|
160
167
|
end
|
|
161
168
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bard
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Micah Geisel
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2025-12-
|
|
10
|
+
date: 2025-12-11 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: thor
|
|
@@ -121,6 +121,20 @@ dependencies:
|
|
|
121
121
|
- - ">="
|
|
122
122
|
- !ruby/object:Gem::Version
|
|
123
123
|
version: '0'
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: cucumber
|
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - ">="
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '0'
|
|
131
|
+
type: :development
|
|
132
|
+
prerelease: false
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - ">="
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '0'
|
|
124
138
|
- !ruby/object:Gem::Dependency
|
|
125
139
|
name: testcontainers
|
|
126
140
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -157,14 +171,17 @@ files:
|
|
|
157
171
|
- features/bard_deploy.feature
|
|
158
172
|
- features/bard_pull.feature
|
|
159
173
|
- features/bard_push.feature
|
|
174
|
+
- features/podman_testcontainers.feature
|
|
160
175
|
- features/step_definitions/check_steps.rb
|
|
161
176
|
- features/step_definitions/git_steps.rb
|
|
162
177
|
- features/step_definitions/global_steps.rb
|
|
178
|
+
- features/step_definitions/podman_steps.rb
|
|
163
179
|
- features/step_definitions/rails_steps.rb
|
|
164
180
|
- features/step_definitions/submodule_steps.rb
|
|
165
181
|
- features/support/env.rb
|
|
166
182
|
- features/support/grit_ext.rb
|
|
167
183
|
- features/support/io.rb
|
|
184
|
+
- features/support/podman.rb
|
|
168
185
|
- install_files/.github/dependabot.yml
|
|
169
186
|
- install_files/.github/workflows/cache-ci.yml
|
|
170
187
|
- install_files/.github/workflows/ci.yml
|
|
@@ -228,7 +245,6 @@ files:
|
|
|
228
245
|
- spec/acceptance/docker/Dockerfile
|
|
229
246
|
- spec/acceptance/docker/test_key
|
|
230
247
|
- spec/acceptance/docker/test_key.pub
|
|
231
|
-
- spec/acceptance/podman_testcontainers_spec.rb
|
|
232
248
|
- spec/bard/ci/github_actions_spec.rb
|
|
233
249
|
- spec/bard/ci_spec.rb
|
|
234
250
|
- spec/bard/cli/ci_spec.rb
|
|
@@ -301,19 +317,21 @@ test_files:
|
|
|
301
317
|
- features/bard_deploy.feature
|
|
302
318
|
- features/bard_pull.feature
|
|
303
319
|
- features/bard_push.feature
|
|
320
|
+
- features/podman_testcontainers.feature
|
|
304
321
|
- features/step_definitions/check_steps.rb
|
|
305
322
|
- features/step_definitions/git_steps.rb
|
|
306
323
|
- features/step_definitions/global_steps.rb
|
|
324
|
+
- features/step_definitions/podman_steps.rb
|
|
307
325
|
- features/step_definitions/rails_steps.rb
|
|
308
326
|
- features/step_definitions/submodule_steps.rb
|
|
309
327
|
- features/support/env.rb
|
|
310
328
|
- features/support/grit_ext.rb
|
|
311
329
|
- features/support/io.rb
|
|
330
|
+
- features/support/podman.rb
|
|
312
331
|
- spec/acceptance/.gitignore
|
|
313
332
|
- spec/acceptance/docker/Dockerfile
|
|
314
333
|
- spec/acceptance/docker/test_key
|
|
315
334
|
- spec/acceptance/docker/test_key.pub
|
|
316
|
-
- spec/acceptance/podman_testcontainers_spec.rb
|
|
317
335
|
- spec/bard/ci/github_actions_spec.rb
|
|
318
336
|
- spec/bard/ci_spec.rb
|
|
319
337
|
- spec/bard/cli/ci_spec.rb
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
# Acceptance test for Bard using Podman + TestContainers
|
|
2
|
-
#
|
|
3
|
-
# This test validates end-to-end functionality of `bard run ls` by:
|
|
4
|
-
# 1. Starting an SSH server container using TestContainers
|
|
5
|
-
# 2. Configuring Bard to connect to it
|
|
6
|
-
# 3. Running bard commands against the container
|
|
7
|
-
# 4. Automatically cleaning up when done
|
|
8
|
-
#
|
|
9
|
-
# Prerequisites:
|
|
10
|
-
# - gem install testcontainers
|
|
11
|
-
# - podman installed
|
|
12
|
-
# - podman socket running (systemctl --user start podman.socket)
|
|
13
|
-
# - Set DOCKER_HOST to podman socket
|
|
14
|
-
|
|
15
|
-
require 'spec_helper'
|
|
16
|
-
require 'testcontainers'
|
|
17
|
-
require 'open3'
|
|
18
|
-
|
|
19
|
-
RSpec.describe "Bard acceptance test with Podman + TestContainers", type: :acceptance do
|
|
20
|
-
# Disable WebMock for acceptance tests - we need real HTTP connections to Podman
|
|
21
|
-
before(:all) do
|
|
22
|
-
WebMock.allow_net_connect!
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
after(:all) do
|
|
26
|
-
WebMock.disable_net_connect!
|
|
27
|
-
end
|
|
28
|
-
# Configure TestContainers to use Podman
|
|
29
|
-
before(:all) do
|
|
30
|
-
# Set up podman socket for TestContainers
|
|
31
|
-
# Use existing DOCKER_HOST if set (e.g., in CI), otherwise use default location
|
|
32
|
-
unless ENV['DOCKER_HOST']
|
|
33
|
-
podman_socket = "/run/user/#{Process.uid}/podman/podman.sock"
|
|
34
|
-
|
|
35
|
-
# Start podman socket if not running
|
|
36
|
-
unless File.exist?(podman_socket)
|
|
37
|
-
system("systemctl --user start podman.socket 2>/dev/null || podman system service --time=0 unix://#{podman_socket} &")
|
|
38
|
-
sleep 2
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# Configure TestContainers to use podman
|
|
42
|
-
ENV['DOCKER_HOST'] = "unix://#{podman_socket}"
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Ensure SSH key has correct permissions
|
|
46
|
-
system("chmod 600 spec/acceptance/docker/test_key")
|
|
47
|
-
|
|
48
|
-
# Check if we can pull images
|
|
49
|
-
unless system("podman pull ubuntu:22.04 >/dev/null 2>&1")
|
|
50
|
-
skip "Cannot pull images in this environment. Run in a network-enabled environment."
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Build the test image
|
|
54
|
-
unless system("podman build -t bard-test-server -f spec/acceptance/docker/Dockerfile spec/acceptance/docker 2>&1")
|
|
55
|
-
skip "Failed to build test image"
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# TestContainers will automatically manage this container
|
|
60
|
-
let(:container) do
|
|
61
|
-
Testcontainers::DockerContainer.new("localhost/bard-test-server:latest")
|
|
62
|
-
.with_exposed_port(22)
|
|
63
|
-
.with_name("bard-test-#{SecureRandom.hex(4)}")
|
|
64
|
-
.start
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
let(:ssh_port) { container.mapped_port(22) }
|
|
68
|
-
|
|
69
|
-
before(:each) do
|
|
70
|
-
# Ensure container is started
|
|
71
|
-
container
|
|
72
|
-
|
|
73
|
-
# Wait for SSH to be ready
|
|
74
|
-
30.times do
|
|
75
|
-
break if system("ssh -o StrictHostKeyChecking=no -o ConnectTimeout=1 -p #{ssh_port} deploy@localhost -i spec/acceptance/docker/test_key 'echo ready' 2>/dev/null")
|
|
76
|
-
sleep 0.5
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Create test project directory
|
|
80
|
-
system("ssh -o StrictHostKeyChecking=no -p #{ssh_port} deploy@localhost -i spec/acceptance/docker/test_key 'mkdir -p testproject'")
|
|
81
|
-
|
|
82
|
-
# Create bard config for this container
|
|
83
|
-
@bard_config_path = "tmp/test_bard_#{SecureRandom.hex(4)}.rb"
|
|
84
|
-
FileUtils.mkdir_p("tmp")
|
|
85
|
-
ssh_key_path = File.expand_path("spec/acceptance/docker/test_key")
|
|
86
|
-
File.write(@bard_config_path, <<~RUBY)
|
|
87
|
-
server :production do
|
|
88
|
-
ssh "deploy@localhost:#{ssh_port}"
|
|
89
|
-
path "testproject"
|
|
90
|
-
ssh_key "#{ssh_key_path}"
|
|
91
|
-
ping false
|
|
92
|
-
end
|
|
93
|
-
RUBY
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
after(:each) do
|
|
97
|
-
# Clean up bard config
|
|
98
|
-
FileUtils.rm_f(@bard_config_path) if @bard_config_path
|
|
99
|
-
|
|
100
|
-
# TestContainers will automatically stop and remove the container
|
|
101
|
-
container.stop if container
|
|
102
|
-
container.remove if container
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
it "runs ls command via bard run" do
|
|
106
|
-
# Create a test file in the container
|
|
107
|
-
result = system("ssh -o StrictHostKeyChecking=no -p #{ssh_port} deploy@localhost -i spec/acceptance/docker/test_key 'touch testproject/test-file.txt'")
|
|
108
|
-
expect(result).to be true
|
|
109
|
-
|
|
110
|
-
# Run bard command
|
|
111
|
-
Dir.chdir("tmp") do
|
|
112
|
-
# Copy config to bard.rb
|
|
113
|
-
FileUtils.cp("../#{@bard_config_path}", "bard.rb")
|
|
114
|
-
|
|
115
|
-
output, status = Open3.capture2e("bard run ls")
|
|
116
|
-
|
|
117
|
-
# Clean up
|
|
118
|
-
FileUtils.rm_f("bard.rb")
|
|
119
|
-
|
|
120
|
-
# Verify the command succeeded
|
|
121
|
-
expect(status).to be_success, "bard run failed with output: #{output}"
|
|
122
|
-
expect(output).to include("test-file.txt")
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
it "runs multiple commands in isolated containers" do
|
|
127
|
-
# Each test gets its own container automatically!
|
|
128
|
-
result = system("ssh -o StrictHostKeyChecking=no -p #{ssh_port} deploy@localhost -i spec/acceptance/docker/test_key 'echo content > testproject/another-file.txt'")
|
|
129
|
-
expect(result).to be true
|
|
130
|
-
|
|
131
|
-
Dir.chdir("tmp") do
|
|
132
|
-
FileUtils.cp("../#{@bard_config_path}", "bard.rb")
|
|
133
|
-
output, status = Open3.capture2e("bard run 'cat another-file.txt'")
|
|
134
|
-
FileUtils.rm_f("bard.rb")
|
|
135
|
-
|
|
136
|
-
expect(status).to be_success, "bard run failed with output: #{output}"
|
|
137
|
-
expect(output).to include("content")
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
end
|