porkbun 1.0.0 → 2.0.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/AGENTS.md +41 -0
- data/CHANGELOG.md +20 -0
- data/CLAUDE.md +1 -0
- data/Gemfile +5 -7
- data/Gemfile.lock +36 -54
- data/README.md +67 -19
- data/Rakefile +4 -1
- data/bin/porkbun +61 -69
- data/justfile +8 -0
- data/lib/porkbun/version.rb +1 -1
- data/lib/porkbun.rb +234 -7
- data/mise.toml +2 -0
- metadata +11 -20
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0d5e8faabfe0bc6179bdb965bb3d2150e40a8ef4be37d70e16ea625b6ed1d653
|
|
4
|
+
data.tar.gz: 1044c9005c5ebbe24ac2c2478e70d87638ff68d034c05b78baa26913de8b15a8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 352e4fe4edb99376d3965018027d4b06e6881d0bb9f20b2e34f0d41c210b2239fa79807692d3a554ebcab03f24d223299da1cf1acd140c64ff0dfdfbfdb56b43
|
|
7
|
+
data.tar.gz: bae37bcc7ee6640c7e3bab4576abfa1d97b092dea3041677cb70414e47b472ec193db9033c85dd1a5d4151a7785c391c07e3aa26c9310fbe6014237cf156885a
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Porkbun Ruby Agent Guide
|
|
2
|
+
|
|
3
|
+
This repository contains a Ruby gem and CLI for the Porkbun API. The project is a partial implementation focused on the author's needs.
|
|
4
|
+
|
|
5
|
+
## Rules
|
|
6
|
+
|
|
7
|
+
- Read the relevant Ruby source and tests before changing behavior.
|
|
8
|
+
- Implement behavior in the local library API (`lib/`), not in the CLI. Keep CLI commands as thin argument and output adapters.
|
|
9
|
+
- Keep changes focused. Do not add speculative features or refactors.
|
|
10
|
+
- Add or update RSpec coverage for behavior changes.
|
|
11
|
+
- Run the test suite after every code modification and before every commit. Report the result.
|
|
12
|
+
- Update `README.md` every time you change code. Keep it in sync with the current API, CLI, and usage after every code modification; do not defer documentation updates. Update `CHANGELOG.md` with every user-facing change.
|
|
13
|
+
- Before bumping any dependency or project version, actually check the current latest release in an authoritative package registry. Do not rely on memory, an old lockfile, or an assumed version.
|
|
14
|
+
- Treat API and CLI renames as breaking changes. Update the major version when they remove or rename public behavior.
|
|
15
|
+
- Absolutely never commit, push, or change shared Git state unless the user explicitly instructs you to do so.
|
|
16
|
+
|
|
17
|
+
## Structure
|
|
18
|
+
|
|
19
|
+
- `lib/` — gem implementation.
|
|
20
|
+
- `bin/` — executable scripts, including the CLI.
|
|
21
|
+
- `spec/` — RSpec tests.
|
|
22
|
+
- `README.md` — usage, API, CLI, and development documentation.
|
|
23
|
+
- `.github/workflows/` — CI configuration.
|
|
24
|
+
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
27
|
+
- Install dependencies: `bin/setup`
|
|
28
|
+
- Run tests: `bundle exec rake` or `bundle exec rspec`
|
|
29
|
+
- Open a console: `bin/console`
|
|
30
|
+
- Install the gem locally: `bundle exec rake install`
|
|
31
|
+
- Release the gem: `bundle exec rake release`
|
|
32
|
+
|
|
33
|
+
## Documentation routing
|
|
34
|
+
|
|
35
|
+
| Topic | Document |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| Usage, API, CLI, development, and contributing | [README.md](README.md) |
|
|
38
|
+
|
|
39
|
+
## Documentation and changelog
|
|
40
|
+
|
|
41
|
+
Update the relevant documentation and `CHANGELOG.md` with each user-facing change. Keep entries short and factual. If a change needs a design note, explain the decision and its impact in the relevant documentation.
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
|
|
5
|
+
## [2.0.0] - 2026-09-18
|
|
6
|
+
|
|
7
|
+
- Removed the CLI `list` and `retrieve` commands and replaced them with the single `ls` command for listing domains or retrieving records for a domain and optional record ID.
|
|
8
|
+
- Renamed the CLI `create` command to `add` and `delete_all` to `rm_rf`.
|
|
9
|
+
- Added the `rm` CLI command for deleting one unambiguous record by hostname, with or without a trailing dot.
|
|
10
|
+
- Added the `update` CLI command, with `up` as an alias, to change record content or TTL.
|
|
11
|
+
- Replaced the public `Porkbun::DNS.retrieve` API with `Porkbun::Domain.list`.
|
|
12
|
+
- Added `Porkbun.new(domain)` domain objects and `Porkbun::Record` objects for record retrieval, creation, update, and deletion.
|
|
13
|
+
- Added `Porkbun::Domain#zone_file`, `Porkbun::Domain.import`, and explicit `get_record` naming.
|
|
14
|
+
- Added RSpec coverage for `Porkbun::Domain.list`.
|
|
15
|
+
- Updated Thor to `1.5.0`.
|
|
16
|
+
- Fixed `--help` handling for CLI commands.
|
|
17
|
+
- Added project guidance in `AGENTS.md` and `CLAUDE.md`.
|
|
18
|
+
- Removed the external `http` dependency in favor of Ruby's standard library.
|
|
19
|
+
- Raised the minimum supported Ruby version to `3.2.0`.
|
|
20
|
+
- Updated development dependencies to their latest compatible releases.
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
data/Gemfile
CHANGED
|
@@ -5,11 +5,9 @@ source 'https://rubygems.org'
|
|
|
5
5
|
# Specify your gem's dependencies in porkbun.gemspec
|
|
6
6
|
gemspec
|
|
7
7
|
|
|
8
|
-
group :development do
|
|
9
|
-
|
|
10
|
-
gem 'pry', '~> 0.
|
|
11
|
-
gem 'rspec', '~> 3.
|
|
12
|
-
gem 'webmock', '~> 3.
|
|
8
|
+
group :development, :test do
|
|
9
|
+
gem 'rake', '~> 13.4'
|
|
10
|
+
gem 'pry', '~> 0.16'
|
|
11
|
+
gem 'rspec', '~> 3.13'
|
|
12
|
+
gem 'webmock', '~> 3.26'
|
|
13
13
|
end
|
|
14
|
-
|
|
15
|
-
gem 'thor', '~> 1.2'
|
data/Gemfile.lock
CHANGED
|
@@ -1,78 +1,60 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
porkbun (
|
|
5
|
-
|
|
6
|
-
thor (~> 1.2)
|
|
4
|
+
porkbun (2.0.0)
|
|
5
|
+
thor (~> 1.5)
|
|
7
6
|
|
|
8
7
|
GEM
|
|
9
8
|
remote: https://rubygems.org/
|
|
10
9
|
specs:
|
|
11
|
-
addressable (2.
|
|
12
|
-
public_suffix (>= 2.0.2, <
|
|
13
|
-
|
|
10
|
+
addressable (2.9.0)
|
|
11
|
+
public_suffix (>= 2.0.2, < 8.0)
|
|
12
|
+
bigdecimal (4.1.3)
|
|
14
13
|
coderay (1.1.3)
|
|
15
|
-
crack (0.
|
|
14
|
+
crack (1.0.1)
|
|
15
|
+
bigdecimal
|
|
16
16
|
rexml
|
|
17
|
-
diff-lcs (1.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
ffi (>= 1.0.0)
|
|
23
|
-
rake
|
|
24
|
-
hashdiff (1.0.1)
|
|
25
|
-
http (5.2.0)
|
|
26
|
-
addressable (~> 2.8)
|
|
27
|
-
base64 (~> 0.1)
|
|
28
|
-
http-cookie (~> 1.0)
|
|
29
|
-
http-form_data (~> 2.2)
|
|
30
|
-
llhttp-ffi (~> 0.5.0)
|
|
31
|
-
http-cookie (1.0.5)
|
|
32
|
-
domain_name (~> 0.5)
|
|
33
|
-
http-form_data (2.3.0)
|
|
34
|
-
llhttp-ffi (0.5.0)
|
|
35
|
-
ffi-compiler (~> 1.0)
|
|
36
|
-
rake (~> 13.0)
|
|
37
|
-
method_source (1.0.0)
|
|
38
|
-
pry (0.14.2)
|
|
17
|
+
diff-lcs (1.6.2)
|
|
18
|
+
hashdiff (1.2.1)
|
|
19
|
+
io-console (0.9.4)
|
|
20
|
+
method_source (1.1.0)
|
|
21
|
+
pry (0.16.0)
|
|
39
22
|
coderay (~> 1.1)
|
|
40
23
|
method_source (~> 1.0)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
rspec-
|
|
50
|
-
|
|
24
|
+
reline (>= 0.6.0)
|
|
25
|
+
public_suffix (7.0.5)
|
|
26
|
+
rake (13.4.2)
|
|
27
|
+
reline (0.7.0)
|
|
28
|
+
io-console (~> 0.5)
|
|
29
|
+
rexml (3.4.4)
|
|
30
|
+
rspec (3.13.2)
|
|
31
|
+
rspec-core (~> 3.13.0)
|
|
32
|
+
rspec-expectations (~> 3.13.0)
|
|
33
|
+
rspec-mocks (~> 3.13.0)
|
|
34
|
+
rspec-core (3.13.6)
|
|
35
|
+
rspec-support (~> 3.13.0)
|
|
36
|
+
rspec-expectations (3.13.5)
|
|
51
37
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
52
|
-
rspec-support (~> 3.
|
|
53
|
-
rspec-mocks (3.
|
|
38
|
+
rspec-support (~> 3.13.0)
|
|
39
|
+
rspec-mocks (3.13.8)
|
|
54
40
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
55
|
-
rspec-support (~> 3.
|
|
56
|
-
rspec-support (3.
|
|
57
|
-
thor (1.
|
|
58
|
-
|
|
59
|
-
unf_ext
|
|
60
|
-
unf_ext (0.0.8.2)
|
|
61
|
-
webmock (3.19.1)
|
|
41
|
+
rspec-support (~> 3.13.0)
|
|
42
|
+
rspec-support (3.13.7)
|
|
43
|
+
thor (1.5.0)
|
|
44
|
+
webmock (3.26.4)
|
|
62
45
|
addressable (>= 2.8.0)
|
|
63
46
|
crack (>= 0.3.2)
|
|
64
47
|
hashdiff (>= 0.4.0, < 2.0.0)
|
|
65
48
|
|
|
66
49
|
PLATFORMS
|
|
67
|
-
|
|
68
|
-
arm64-darwin-23
|
|
50
|
+
ruby
|
|
69
51
|
|
|
70
52
|
DEPENDENCIES
|
|
71
53
|
porkbun!
|
|
72
|
-
pry (~> 0.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
webmock (~> 3.
|
|
54
|
+
pry (~> 0.16)
|
|
55
|
+
rake (~> 13.4)
|
|
56
|
+
rspec (~> 3.13)
|
|
57
|
+
webmock (~> 3.26)
|
|
76
58
|
|
|
77
59
|
BUNDLED WITH
|
|
78
60
|
2.4.12
|
data/README.md
CHANGED
|
@@ -14,11 +14,18 @@ need a specific call to be implemented, let me know, or submit a PR
|
|
|
14
14
|
```ruby
|
|
15
15
|
ENV['PORKBUN_API_KEY'] = 'YOUR_API_KEY'
|
|
16
16
|
ENV['PORKBUN_SECRET_API_KEY'] = 'YOUR_SECRET_API_KEY'
|
|
17
|
-
|
|
17
|
+
domain = Porkbun.new('domain.org')
|
|
18
|
+
|
|
19
|
+
domain.records.each { |record| puts record.to_s }
|
|
20
|
+
|
|
21
|
+
record = domain.create_record('test',
|
|
18
22
|
type: 'A',
|
|
19
23
|
content: '1.1.1.1',
|
|
20
24
|
ttl: 300
|
|
21
25
|
)
|
|
26
|
+
record.update(content: '8.8.8.8')
|
|
27
|
+
record.delete
|
|
28
|
+
|
|
22
29
|
```
|
|
23
30
|
|
|
24
31
|
## API
|
|
@@ -27,22 +34,60 @@ record = Porkbun::DNS.create(name: 'test',
|
|
|
27
34
|
|
|
28
35
|
Make sure your keys are good.
|
|
29
36
|
|
|
30
|
-
|
|
37
|
+
## Classes
|
|
38
|
+
|
|
39
|
+
### `Porkbun::Domain`
|
|
40
|
+
|
|
41
|
+
#### class methods
|
|
31
42
|
|
|
32
|
-
|
|
43
|
+
- `all` - returns an array of `Porkbun::Domain` objects for each domain in the account.
|
|
44
|
+
- `create_record(hostname, options)` - returns a `Porkbun::Record` created from a full hostname.
|
|
45
|
+
- `get_record(hostname)` - returns one unambiguous `Porkbun::Record`.
|
|
46
|
+
- `import(file)` - returns an array of `Porkbun::Record` objects created from a zone file.
|
|
47
|
+
- `update_dynamic(hostname, ip)` - returns a hash describing the created or updated record.
|
|
33
48
|
|
|
34
|
-
|
|
49
|
+
```ruby
|
|
50
|
+
Porkbun::Domain.all.each { |domain| puts domain }
|
|
51
|
+
record = Porkbun::Domain.get_record('www.domain.org')
|
|
52
|
+
record.update(content: '8.8.8.8')
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
#### instance methods
|
|
35
56
|
|
|
36
|
-
|
|
57
|
+
`Porkbun.new('domain.org')` returns a `Porkbun::Domain` object.
|
|
37
58
|
|
|
38
|
-
|
|
39
|
-
- `name` -
|
|
40
|
-
- `
|
|
41
|
-
- `
|
|
42
|
-
- `
|
|
43
|
-
|
|
59
|
+
- `records` - returns an array of `Porkbun::Record` objects for the domain.
|
|
60
|
+
- `get_record(name)` - returns one unambiguous `Porkbun::Record`; `name` can be relative or fully qualified.
|
|
61
|
+
- `create_record(name, options)` - returns a new `Porkbun::Record`.
|
|
62
|
+
- `delete_all_records` - returns an array of `Porkbun::Record` objects after deleting all non-NS records.
|
|
63
|
+
- `zone_file` - returns a string that can be passed to `import`.
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
domain = Porkbun.new('domain.org')
|
|
67
|
+
record = domain.get_record('www')
|
|
68
|
+
record.update(content: '8.8.8.8')
|
|
69
|
+
record.delete
|
|
70
|
+
|
|
71
|
+
domain.delete_all_records
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### `Porkbun::Record`
|
|
75
|
+
|
|
76
|
+
A `Porkbun::Record` represents one DNS record and owns its mutations.
|
|
77
|
+
|
|
78
|
+
#### instance methods
|
|
79
|
+
|
|
80
|
+
- `update(content:, ttl:)` - returns the updated `Porkbun::Record`.
|
|
81
|
+
- `delete` - returns the deleted `Porkbun::Record`.
|
|
82
|
+
- `to_s` - returns a string containing the BIND zone entry.
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
record = domain.get_record('www')
|
|
86
|
+
puts record.to_s
|
|
87
|
+
record.update(ttl: 700)
|
|
88
|
+
record.delete
|
|
89
|
+
```
|
|
44
90
|
|
|
45
|
-
returns instance of DNS which can be used to delete
|
|
46
91
|
|
|
47
92
|
## CLI
|
|
48
93
|
|
|
@@ -50,12 +95,15 @@ The gem also comes with a CLI
|
|
|
50
95
|
|
|
51
96
|
$ porkbun
|
|
52
97
|
Commands:
|
|
53
|
-
porkbun
|
|
54
|
-
porkbun
|
|
55
|
-
porkbun
|
|
56
|
-
porkbun
|
|
57
|
-
porkbun
|
|
58
|
-
porkbun
|
|
98
|
+
porkbun add RECORD --content=CONTENT --type=TYPE # Add a new record
|
|
99
|
+
porkbun update RECORD [--content=CONTENT] [--ttl=TTL] # Update a record (alias: up)
|
|
100
|
+
porkbun rm_rf DOMAIN # deletes all records for a domain. this is destructive. use with caution
|
|
101
|
+
porkbun rm RECORD # Delete one record for a hostname
|
|
102
|
+
porkbun dyndns HOSTNAME [IP] # Update a dynamic dns record. example: porkbun dyndns home.example.com
|
|
103
|
+
porkbun env # Print environment variables
|
|
104
|
+
porkbun help [COMMAND] # Describe available commands or one specific command
|
|
105
|
+
porkbun import FILE # Import BIND zone file
|
|
106
|
+
porkbun ls [DOMAIN] [ID] # List all domains or records for a domain
|
|
59
107
|
|
|
60
108
|
|
|
61
109
|
be sure to set the environmental variables for it to work
|
|
@@ -67,7 +115,7 @@ export PORKBUN_SECRET_API_KEY = YOUR_SECRET_API_KEY
|
|
|
67
115
|
|
|
68
116
|
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
69
117
|
|
|
70
|
-
|
|
118
|
+
Run the test suite with `just test`. To publish a release, update the version number, then run `just publish`. This runs the tests and then calls the supported release task, which creates a git tag, pushes git commits and the tag, and publishes the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
71
119
|
|
|
72
120
|
## Contributing
|
|
73
121
|
|
data/Rakefile
CHANGED
data/bin/porkbun
CHANGED
|
@@ -5,109 +5,101 @@ require 'porkbun'
|
|
|
5
5
|
require 'thor'
|
|
6
6
|
|
|
7
7
|
class CLI < Thor
|
|
8
|
-
|
|
8
|
+
remove_command :tree
|
|
9
|
+
map 'up' => 'update'
|
|
10
|
+
|
|
11
|
+
def self.basename
|
|
12
|
+
'porkbun'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
desc 'import FILE', 'Import BIND zone file'
|
|
9
16
|
def import(file)
|
|
10
|
-
|
|
11
|
-
IO.readlines(file).each do |line|
|
|
12
|
-
match_data = line.match(record_regex)
|
|
13
|
-
next unless match_data
|
|
14
|
-
|
|
15
|
-
domain = match_data[:hostname].split('.')[-2..].join('.').chomp '.'
|
|
16
|
-
name = match_data[:hostname].split('.')[0..-3].join('.')
|
|
17
|
-
|
|
18
|
-
options = {
|
|
19
|
-
domain:,
|
|
20
|
-
name:,
|
|
21
|
-
ttl: match_data[:ttl],
|
|
22
|
-
type: match_data[:type],
|
|
23
|
-
prio: match_data[:priority],
|
|
24
|
-
content: match_data[:content].chomp('.').gsub(/^"|"$/, '')
|
|
25
|
-
}.compact
|
|
26
|
-
|
|
27
|
-
record = Porkbun::DNS.create options
|
|
28
|
-
pp record
|
|
29
|
-
end
|
|
17
|
+
Porkbun::Domain.import(file).each { |record| puts record }
|
|
30
18
|
end
|
|
31
19
|
|
|
32
|
-
desc '
|
|
33
|
-
def
|
|
34
|
-
|
|
35
|
-
puts domain
|
|
20
|
+
desc 'ls [DOMAIN] [ID]', 'List all domains or records for a domain'
|
|
21
|
+
def ls(domain = nil, id = '')
|
|
22
|
+
if domain
|
|
23
|
+
puts Porkbun.new(domain).records
|
|
24
|
+
else
|
|
25
|
+
Porkbun::Domain.all.each do |domain|
|
|
26
|
+
puts domain
|
|
27
|
+
end
|
|
36
28
|
end
|
|
37
29
|
end
|
|
38
30
|
|
|
31
|
+
|
|
39
32
|
desc 'env', 'Print environment variables'
|
|
40
33
|
def env
|
|
41
34
|
puts 'PORKBUN_API_KEY: ' + ENV['PORKBUN_API_KEY'].to_s
|
|
42
|
-
puts '
|
|
35
|
+
puts 'PORKBUN_SECRET_API_KEY: ' + ENV['PORKBUN_SECRET_API_KEY'].to_s
|
|
43
36
|
end
|
|
44
37
|
|
|
45
|
-
desc '
|
|
46
|
-
def
|
|
47
|
-
|
|
48
|
-
records.each do |record|
|
|
49
|
-
next if record.type == 'NS'
|
|
50
|
-
|
|
38
|
+
desc 'rm_rf DOMAIN', 'deletes all records for a domain. this is destructive. use with caution'
|
|
39
|
+
def rm_rf(domain, id = '')
|
|
40
|
+
Porkbun.new(domain).delete_all_records.each do |record|
|
|
51
41
|
puts "DELETE #{record}"
|
|
52
|
-
record.delete
|
|
53
42
|
end
|
|
54
43
|
end
|
|
55
44
|
|
|
56
|
-
desc '
|
|
45
|
+
desc 'rm RECORD', 'Delete one record for a hostname'
|
|
46
|
+
def rm(hostname)
|
|
47
|
+
record = Porkbun::Domain.get_record(hostname)
|
|
48
|
+
record.delete
|
|
49
|
+
puts "DELETE #{record}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
desc 'update RECORD', 'Update a record (alias: up)'
|
|
53
|
+
option :content, desc: 'The new record content'
|
|
54
|
+
option :ttl, type: :numeric, desc: 'The new TTL in seconds'
|
|
55
|
+
def update(hostname)
|
|
56
|
+
raise Thor::Error, 'Specify --content or --ttl' unless options[:content] || options[:ttl]
|
|
57
|
+
|
|
58
|
+
record = Porkbun::Domain.get_record(hostname)
|
|
59
|
+
puts "UPDATE #{record}"
|
|
60
|
+
record.update(options)
|
|
61
|
+
puts record.message
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
desc 'add RECORD', 'Add a new record'
|
|
57
66
|
option :type, required: true, desc: 'The record type: A, AAAA, CNAME, MX, TXT, SRV, NS, SOA'
|
|
58
|
-
option :domain, required: true, desc: 'The domain name: google.com'
|
|
59
|
-
option :name, required: true, desc: 'The hostname: www'
|
|
60
67
|
option :content, required: true, desc: 'The content: hostname, or IP address'
|
|
61
68
|
option :ttl, default: 600, type: :numeric, desc: 'The TTL in seconds'
|
|
62
69
|
option :prio, default: 0, type: :numeric, desc: 'The priority for MX records'
|
|
63
|
-
def
|
|
64
|
-
record = Porkbun::
|
|
70
|
+
def add(hostname)
|
|
71
|
+
record = Porkbun::Domain.create_record(hostname, options)
|
|
65
72
|
puts record
|
|
66
73
|
puts record.message
|
|
67
74
|
end
|
|
68
75
|
|
|
69
|
-
desc 'dyndns
|
|
76
|
+
desc 'dyndns HOSTNAME [IP]', 'Update a dynamic dns record. example: porkbun dyndns home.example.com'
|
|
70
77
|
long_desc 'If no IP is provided, the current public IP is used.'
|
|
71
78
|
def dyndns(hostname, ip = nil)
|
|
72
|
-
ip ||=
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
records = get_all(domain)
|
|
76
|
-
record = records.find { |r| r.name == hostname }
|
|
77
|
-
if record
|
|
78
|
-
record.content = ip
|
|
79
|
-
record.name = name
|
|
80
|
-
puts "UPDATE #{record}"
|
|
81
|
-
record.save
|
|
82
|
-
puts record.message
|
|
83
|
-
else
|
|
79
|
+
ip ||= Porkbun::Domain.public_ip
|
|
80
|
+
result = Porkbun::Domain.update_dynamic(hostname, ip)
|
|
81
|
+
if result[:created]
|
|
84
82
|
print 'CREATE '
|
|
85
|
-
|
|
86
|
-
puts record
|
|
87
|
-
|
|
83
|
+
puts result[:record]
|
|
84
|
+
puts result[:record].message
|
|
85
|
+
else
|
|
86
|
+
puts "UPDATE #{result[:display]}"
|
|
87
|
+
puts result[:record].message
|
|
88
88
|
end
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
-
desc 'retrieve <domain> [<id>]', 'List all records for a domain'
|
|
92
|
-
def retrieve(domain, id = '')
|
|
93
|
-
records = get_all(domain, id)
|
|
94
|
-
puts records
|
|
95
|
-
end
|
|
96
91
|
|
|
97
92
|
def self.exit_on_failure?
|
|
98
93
|
exit 1
|
|
99
94
|
end
|
|
100
95
|
|
|
101
|
-
|
|
96
|
+
end
|
|
102
97
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
records
|
|
98
|
+
if $PROGRAM_NAME == __FILE__
|
|
99
|
+
if ARGV.include?('--help')
|
|
100
|
+
command = ARGV.find { |arg| !arg.start_with?('-') }
|
|
101
|
+
CLI.start(command ? ['help', command] : ['help'])
|
|
102
|
+
else
|
|
103
|
+
CLI.start(ARGV)
|
|
110
104
|
end
|
|
111
105
|
end
|
|
112
|
-
|
|
113
|
-
CLI.start(ARGV)
|
data/justfile
ADDED
data/lib/porkbun/version.rb
CHANGED
data/lib/porkbun.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require '
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'uri'
|
|
4
6
|
require_relative 'porkbun/version'
|
|
5
7
|
|
|
6
8
|
module Porkbun
|
|
@@ -10,10 +12,16 @@ module Porkbun
|
|
|
10
12
|
if ENV.fetch('PORKBUN_API_KEY', nil).nil? || ENV.fetch('PORKBUN_SECRET_API_KEY', nil).nil?
|
|
11
13
|
abort 'PORKBUN_API_KEY and PORKBUN_SECRET_API_KEY must be set'
|
|
12
14
|
end
|
|
13
|
-
|
|
15
|
+
uri = URI(File.join('https://api.porkbun.com/api/json/v3', path))
|
|
16
|
+
request = Net::HTTP::Post.new(uri)
|
|
17
|
+
request['Content-Type'] = 'application/json'
|
|
18
|
+
request.body = {
|
|
14
19
|
secretapikey: ENV.fetch('PORKBUN_SECRET_API_KEY', nil),
|
|
15
20
|
apikey: ENV.fetch('PORKBUN_API_KEY', nil)
|
|
16
|
-
}.merge(options)
|
|
21
|
+
}.merge(options).to_json
|
|
22
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
23
|
+
http.request(request)
|
|
24
|
+
end
|
|
17
25
|
|
|
18
26
|
JSON.parse(res.body, symbolize_names: true)
|
|
19
27
|
end
|
|
@@ -41,7 +49,11 @@ module Porkbun
|
|
|
41
49
|
porkbun 'ping'
|
|
42
50
|
end
|
|
43
51
|
|
|
44
|
-
|
|
52
|
+
def self.new(domain)
|
|
53
|
+
Domain.new(domain)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class Record < Abstract
|
|
45
57
|
attr_accessor :name, :content, :type, :ttl, :prio, :domain, :id, :notes
|
|
46
58
|
|
|
47
59
|
def initialize(options)
|
|
@@ -55,7 +67,7 @@ module Porkbun
|
|
|
55
67
|
end
|
|
56
68
|
|
|
57
69
|
def self.create(options)
|
|
58
|
-
record =
|
|
70
|
+
record = Record.new options
|
|
59
71
|
record.create
|
|
60
72
|
end
|
|
61
73
|
|
|
@@ -72,17 +84,124 @@ module Porkbun
|
|
|
72
84
|
edit
|
|
73
85
|
end
|
|
74
86
|
|
|
75
|
-
def
|
|
87
|
+
def update(options = {})
|
|
88
|
+
self.name = Record.relative_record_name(self)
|
|
89
|
+
self.content = options[:content] if options.key?(:content)
|
|
90
|
+
self.ttl = options[:ttl] if options.key?(:ttl)
|
|
91
|
+
save
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.list(domain, id = nil)
|
|
76
95
|
raise Error, 'need domain' unless domain
|
|
77
96
|
|
|
78
97
|
res = Porkbun.porkbun File.join('dns/retrieve', domain, id || '').chomp('/')
|
|
79
98
|
return Error.new(res[:message]) if res[:status] == 'ERROR'
|
|
80
99
|
|
|
81
100
|
res[:records].map do |record|
|
|
82
|
-
|
|
101
|
+
Record.new record.merge(domain:)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def self.domain_for(hostname)
|
|
106
|
+
fqdn = hostname.to_s.chomp('.')
|
|
107
|
+
domains = Porkbun::Domain.list_all[:domains].map { |item| item[:domain] }
|
|
108
|
+
domains.sort_by(&:length).reverse.find do |domain|
|
|
109
|
+
fqdn == domain || fqdn.end_with?(".#{domain}")
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def self.records_for(target, id = nil)
|
|
114
|
+
requested = target.to_s.chomp('.')
|
|
115
|
+
owner = domain_for(requested) || requested.split('.').last(2).join('.')
|
|
116
|
+
records = list(owner, id)
|
|
117
|
+
raise records if records.is_a?(Error)
|
|
118
|
+
return records if owner == requested
|
|
119
|
+
|
|
120
|
+
records.select { |record| record_hostname(record, owner) == requested }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def self.find_record(hostname)
|
|
124
|
+
records = records_for(hostname)
|
|
125
|
+
raise Error, 'No record found for hostname' if records.empty?
|
|
126
|
+
raise Error, 'Multiple records found for hostname' if records.length > 1
|
|
127
|
+
|
|
128
|
+
records.first
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def self.create_record(record, options)
|
|
132
|
+
fqdn = record.chomp('.')
|
|
133
|
+
domain = domain_for(fqdn) || fqdn.split('.').last(2).join('.')
|
|
134
|
+
name = fqdn == domain ? '' : fqdn.delete_suffix(".#{domain}")
|
|
135
|
+
create(options.merge(domain:, name:))
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def self.update_record(record, options)
|
|
139
|
+
record.name = relative_record_name(record)
|
|
140
|
+
record.content = options[:content] if options[:content]
|
|
141
|
+
record.ttl = options[:ttl] if options[:ttl]
|
|
142
|
+
record.save
|
|
143
|
+
record
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def self.delete_record(hostname)
|
|
147
|
+
record = find_record(hostname)
|
|
148
|
+
record.delete
|
|
149
|
+
record
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def self.delete_all(domain, id = '')
|
|
153
|
+
list(domain, id).reject { |record| record.type == 'NS' }.each(&:delete)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def self.import_zone(file)
|
|
157
|
+
record_regex = /^(?<hostname>[^\s]+)\.\s+(?<ttl>\d+)\s+IN\s+(?<type>[^\s]+)\s*(?<priority>\d+)?\s+(?<content>.+)$/
|
|
158
|
+
IO.readlines(file).filter_map do |line|
|
|
159
|
+
match_data = line.match(record_regex)
|
|
160
|
+
next unless match_data
|
|
161
|
+
|
|
162
|
+
labels = match_data[:hostname].split('.')
|
|
163
|
+
options = {
|
|
164
|
+
domain: labels[-2..].join('.'),
|
|
165
|
+
name: labels[0..-3].join('.'),
|
|
166
|
+
ttl: match_data[:ttl],
|
|
167
|
+
type: match_data[:type],
|
|
168
|
+
prio: match_data[:priority],
|
|
169
|
+
content: match_data[:content].chomp('.').gsub(/^"|"$/, '')
|
|
170
|
+
}.compact
|
|
171
|
+
create(options)
|
|
83
172
|
end
|
|
84
173
|
end
|
|
85
174
|
|
|
175
|
+
def self.public_ip
|
|
176
|
+
Net::HTTP.get(URI('https://canhazip.com')).chomp
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def self.update_dynamic(hostname, ip)
|
|
180
|
+
records = records_for(hostname)
|
|
181
|
+
return { created: true, record: create_record(hostname, type: 'A', content: ip) } if records.empty?
|
|
182
|
+
|
|
183
|
+
record = records.first
|
|
184
|
+
display = record.to_s
|
|
185
|
+
update_record(record, content: ip)
|
|
186
|
+
{ created: false, display:, record: }
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def self.record_hostname(record, domain)
|
|
190
|
+
name = record.name.to_s.chomp('.')
|
|
191
|
+
return domain if name.empty? || name == '@'
|
|
192
|
+
|
|
193
|
+
name.end_with?(".#{domain}") ? name : "#{name}.#{domain}"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def self.relative_record_name(record)
|
|
197
|
+
name = record.name.to_s.chomp('.')
|
|
198
|
+
domain = record.domain.to_s.chomp('.')
|
|
199
|
+
return '' if name == domain
|
|
200
|
+
return name.delete_suffix(".#{domain}") if name.end_with?(".#{domain}")
|
|
201
|
+
|
|
202
|
+
name
|
|
203
|
+
end
|
|
204
|
+
|
|
86
205
|
def delete
|
|
87
206
|
raise Error, 'Need ID to delete record' unless id
|
|
88
207
|
|
|
@@ -105,6 +224,7 @@ module Porkbun
|
|
|
105
224
|
"#{name}. #{ttl} IN #{type} #{prio_str} #{content_str}".tr_s(' ', ' ')
|
|
106
225
|
end
|
|
107
226
|
|
|
227
|
+
|
|
108
228
|
def to_h
|
|
109
229
|
{
|
|
110
230
|
name: name,
|
|
@@ -137,4 +257,111 @@ module Porkbun
|
|
|
137
257
|
options
|
|
138
258
|
end
|
|
139
259
|
end
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class Domain
|
|
263
|
+
attr_reader :domain
|
|
264
|
+
|
|
265
|
+
def initialize(domain)
|
|
266
|
+
@domain = domain.to_s.chomp('.')
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def self.all
|
|
270
|
+
list_all[:domains].map { |item| new(item[:domain]) }
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def to_s
|
|
274
|
+
domain
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def records
|
|
278
|
+
Record.list(domain)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def records_for(target)
|
|
282
|
+
requested = normalize_hostname(target)
|
|
283
|
+
records = Record.list(domain)
|
|
284
|
+
raise records if records.is_a?(Error)
|
|
285
|
+
return records if requested == domain
|
|
286
|
+
|
|
287
|
+
records.select { |record| Record.record_hostname(record, domain) == requested }
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def get_record(name)
|
|
291
|
+
matches = records_for(name)
|
|
292
|
+
raise Error, 'No record found for hostname' if matches.empty?
|
|
293
|
+
raise Error, 'Multiple records found for hostname' if matches.length > 1
|
|
294
|
+
|
|
295
|
+
matches.first
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def create_record(name, options)
|
|
299
|
+
hostname = normalize_hostname(name)
|
|
300
|
+
relative_name = hostname == domain ? '' : hostname.delete_suffix(".#{domain}")
|
|
301
|
+
Record.create(options.merge(domain:, name: relative_name))
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def delete_all_records
|
|
306
|
+
Record.delete_all(domain)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def zone_file
|
|
310
|
+
records.map(&:to_s).join("\n") + "\n"
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def self.get_record(hostname)
|
|
314
|
+
Record.find_record(hostname)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def self.update_dynamic(hostname, ip)
|
|
319
|
+
Record.update_dynamic(hostname, ip)
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def update_dynamic(name, ip)
|
|
323
|
+
matches = records_for(name)
|
|
324
|
+
return { created: true, record: create_record(name, type: 'A', content: ip) } if matches.empty?
|
|
325
|
+
|
|
326
|
+
record = get_record(name)
|
|
327
|
+
display = record.to_s
|
|
328
|
+
record.update(content: ip)
|
|
329
|
+
{ created: false, display:, record: }
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
private
|
|
333
|
+
|
|
334
|
+
def normalize_hostname(name)
|
|
335
|
+
hostname = name.to_s.chomp('.')
|
|
336
|
+
return hostname if hostname == domain || hostname.end_with?(".#{domain}")
|
|
337
|
+
|
|
338
|
+
"#{hostname}.#{domain}"
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
class << self
|
|
342
|
+
def get_record(hostname)
|
|
343
|
+
Record.find_record(hostname)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def create_record(hostname, options)
|
|
350
|
+
Record.create_record(hostname, options)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def import(file)
|
|
355
|
+
Record.import_zone(file)
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def public_ip
|
|
359
|
+
Record.public_ip
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def update_dynamic(hostname, ip)
|
|
363
|
+
Record.update_dynamic(hostname, ip)
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
140
367
|
end
|
data/mise.toml
ADDED
metadata
CHANGED
|
@@ -1,42 +1,28 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: porkbun
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel Bretoi
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
-
- !ruby/object:Gem::Dependency
|
|
13
|
-
name: http
|
|
14
|
-
requirement: !ruby/object:Gem::Requirement
|
|
15
|
-
requirements:
|
|
16
|
-
- - "~>"
|
|
17
|
-
- !ruby/object:Gem::Version
|
|
18
|
-
version: 5.2.0
|
|
19
|
-
type: :runtime
|
|
20
|
-
prerelease: false
|
|
21
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
-
requirements:
|
|
23
|
-
- - "~>"
|
|
24
|
-
- !ruby/object:Gem::Version
|
|
25
|
-
version: 5.2.0
|
|
26
12
|
- !ruby/object:Gem::Dependency
|
|
27
13
|
name: thor
|
|
28
14
|
requirement: !ruby/object:Gem::Requirement
|
|
29
15
|
requirements:
|
|
30
16
|
- - "~>"
|
|
31
17
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '1.
|
|
18
|
+
version: '1.5'
|
|
33
19
|
type: :runtime
|
|
34
20
|
prerelease: false
|
|
35
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
22
|
requirements:
|
|
37
23
|
- - "~>"
|
|
38
24
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '1.
|
|
25
|
+
version: '1.5'
|
|
40
26
|
description: Porkbun API wrapper for Ruby.
|
|
41
27
|
email:
|
|
42
28
|
- daniel@otherware.org
|
|
@@ -46,13 +32,18 @@ extensions: []
|
|
|
46
32
|
extra_rdoc_files: []
|
|
47
33
|
files:
|
|
48
34
|
- ".rspec"
|
|
35
|
+
- AGENTS.md
|
|
36
|
+
- CHANGELOG.md
|
|
37
|
+
- CLAUDE.md
|
|
49
38
|
- Gemfile
|
|
50
39
|
- Gemfile.lock
|
|
51
40
|
- README.md
|
|
52
41
|
- Rakefile
|
|
53
42
|
- bin/porkbun
|
|
43
|
+
- justfile
|
|
54
44
|
- lib/porkbun.rb
|
|
55
45
|
- lib/porkbun/version.rb
|
|
46
|
+
- mise.toml
|
|
56
47
|
homepage: https://github.com/danielb2/porkbun-ruby
|
|
57
48
|
licenses: []
|
|
58
49
|
metadata:
|
|
@@ -65,14 +56,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
65
56
|
requirements:
|
|
66
57
|
- - ">="
|
|
67
58
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: 2.
|
|
59
|
+
version: 3.2.0
|
|
69
60
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
70
61
|
requirements:
|
|
71
62
|
- - ">="
|
|
72
63
|
- !ruby/object:Gem::Version
|
|
73
64
|
version: '0'
|
|
74
65
|
requirements: []
|
|
75
|
-
rubygems_version:
|
|
66
|
+
rubygems_version: 4.0.10
|
|
76
67
|
specification_version: 4
|
|
77
68
|
summary: Porkbun API wrapper for Ruby.
|
|
78
69
|
test_files: []
|