active_zone 0.2.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5883915c05075277429a22c1be2faa60d1b0e9b2a9dcff61f631589ed60c7d0c
4
+ data.tar.gz: 4d31ca5a2f065943356905f5f142d89ada3ac43a9e2468ec0f9c020df2a4fe9b
5
+ SHA512:
6
+ metadata.gz: d724771b2abf9bd36a2280ac1ae7d90df0cbc4c5305c08d74285ea527856f2adb82fef47f866dbd8d9fa209a59a8fa3dc8bde711fe403251c4d4894ef531e4f5
7
+ data.tar.gz: e1bfe415fc7a9d37ca0c834bbefb34fd461fc1793dd23e9fa101f9c6301ccaed205c79ee4344970136baffb2f01e4aa2943b02a230c42d071b93ef834ba3678b
data/LICENSE.md ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Obl.ong
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # Active Zone
2
+ A Rails engine for managing database-backed DNS zones with various nameserver adapters.
3
+
4
+ ## Usage
5
+ How to use my plugin.
6
+
7
+ ## Installation
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem "active_zone"
12
+ ```
13
+
14
+ And then execute:
15
+ ```bash
16
+ $ bundle
17
+ ```
18
+
19
+ Or install it yourself as:
20
+ ```bash
21
+ $ gem install active_zone
22
+ ```
23
+
24
+ ## Contributing
25
+ Contribution directions go here.
26
+
27
+ ## License
28
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT), and aims to remain compatible with the [Ruby on Rails](https://github.com/rails/rails) licensing terms.
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
@@ -0,0 +1 @@
1
+ ActiveZone.provider = RactorDNS::ActiveZone.new(cpu_count: 24, zones: {})
@@ -0,0 +1,77 @@
1
+ module ActiveZone
2
+ # An Active Model class representing a DNS Zone at a provider.
3
+ # @abstract Subclass and override to create a functional Zone model
4
+ # @see Zone
5
+ # @see Zone::Builtins
6
+ class Provider::Zone
7
+ include ActiveModel::Model
8
+ include ActiveModel::Attributes
9
+ include ActiveModel::AttributeAssignment
10
+ include ActiveModel::Dirty
11
+ include ActiveModel::Conversion
12
+ include ActiveModel::Serialization
13
+ include ActiveModel::Validations
14
+ extend ActiveModel::Callbacks
15
+ include ActiveZone::Zone::Builtins
16
+
17
+ # Whether the current zone is persisted with a provider
18
+ # @api private
19
+ # @macro boolean
20
+ attr_reader :persisted
21
+
22
+ # @param provider [ActiveZone::Provider] Provider to store the zone with
23
+ # @param attributes [Hash] Attributes for the zone
24
+ # @option attributes [Array] :resources Array of resources mappable to {ActiveZone::Resource}
25
+ # @option attributes [String] :name Name of the zone
26
+ # @option attributes [TrueClass, FalseClass] :persisted Whether the zone is persisted at the provider
27
+ def initialize(provider = ActiveZone.provider, **attributes)
28
+ @provider = provider
29
+ (attributes[:resources] = attributes[:resources].map { |el| ActiveZone::Resource.new(**el) }) if attributes[:resources]
30
+ super(attributes)
31
+ clear_changes_information
32
+ end
33
+
34
+ # Delegate missing class methods to {all}
35
+ def self.method_missing(method_name, *, &)
36
+ all.send(method_name, *, &)
37
+ end
38
+
39
+ # (see method_missing)
40
+ # @macro boolean
41
+ def self.respond_to_missing?(method_name, include_private = false)
42
+ all.respond_to?(method_name, include_private)
43
+ end
44
+
45
+ attribute :name, :string, default: ""
46
+ attribute :persisted, default: false
47
+ attribute :resources, array: true, default: []
48
+ define_attribute_methods :name, :resources
49
+ # Provider the zone will be stored at
50
+ # @return [ActiveZone::Provider]
51
+ attr_reader :provider
52
+
53
+ define_model_callbacks :create, :save, :destroy
54
+
55
+ # Underlying method to save the current zone
56
+ # @api private
57
+ # @abstract Override in subclass with how to persist a Zone at your provider
58
+ # @macro boolean
59
+ def persist!(**options, &block)
60
+ raise NotImplementedError
61
+ end
62
+
63
+ # Underlying method to delete the current zone
64
+ # @api private
65
+ # @abstract Override in subclass with how to delete a Zone at your provider.
66
+ # @macro boolean
67
+ def delete!
68
+ raise NotImplementedError
69
+ end
70
+
71
+ # (see persisted)
72
+ # @see persisted
73
+ def persisted?
74
+ persisted
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,80 @@
1
+ module ActiveZone
2
+ # A queryable array-like class representing a collection of {Zone} at a provider
3
+ # @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/Relation.html
4
+ # @abstract Subclass and override to create a queryable zone collection
5
+ class Provider::ZoneCollection
6
+ include Enumerable
7
+ # Provider the ZoneCollection is bound to
8
+ # @return [Provider]
9
+ attr_reader :provider
10
+ # Set of names in the ZoneCollection
11
+ # @return [Array]
12
+ # @return [nil] if all zones is desired
13
+ attr_accessor :set
14
+
15
+ # Initialize new ZoneCollection
16
+ # @param provider [ActiveZone::Provider] the provider
17
+ # @param set [Array] Array of names in the ZoneCollection, or nil for all zones
18
+ # @return ActiveZone::Provider::ZoneCollection
19
+ def initialize(provider, set = nil)
20
+ @provider = provider
21
+ @set = set
22
+ end
23
+
24
+ # Iterate over the zones at the provider
25
+ # @abstract Override and yield all zones in the set at the provider
26
+ # @yield [Zone] Zones within the {set} at the provider
27
+ # @raise {NotImplementedError}
28
+ def each(&block)
29
+ raise NotImplementedError
30
+ end
31
+
32
+ # Destroy all zones in this collection
33
+ # @return {nil}
34
+ # @abstract Override and destroy all zones in the set at the provider
35
+ # @raise {NotImplementedError}
36
+ def destroy_all
37
+ raise NotImplementedError
38
+ end
39
+
40
+ # Find a zone by name
41
+ # @return {Zone}
42
+ # @abstract Override and find the first zone by name in the set at the provider
43
+ # @raise {NotImplementedError}
44
+ def find(name)
45
+ raise NotImplementedError
46
+ end
47
+
48
+ # Find a zone by attribute in this collection
49
+ # @param attributes [Hash] attributes to filter by
50
+ # @return {Zone}
51
+ # @abstract Override and find the first zone that matches the provided attributes in the set at the provider
52
+ # @raise {NotImplementedError}
53
+ def find_by(**attributes)
54
+ raise NotImplementedError
55
+ end
56
+
57
+ # Find a zone by name inside this collection, if not found then create it
58
+ # @param attributes [Hash] the attributes of the zone to find by or create with
59
+ # @return {Zone}
60
+ def find_or_create_by(**attributes)
61
+ find_by(attributes) || provider.Zone.create(attributes)
62
+ end
63
+
64
+ # Update a zone by name in this collection, if not found then create it
65
+ # @param name [String] the name of the zone
66
+ # @return {Zone}
67
+ def upsert(name)
68
+ z = find_or_create_by(name:)
69
+ z.update(attributes)
70
+ end
71
+
72
+ # Get nth value in collection
73
+ # @abstract Override and get the nth value in the set at the provider
74
+ # @raise {NotImplementedError}
75
+ # @return {Zone}
76
+ def [](i)
77
+ raise NotImplementedError
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,19 @@
1
+ module ActiveZone
2
+ # The base class for Active Zone providers
3
+ # @abstract Subclass in your provider gem.
4
+ # @!method initialize(**options)
5
+ # @abstract Override in your provider subclass to configure & initialize your provider.
6
+ # @example Rails initializer
7
+ # ActiveZone.provider = YourProvider.new(username:, password:, base_url: "dns.example.com")
8
+ class Provider
9
+ # @return [Object<Zone>] Zone constant scoped under this Provider
10
+ def Zone
11
+ self.class.const_get(:Zone)
12
+ end
13
+
14
+ # @return [Object<ZoneCollection>] ZoneCollection constant scoped under this Provider
15
+ def ZoneCollection
16
+ self.class.const_get(:ZoneCollection)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,44 @@
1
+ # Simple wrapper for a complete resource
2
+ class ActiveZone::Resource
3
+ # @return [String]
4
+ attr_reader :name
5
+ # @return [Integer]
6
+ attr_reader :ttl
7
+ # @return [RRs::Resource]
8
+ attr_reader :rr
9
+
10
+ def initialize(zone: nil, name: "", ttl: 300, rr: nil)
11
+ @zone = zone
12
+ @name = name
13
+ @ttl = ttl
14
+ @rr = rr
15
+ @zone&.resources_will_change!
16
+ end
17
+
18
+ # Set name and mark changed on zone
19
+ # @param v [String]
20
+ def name=(v)
21
+ @name = v
22
+ @zone&.resources_will_change!
23
+ end
24
+
25
+ # Set TTL and mark changed on zone
26
+ # @param v [Integer]
27
+ def ttl=(v)
28
+ @ttl = v
29
+ @zone&.resources_will_change!
30
+ end
31
+
32
+ # Set RR and mark changed on zone
33
+ # @param v [RRs::Resource]
34
+ def rr=(v)
35
+ @rr = v
36
+ @zone&.resources_will_change!
37
+ end
38
+
39
+ # Convert to hash
40
+ # @return [Hash]
41
+ def to_h
42
+ {name:, ttl:, rr:}
43
+ end
44
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveZone
2
+ # Version of the gem
3
+ # @return [String]
4
+ VERSION = "0.2.1"
5
+ end
@@ -0,0 +1,147 @@
1
+ module ActiveZone::Zone::Builtins
2
+ # Adds Active Record-like persistence methods to {ActiveZone::Provider::Zone}
3
+ # @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/Persistence.html
4
+ module Persistence
5
+ extend ActiveSupport::Concern
6
+
7
+ class_methods do
8
+ # Creates and persists a {ActiveZone::Provider::Zone} without raising errors
9
+ # @return [ActiveZone::Provider::Zone]
10
+ # @return [FalseClass] if errored out
11
+ def create(attributes = nil, &block)
12
+ run_callbacks(:create) do
13
+ if attributes.is_a?(Array)
14
+ attributes.collect { |attr| create(attr, &block) }
15
+ else
16
+ object = new(attributes, &block)
17
+ object.save
18
+ object
19
+ end
20
+ end
21
+ end
22
+
23
+ # Creates and persists a {ActiveZone::Provider::Zone} and raises an error on failure
24
+ # @raise (see #save!)
25
+ # @return [ActiveZone::Provider::Zone]
26
+ def create!(attributes = nil, &block)
27
+ run_callbacks(:create) do
28
+ if attributes.is_a?(Array)
29
+ attributes.collect { |attr| create!(attr, &block) }
30
+ else
31
+ object = new(attributes, &block)
32
+ object.save!
33
+ object
34
+ end
35
+ end
36
+ end
37
+
38
+ # Bulk update attributes on a set of {ActiveZone::Provider::Zone}
39
+ # @return {ActiveZone::Provider::ZoneCollection}
40
+ # @return {ActiveZone::Provider::Zone}
41
+ # @return [FalseClass] if errored out
42
+ def update(name = :all, **attributes)
43
+ if name.is_a?(Array)
44
+ if name.any?(ActiveZone::Provider::Zone)
45
+ raise ArgumentError,
46
+ "You are passing an array of Zone instances to `update`. " \
47
+ "Please pass the names of the objects by calling `pluck(:name)` or `map(&:name)`."
48
+ end
49
+ name.map { |one_name| find(one_name) }.each_with_index { |object, namex|
50
+ object.update(attributes[namex])
51
+ }
52
+ elsif name == :all
53
+ all.each { |record| record.update(attributes) }
54
+ else
55
+ if name.is_a? ActiveZone::Provider::Zone
56
+ raise ArgumentError,
57
+ "You are passing an instance of Zone to `update`. " \
58
+ "Please pass the name of the object by calling `.name`."
59
+ end
60
+ object = find(name)
61
+ object.update(attributes)
62
+ object
63
+ end
64
+ end
65
+
66
+ # Bulk update attributes on a set of {ActiveZone::Provider::Zone} and raise errors on failure
67
+ # @return {ActiveZone::Provider::ZoneCollection}
68
+ # @return {ActiveZone::Provider::Zone}
69
+ # @raise (see Persistence#update!)
70
+ def update!(name = :all, **attributes)
71
+ if name.is_a?(Array)
72
+ if name.any?(ActiveZone::Provider::Zone)
73
+ raise ArgumentError,
74
+ "You are passing an array of Zone instances to `update!`. " \
75
+ "Please pass the ids of the objects by calling `pluck(:name)` or `map(&:name)`."
76
+ end
77
+ name.map { |one_name| find(one_name) }.each_with_index { |object, idx|
78
+ object.update!(attributes[idx])
79
+ }
80
+ elsif name == :all
81
+ all.each { |record| record.update!(attributes) }
82
+ else
83
+ if name.is_a? ActiveZone::Provider::Zone
84
+ raise ArgumentError,
85
+ "You are passing an instance of Zone to `update!`. " \
86
+ "Please pass the name of the object by calling `.name`."
87
+ end
88
+ object = find(name)
89
+ object.update!(attributes)
90
+ object
91
+ end
92
+ end
93
+ end
94
+
95
+ # Persists a {ActiveZone::Provider::Zone}
96
+ # @return [ActiveZone::Provider::Zone]
97
+ # @return [FalseClass] if errored out
98
+ def save(**options, &block)
99
+ run_callbacks(:save) do
100
+ persist!
101
+ end
102
+ self
103
+ rescue
104
+ false
105
+ end
106
+
107
+ # Persists a {ActiveZone::Provider::Zone} and raises an error on failure
108
+ # @raise {RuntimeError}
109
+ # @return [ActiveZone::Provider::Zone]
110
+ def save!(**, &)
111
+ save(**, &) || raise("Failed to save the zone")
112
+ end
113
+
114
+ # Destroys a {ActiveZone::Provider::Zone} at the provider and freezes the {Provider::Zone}
115
+ # @macro boolean
116
+ def destroy
117
+ run_callbacks(:destroy) do
118
+ delete! if persisted?
119
+ freeze
120
+ end
121
+ true
122
+ rescue
123
+ false
124
+ end
125
+
126
+ # Destroys a {ActiveZone::Provider::Zone} at the provider, freezes the {Provider::Zone} and raises errors on failure
127
+ # @return {TrueClass}
128
+ # @raise {RuntimeError}
129
+ def destroy!
130
+ destroy || raise("Failed to destroy the zone")
131
+ end
132
+
133
+ # Updates attributes of a {ActiveZone::Provider::Zone} and saves it
134
+ # @return (see #save)
135
+ def update(attributes)
136
+ assign_attributes(attributes)
137
+ save
138
+ end
139
+
140
+ # Updates attributes of a {ActiveZone::Provider::Zone}, saves it and raises errors on failure
141
+ # @return (see #save)
142
+ # @raise {RuntimeError}
143
+ def update!(attributes)
144
+ update || raise("Failed to update the zone")
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,36 @@
1
+ # A module of builtins for {ActiveZone::Provider::Zone}
2
+ module ActiveZone::Zone::Builtins
3
+ include Persistence
4
+ extend ActiveSupport::Concern
5
+
6
+ class_methods do
7
+ # @!scope class
8
+
9
+ @@provider = nil
10
+ @@semaphore = Mutex.new
11
+
12
+ # @return [ActiveZone::Provider]
13
+ # @api private
14
+ # Current provider in use for class methods, set dynamically by {with_provider}
15
+ attr_reader :provider
16
+ # @return [Mutex]
17
+ # @api private
18
+ # Used to lock current provider for class methods in {with_provider}
19
+ attr_reader :semaphore
20
+
21
+ # Locks the {ActiveZone::Provider} used in {ActiveZone::Provider::Zone} class methods and executes the block
22
+ def with_provider(provider, &block)
23
+ @@semaphore.synchronize {
24
+ @@provider = provider
25
+ val = block.call(self)
26
+ @@provider = nil
27
+ return val
28
+ }
29
+ end
30
+
31
+ # @return {Provider::ZoneCollection} for all zones at the current {Provider}
32
+ def all
33
+ @@provider&.ZoneCollection&.new(@@provider, nil)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,32 @@
1
+ # Proxy class for easily accessing {Provider::Zone} at the current {Provider}
2
+ # @see ActiveZone::Provider::Zone
3
+ class ActiveZone::Zone
4
+ # @param attributes [Hash] attributes for the new {Provider::Zone}
5
+ def initialize(**attributes)
6
+ @zone = ActiveZone.provider.Zone.new(**attributes)
7
+ end
8
+
9
+ # Delegate to {Provider::Zone}
10
+ def method_missing(method_name, *, &)
11
+ @zone.send(method_name, *, &)
12
+ end
13
+
14
+ # Delegate to {Provider::Zone}
15
+ # @macro boolean
16
+ def respond_to_missing?(method_name, include_private = false)
17
+ @zone.respond_to?(method_name, include_private)
18
+ end
19
+
20
+ # Delegate class methods to current [Provider::Zone]
21
+ def self.method_missing(method_name, *arguments, &block)
22
+ ActiveZone.provider.Zone.with_provider(ActiveZone.provider) do |z|
23
+ z.send(method_name, *arguments, &block)
24
+ end
25
+ end
26
+
27
+ # Delegate class methods to current [Provider::Zone]
28
+ # @macro boolean
29
+ def self.respond_to_missing?(method_name, include_private = false)
30
+ ActiveZone.provider.Zone.respond_to?(method_name, include_private)
31
+ end
32
+ end
@@ -0,0 +1,18 @@
1
+ require "active_support/all"
2
+ require "rrs"
3
+ require "ractor_dns"
4
+ require "zeitwerk"
5
+
6
+ loader = Zeitwerk::Loader.for_gem
7
+ loader.setup
8
+
9
+ # Base Active Zone module
10
+ module ActiveZone
11
+ # Current default provider
12
+ # @return [Provider]
13
+ mattr_accessor :provider
14
+ end
15
+
16
+ # @!macro [new] boolean
17
+ # @return [TrueClass] if true
18
+ # @return [FalseClass] if false
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_zone
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Obl.ong
8
+ - Reese Armstrong
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2024-08-24 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: 7.2.0
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: 7.2.0
28
+ - !ruby/object:Gem::Dependency
29
+ name: zeitwerk
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '2.6'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '2.6'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rrs
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: 0.2.0
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: 0.2.0
56
+ description: A Rails engine for managing database-backed DNS zones with various nameserver
57
+ adapters.
58
+ email:
59
+ - team@obl.ong
60
+ executables: []
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - LICENSE.md
65
+ - README.md
66
+ - Rakefile
67
+ - config/initializers/provider.rb
68
+ - lib/active_zone.rb
69
+ - lib/active_zone/provider.rb
70
+ - lib/active_zone/provider/zone.rb
71
+ - lib/active_zone/provider/zone_collection.rb
72
+ - lib/active_zone/resource.rb
73
+ - lib/active_zone/version.rb
74
+ - lib/active_zone/zone.rb
75
+ - lib/active_zone/zone/builtins.rb
76
+ - lib/active_zone/zone/builtins/persistence.rb
77
+ homepage: https://github.com/obl-ong/active_zone
78
+ licenses:
79
+ - MIT
80
+ metadata:
81
+ allowed_push_host: https://rubygems.org
82
+ homepage_uri: https://github.com/obl-ong/active_zone
83
+ source_code_uri: https://github.com/obl-ong/active_zone
84
+ changelog_uri: https://github.com/obl-ong/active_zone/releases
85
+ post_install_message:
86
+ rdoc_options: []
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ requirements: []
100
+ rubygems_version: 3.5.17
101
+ signing_key:
102
+ specification_version: 4
103
+ summary: Manage database-backed DNS zones with Rails
104
+ test_files: []