-
Notifications
You must be signed in to change notification settings - Fork 3
feat: Add RSpec test suite and multi-version Ruby CI support #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,3 +5,7 @@ server/psk | |
| .ruby-version | ||
| .idea | ||
| Gemfile.lock | ||
|
|
||
| # Test artifacts | ||
| spec/examples.txt | ||
| coverage/ | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| --require spec_helper | ||
| --color | ||
| --format documentation |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ AllCops: | |
| NewCops: enable | ||
| Exclude: | ||
| - lib/**/* | ||
| - vendor/**/* | ||
|
|
||
| Metrics/BlockLength: | ||
| Exclude: | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| # CLAUDE.md | ||
|
|
||
| This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. | ||
|
|
||
| ## Project Overview | ||
|
|
||
| Deploy Agent is a Ruby gem that creates a secure proxy allowing DeployHQ to forward connections to protected servers. It establishes a TLS connection to DeployHQ's servers and proxies connections to allowed destinations based on an IP/network allowlist. | ||
|
|
||
| ## Development Commands | ||
|
|
||
| **Install dependencies:** | ||
| ```bash | ||
| bundle install | ||
| ``` | ||
|
|
||
| **Run all tests:** | ||
| ```bash | ||
| bundle exec rspec | ||
| ``` | ||
|
|
||
| **Run linter:** | ||
| ```bash | ||
| bundle exec rubocop | ||
| ``` | ||
|
|
||
| **Run both tests and linter:** | ||
| ```bash | ||
| bundle exec rake | ||
| ``` | ||
|
|
||
| **Build gem locally:** | ||
| ```bash | ||
| gem build deploy-agent.gemspec | ||
| ``` | ||
|
|
||
| **Install gem locally for testing:** | ||
| ```bash | ||
| gem install ./deploy-agent-*.gem | ||
| ``` | ||
|
|
||
| **Test agent commands:** | ||
| ```bash | ||
| deploy-agent setup # Configure agent with certificate and access list | ||
| deploy-agent run # Run in foreground | ||
| deploy-agent start # Start as background daemon | ||
| deploy-agent stop # Stop background daemon | ||
| deploy-agent status # Check daemon status | ||
| deploy-agent accesslist # View allowed destinations | ||
| ``` | ||
|
|
||
| ## Architecture | ||
|
|
||
| ### Core Components | ||
|
|
||
| The agent uses an event-driven, non-blocking I/O architecture with NIO4r for socket multiplexing: | ||
|
|
||
| **DeployAgent::Agent** (`lib/deploy_agent/agent.rb`) | ||
| - Main event loop using NIO::Selector and Timers::Group | ||
| - Manages lifecycle of server and destination connections | ||
| - Handles retries and error recovery | ||
| - Configures logging (STDOUT or file-based when backgrounded) | ||
|
|
||
| **DeployAgent::ServerConnection** (`lib/deploy_agent/server_connection.rb`) | ||
| - Maintains secure TLS connection to DeployHQ's control server (port 7777) | ||
| - Uses mutual TLS authentication with client certificates | ||
| - Processes binary protocol packets (connection requests, data transfer, keepalive, shutdown) | ||
| - Enforces destination access control via allowlist | ||
| - Manages multiple concurrent destination connections by ID | ||
|
|
||
| **DeployAgent::DestinationConnection** (`lib/deploy_agent/destination_connection.rb`) | ||
| - Handles non-blocking connections to backend servers (the actual deployment targets) | ||
| - Implements asynchronous connect with status tracking (:connecting, :connected) | ||
| - Bidirectional data proxying between DeployHQ and destination | ||
| - Reports connection status and errors back to ServerConnection | ||
|
|
||
| **DeployAgent::CLI** (`lib/deploy_agent/cli.rb`) | ||
| - Command-line interface using OptionParser | ||
| - Daemon process management (start/stop/restart/status) | ||
| - PID file management at `~/.deploy/agent.pid` | ||
|
|
||
| **DeployAgent::ConfigurationGenerator** (`lib/deploy_agent/configuration_generator.rb`) | ||
| - Interactive setup wizard for initial configuration | ||
| - Generates certificate via DeployHQ API | ||
| - Creates access list file with allowed destinations | ||
|
|
||
| ### Binary Protocol | ||
|
|
||
| ServerConnection implements a length-prefixed binary protocol: | ||
| - Packet format: 2-byte length (network byte order) + packet data | ||
| - Packet types identified by first byte: connection request (1), connection response (2), close (3), data (4), shutdown (5), reconnect (6), keepalive (7) | ||
| - Connection IDs (2-byte unsigned) track multiple simultaneous proxied connections | ||
|
|
||
| ### Configuration Files | ||
|
|
||
| All configuration stored in `~/.deploy/`: | ||
| - `agent.crt` - Client certificate for TLS authentication | ||
| - `agent.key` - Private key for client certificate | ||
| - `agent.access` - Newline-separated list of allowed IPs/networks (CIDR format) | ||
| - `agent.pid` - Process ID when running as daemon | ||
| - `agent.log` - Log file when running as daemon | ||
|
|
||
| ### Security Model | ||
|
|
||
| - Mutual TLS: Both agent and server authenticate with certificates | ||
| - CA verification: Agent verifies server certificate against bundled `ca.crt` | ||
| - Allowlist enforcement: Only connections to explicitly permitted destinations are allowed | ||
| - IP/CIDR matching: Supports individual IPs and network ranges in access list | ||
|
|
||
| ## Release Process | ||
|
|
||
| This project uses [release-please](https://github.com/googleapis/release-please) for automated releases. Follow [Conventional Commits](https://www.conventionalcommits.org/) specification for all commits: | ||
|
|
||
| - `fix:` patches (1.0.x) | ||
| - `feat:` minor versions (1.x.0) | ||
| - `!` or `BREAKING CHANGE:` major versions (x.0.0) | ||
|
|
||
| Example commit messages: | ||
| ``` | ||
| fix: Prevent connection leak on destination timeout | ||
| feat: Add support for IPv6 destinations | ||
| feat!: Change default server port to 7778 | ||
| ``` | ||
|
|
||
| ## Code Style | ||
|
|
||
| Ruby 2.7+ syntax required. RuboCop configured with: | ||
| - Single quotes for strings | ||
| - Frozen string literals enabled | ||
| - Symbol arrays with brackets | ||
| - Empty lines around class bodies | ||
| - No documentation requirement (Style/Documentation disabled) | ||
| - `lib/` directory excluded from most metrics | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,4 +4,9 @@ source 'https://rubygems.org' | |
|
|
||
| gemspec | ||
|
|
||
| gem 'rake' | ||
| gem 'rubocop' | ||
|
|
||
| group :test do | ||
| gem 'rspec', '~> 3.13' | ||
| end | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require 'bundler/gem_tasks' | ||
| require 'rspec/core/rake_task' | ||
| require 'rubocop/rake_task' | ||
|
|
||
| RSpec::Core::RakeTask.new(:spec) | ||
| RuboCop::RakeTask.new | ||
|
|
||
| task default: [:rubocop, :spec] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require 'spec_helper' | ||
|
|
||
| RSpec.describe DeployAgent::CLI do | ||
| let(:cli) { described_class.new } | ||
|
|
||
| describe '#dispatch' do | ||
| it 'responds to version command' do | ||
| expect { cli.dispatch(['version']) }.to output(/\d+\.\d+\.\d+/).to_stdout | ||
| end | ||
|
|
||
| it 'shows usage for invalid command' do | ||
| expect { cli.dispatch(['invalid']) }.to output(/Usage: deploy-agent/).to_stdout | ||
| end | ||
|
|
||
| it 'shows usage when no arguments provided' do | ||
| expect { cli.dispatch([]) }.to output(/Usage: deploy-agent/).to_stdout | ||
| end | ||
| end | ||
|
|
||
| describe '#version' do | ||
| it 'outputs the version number' do | ||
| expect { cli.version }.to output("#{DeployAgent::VERSION}\n").to_stdout | ||
| end | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require 'spec_helper' | ||
|
|
||
| RSpec.describe DeployAgent::VERSION do | ||
| it 'is defined' do | ||
| expect(DeployAgent::VERSION).not_to be_nil | ||
| end | ||
|
|
||
| it 'has a valid semantic version format' do | ||
| expect(DeployAgent::VERSION).to match(/\A\d+\.\d+\.\d+\z/) | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require 'spec_helper' | ||
|
|
||
| RSpec.describe DeployAgent do | ||
| describe 'module constants' do | ||
| it 'defines CONFIG_PATH' do | ||
| expect(DeployAgent::CONFIG_PATH).to eq(File.expand_path('~/.deploy')) | ||
| end | ||
|
|
||
| it 'defines CERTIFICATE_PATH' do | ||
| expect(DeployAgent::CERTIFICATE_PATH).to eq(File.expand_path('~/.deploy/agent.crt')) | ||
| end | ||
|
|
||
| it 'defines KEY_PATH' do | ||
| expect(DeployAgent::KEY_PATH).to eq(File.expand_path('~/.deploy/agent.key')) | ||
| end | ||
|
|
||
| it 'defines PID_PATH' do | ||
| expect(DeployAgent::PID_PATH).to eq(File.expand_path('~/.deploy/agent.pid')) | ||
| end | ||
|
|
||
| it 'defines LOG_PATH' do | ||
| expect(DeployAgent::LOG_PATH).to eq(File.expand_path('~/.deploy/agent.log')) | ||
| end | ||
|
|
||
| it 'defines ACCESS_PATH' do | ||
| expect(DeployAgent::ACCESS_PATH).to eq(File.expand_path('~/.deploy/agent.access')) | ||
| end | ||
| end | ||
|
|
||
| describe '.allowed_destinations' do | ||
| let(:temp_access_file) { "/tmp/test_access_#{Process.pid}" } | ||
|
|
||
| before do | ||
| stub_const('DeployAgent::ACCESS_PATH', temp_access_file) | ||
| end | ||
|
|
||
| after do | ||
| FileUtils.rm_f(temp_access_file) | ||
| end | ||
|
|
||
| it 'reads destinations from access file' do | ||
| File.write(temp_access_file, "127.0.0.1\n192.168.1.0/24\n") | ||
| expect(DeployAgent.allowed_destinations).to eq(['127.0.0.1', '192.168.1.0/24']) | ||
| end | ||
|
|
||
| it 'strips whitespace from destinations' do | ||
| File.write(temp_access_file, " 127.0.0.1 \n 192.168.1.1 \n") | ||
| expect(DeployAgent.allowed_destinations).to eq(['127.0.0.1', '192.168.1.1']) | ||
| end | ||
|
|
||
| it 'ignores empty lines' do | ||
| File.write(temp_access_file, "127.0.0.1\n\n192.168.1.1\n") | ||
| expect(DeployAgent.allowed_destinations).to eq(['127.0.0.1', '192.168.1.1']) | ||
| end | ||
|
|
||
| it 'ignores comment lines' do | ||
| File.write(temp_access_file, "# Comment\n127.0.0.1\n# Another comment\n192.168.1.1\n") | ||
| expect(DeployAgent.allowed_destinations).to eq(['127.0.0.1', '192.168.1.1']) | ||
| end | ||
|
|
||
| it 'extracts only the first field from each line' do | ||
| File.write(temp_access_file, "127.0.0.1 localhost\n192.168.1.1 description\n") | ||
| expect(DeployAgent.allowed_destinations).to eq(['127.0.0.1', '192.168.1.1']) | ||
| end | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require 'deploy_agent' | ||
|
|
||
| RSpec.configure do |config| | ||
| config.expect_with :rspec do |expectations| | ||
| expectations.include_chain_clauses_in_custom_matcher_descriptions = true | ||
| end | ||
|
|
||
| config.mock_with :rspec do |mocks| | ||
| mocks.verify_partial_doubles = true | ||
| end | ||
|
|
||
| config.shared_context_metadata_behavior = :apply_to_host_groups | ||
| config.filter_run_when_matching :focus | ||
| config.example_status_persistence_file_path = 'spec/examples.txt' | ||
| config.disable_monkey_patching! | ||
| config.warnings = true | ||
|
|
||
| config.default_formatter = 'doc' if config.files_to_run.one? | ||
|
|
||
| config.order = :random | ||
| Kernel.srand config.seed | ||
| end |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.