diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ea141b7 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +CLOUDFLARE_EMAIL='your_cloudlare_email' +CLOUDFLARE_KEY='your_cloudflare_api_key' +CLOUDFLARE_TEST_ZONE_MANAGEMENT=true diff --git a/gems.rb b/gems.rb index 7859daf..5ff9915 100644 --- a/gems.rb +++ b/gems.rb @@ -11,23 +11,26 @@ gemspec group :maintenance, optional: true do - gem "bake-gem" - gem "bake-modernize" - - gem "utopia-project" + gem "bake-gem" + gem "bake-modernize" + + gem "utopia-project" end group :test do - gem "sus" - gem "covered" - gem "decode" - gem "rubocop" - - gem "sus-fixtures-async" - - gem "sinatra" - gem "webmock" - - gem "bake-test" - gem "bake-test-external" + gem "sus" + gem "covered" + gem "decode" + gem "rubocop" + + gem "sus-fixtures-async" + + gem "sinatra" + gem "webmock" + + gem "bake-test" + gem "bake-test-external" + + gem "dotenv" + gem "pry" end diff --git a/lib/cloudflare/dns.rb b/lib/cloudflare/dns.rb index f5e24cb..9a39cde 100644 --- a/lib/cloudflare/dns.rb +++ b/lib/cloudflare/dns.rb @@ -30,7 +30,18 @@ def update_content(content, **options) self end end - + + def update(type: nil, name: nil, content: nil, **options) + response = put( + type: type || @record[:type], + name: name || @record[:name], + content: content || @record[:content], + **options + ) + + @value = response.result + end + def type result[:type] end diff --git a/readme.md b/readme.md index 9181b8d..794dc42 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ # Cloudflare -It is a Ruby wrapper for the Cloudflare V4 API. It provides a light weight wrapper using `RestClient::Resource`. The wrapper functionality is limited to zones and DNS records at this time, *PRs welcome*. +It is a Ruby wrapper for the Cloudflare V4 API. It provides a light weight wrapper using `RestClient::Resource`. The wrapper functionality is limited to zones and DNS records at this time, _PRs welcome_. [![Development Status](https://github.com/socketry/cloudflare/workflows/Test/badge.svg)](https://github.com/socketry/cloudflare/actions?workflow=Test) @@ -8,7 +8,7 @@ It is a Ruby wrapper for the Cloudflare V4 API. It provides a light weight wrapp Add this line to your application's Gemfile: -``` ruby +```ruby gem 'cloudflare' ``` @@ -24,7 +24,7 @@ Or install it yourself as: Here are some basic examples. For more details, refer to the code and specs. -``` ruby +```ruby require 'cloudflare' # Grab some details from somewhere: @@ -34,25 +34,30 @@ key = ENV['CLOUDFLARE_KEY'] Cloudflare.connect(key: key, email: email) do |connection| # Get all available zones: zones = connection.zones - + # Get a specific zone: zone = connection.zones.find_by_id("...") zone = connection.zones.find_by_name("example.com") - + # Get DNS records for a given zone: dns_records = zone.dns_records - + # Show some details of the DNS record: dns_record = dns_records.first puts dns_record.name - + # Add a DNS record. Here we add an A record for `batman.example.com`: zone = zones.find_by_name("example.com") zone.dns_records.create('A', 'batman', '1.2.3.4', proxied: false) - + + # Update a DNS record. Here we update the A record above to be a CNAME record to 'nairobi.kanairo.com' + record = zone.dns_records.find_by_name("example.com") } + record.update(type: "CNAME", name: "nairobi", content: "kanairo.com", proxied: true) + + # Get firewall rules: all_rules = zone.firewall_rules - + # Block an ip: rule = zone.firewall_rules.set('block', '1.2.3.4', notes: "ssh dictionary attack") end @@ -62,7 +67,7 @@ end You can read more about [bearer tokens here](https://blog.cloudflare.com/api-tokens-general-availability/). This allows you to limit priviledges. -``` ruby +```ruby require 'cloudflare' token = 'a_generated_api_token' @@ -74,10 +79,10 @@ end ### Using with Async -``` ruby +```ruby Async do connection = Cloudflare.connect(...) - + # ... do something with connection ... ensure connection.close @@ -90,9 +95,10 @@ We welcome contributions to this project. 1. Fork it. 2. Create your feature branch (`git checkout -b my-new-feature`). -3. Commit your changes (`git commit -am 'Add some feature'`). -4. Push to the branch (`git push origin my-new-feature`). -5. Create new Pull Request. +3. Run `cp .env.example .env` to create a .env file and populate it with the required environment variables. Edit the new file appropriately +4. Commit your changes (`git commit -am 'Add some feature'`). +5. Push to the branch (`git push origin my-new-feature`). +6. Create new Pull Request. ### Developer Certificate of Origin @@ -104,5 +110,5 @@ This project is best served by a collaborative and respectful environment. Treat ## See Also - - [Cloudflare::DNS::Update](https://github.com/ioquatix/cloudflare-dns-update) - A dynamic DNS updater based on this gem. - - [Rubyflare](https://github.com/trev/rubyflare) - Another implementation. +- [Cloudflare::DNS::Update](https://github.com/ioquatix/cloudflare-dns-update) - A dynamic DNS updater based on this gem. +- [Rubyflare](https://github.com/trev/rubyflare) - Another implementation. diff --git a/spec/cloudflare/dns_spec.rb b/spec/cloudflare/dns_spec.rb new file mode 100644 index 0000000..302fb59 --- /dev/null +++ b/spec/cloudflare/dns_spec.rb @@ -0,0 +1,73 @@ + +require 'cloudflare/rspec/connection' + +RSpec.describe Cloudflare::DNS, order: :defined, timeout: 30 do + include_context Cloudflare::Zone + + let(:subdomain) {"www-#{job_id}"} + + after do + if defined? @record + expect(@record.delete).to be_success + end + end + + describe "#create" do + it "can create dns record" do + @record = zone.dns_records.create("A", subdomain, "1.2.3.4") + expect(@record.type).to be == "A" + expect(@record.name).to be_start_with subdomain + expect(@record.content).to be == "1.2.3.4" + end + + it "can create dns record with proxied option" do + @record = zone.dns_records.create("A", subdomain, "1.2.3.4", proxied: true) + expect(@record.type).to be == "A" + expect(@record.name).to be_start_with subdomain + expect(@record.content).to be == "1.2.3.4" + expect(@record.proxied).to be_truthy + end + end + + describe "#update_content" do + let(:record) {@record = zone.dns_records.create("A", subdomain, "1.2.3.4")} + + it "can update dns content" do + record.update_content("4.3.2.1") + expect(record.content).to be == "4.3.2.1" + + fetched_record = zone.dns_records.find_by_name(record.name) + expect(fetched_record.content).to be == record.content + end + + it "can update dns content with proxied option" do + record.update_content("4.3.2.1", proxied: true) + expect(record.proxied).to be_truthy + + fetched_record = zone.dns_records.find_by_name(record.name) + expect(fetched_record.proxied).to be_truthy + end + end + + describe "#update" do + let(:subject) { record.update(**new_params)} + + let(:record) { @record = zone.dns_records.create("A", "old", "1.2.3.4", proxied: false) } + + let(:new_params) do + { + type: "CNAME", + name: "new", + content: "example.com", + proxied: true + } + end + + it "can update dns record" do + expect { subject }.to change { record.name }.to("#{new_params[:name]}.#{zone.name}") + .and change { record.type }.to(new_params[:type]) + .and change { record.content }.to(new_params[:content]) + .and change { record.proxied }.to(new_params[:proxied]) + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..e07d884 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'dotenv/load' + +AUTH_EMAIL = ENV['CLOUDFLARE_EMAIL'] +AUTH_KEY = ENV['CLOUDFLARE_KEY'] + +if AUTH_EMAIL.nil? || AUTH_EMAIL.empty? || AUTH_KEY.nil? || AUTH_KEY.empty? + puts 'Please make sure you have defined CLOUDFLARE_EMAIL and CLOUDFLARE_KEY in your environment' + puts 'You can also specify CLOUDFLARE_ZONE_NAME to test with your own zone and' + puts 'CLOUDFLARE_ACCOUNT_ID to use a specific account' + exit(1) +end + +ACCOUNT_ID = ENV['CLOUDFLARE_ACCOUNT_ID'] +NAMES = %w{alligator ant bear bee bird camel cat cheetah chicken chimpanzee cow crocodile deer dog dolphin duck eagle elephant fish fly fox frog giraffe goat goldfish hamster hippopotamus horse kangaroo kitten lion lobster monkey octopus owl panda pig puppy rabbit rat scorpion seal shark sheep snail snake spider squirrel tiger turtle wolf zebra} +JOB_ID = ENV.fetch('INVOCATION_ID', 'testing').hash +ZONE_NAME = ENV['CLOUDFLARE_ZONE_NAME'] || "#{NAMES[JOB_ID % NAMES.size]}.com" + +$stderr.puts "Using zone name: #{ZONE_NAME}" + +require 'covered/rspec' +require 'async/rspec' +require 'pry' + +require 'cloudflare/rspec/connection' +require 'cloudflare/zones' + +Dir[File.expand_path('../support/**/*.rb', __FILE__)].each{|path| require path} + +RSpec.configure do |config| + # Enable flags like --only-failures and --next-failure + config.example_status_persistence_file_path = '.rspec_status' + + config.expect_with :rspec do |c| + c.syntax = :expect + end + + disabled_specs = {} + + # Check for features the current account has enabled + Cloudflare.connect(key: AUTH_KEY, email: AUTH_EMAIL) do |conn| + begin + account = if ACCOUNT_ID + conn.accounts.find_by_id(ACCOUNT_ID) + else + conn.accounts.first + end + account.kv_namespaces.to_a + rescue Cloudflare::RequestError => e + if e.message.include?('your account is not entitled') + puts 'Disabling KV specs due to no access' + disabled_specs[:kv_spec] = true + else + raise + end + end + end + + config.filter_run_excluding disabled_specs unless disabled_specs.empty? +end diff --git a/spec/support/cloudflare/account.rb b/spec/support/cloudflare/account.rb new file mode 100644 index 0000000..88e56a0 --- /dev/null +++ b/spec/support/cloudflare/account.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +RSpec.shared_context Cloudflare::Account do + include_context Cloudflare::RSpec::Connection + + let(:account) do + if ACCOUNT_ID + connection.accounts.find_by_id(ACCOUNT_ID) + else + connection.accounts.first + end + end +end diff --git a/spec/support/cloudflare/zone.rb b/spec/support/cloudflare/zone.rb new file mode 100644 index 0000000..07b194a --- /dev/null +++ b/spec/support/cloudflare/zone.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +RSpec.shared_context Cloudflare::Zone do + include_context Cloudflare::Account + + let(:job_id) { JOB_ID } + let(:names) { NAMES.dup } + let(:name) { ZONE_NAME.dup } + + let(:zones) { connection.zones } + + let(:zone) { @zone = zones.find_by_name(name) || zones.create(name, account) } + + after do + @zone.delete if defined? @zone + end +end diff --git a/test/cloudflare/dns.rb b/test/cloudflare/dns.rb index 7f3bf45..c408fa1 100644 --- a/test/cloudflare/dns.rb +++ b/test/cloudflare/dns.rb @@ -9,53 +9,75 @@ describe Cloudflare::DNS do include_context Cloudflare::AConnection - - let(:subdomain) {"www-#{job_id || SecureRandom.hex(4)}"} - + + let(:subdomain) { "www-#{job_id || SecureRandom.hex(4)}" } + with "new record" do it "can create dns record" do record = zone.dns_records.create("A", subdomain, "1.2.3.4") - + expect(record.type).to be == "A" expect(record.name).to be(:start_with?, subdomain) expect(record.content).to be == "1.2.3.4" - ensure - record&.delete + ensure + record&.delete end - + it "can create dns record with proxied option" do record = zone.dns_records.create("A", subdomain, "1.2.3.4", proxied: true) - + expect(record.type).to be == "A" expect(record.name).to be(:start_with?, subdomain) expect(record.content).to be == "1.2.3.4" expect(record.proxied).to be_truthy - ensure - record&.delete + ensure + record&.delete end end - + with "existing record" do - let(:record) {zone.dns_records.create("A", subdomain, "1.2.3.4")} - + let(:record) { zone.dns_records.create("A", subdomain, "1.2.3.4") } + after do @record&.delete end - + it "can update dns content" do record.update_content("4.3.2.1") expect(record.content).to be == "4.3.2.1" - + fetched_record = zone.dns_records.find_by_name(record.name) expect(fetched_record.content).to be == record.content end - + it "can update dns content with proxied option" do record.update_content("4.3.2.1", proxied: true) expect(record).to be(:proxied?) - + fetched_record = zone.dns_records.find_by_name(record.name) expect(fetched_record).to be(:proxied?) end end + + describe "#update" do + let(:subject) { record.update(**new_params) } + + let(:record) { @record = zone.dns_records.create("A", "old", "1.2.3.4", proxied: false) } + + let(:new_params) do + { + type: "CNAME", + name: "new", + content: "example.com", + proxied: true + } + end + + it "can update dns record" do + expect { subject }.to change { record.name }.to("#{new_params[:name]}.#{zone.name}") + .and change { record.type }.to(new_params[:type]) + .and change { record.content }.to(new_params[:content]) + .and change { record.proxied }.to(new_params[:proxied]) + end + end end