kozo 0.1.0 → 0.2.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.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +7 -2
  3. data/.kzignore +1 -0
  4. data/.overcommit.yml +1 -0
  5. data/.rspec +1 -0
  6. data/.rubocop.yml +44 -5
  7. data/CHANGELOG.md +5 -0
  8. data/Gemfile +3 -1
  9. data/Gemfile.lock +156 -48
  10. data/LICENSE.md +1 -1
  11. data/README.md +28 -0
  12. data/Rakefile +3 -7
  13. data/bin/kozo +6 -2
  14. data/config/application.rb +3 -0
  15. data/config/dependencies.rb +4 -25
  16. data/kozo.gemspec +20 -5
  17. data/lib/core_ext/boolean.rb +12 -0
  18. data/lib/core_ext/nil_class.rb +11 -0
  19. data/lib/core_ext/string.rb +25 -0
  20. data/lib/kozo/backend.rb +81 -0
  21. data/lib/kozo/backends/git.rb +50 -0
  22. data/lib/kozo/backends/local.rb +41 -15
  23. data/lib/kozo/backends/memory.rb +26 -0
  24. data/lib/kozo/cli.rb +30 -9
  25. data/lib/kozo/command.rb +32 -0
  26. data/lib/kozo/commands/apply.rb +44 -0
  27. data/lib/kozo/commands/console.rb +15 -0
  28. data/lib/kozo/commands/import.rb +47 -0
  29. data/lib/kozo/commands/init.rb +15 -0
  30. data/lib/kozo/commands/plan.rb +29 -3
  31. data/lib/kozo/commands/refresh.rb +21 -0
  32. data/lib/kozo/commands/show.rb +15 -0
  33. data/lib/kozo/commands/state.rb +64 -0
  34. data/lib/kozo/commands/validate.rb +13 -0
  35. data/lib/kozo/commands/version.rb +13 -0
  36. data/lib/kozo/concerns/assignment.rb +17 -0
  37. data/lib/kozo/concerns/attributes.rb +99 -0
  38. data/lib/kozo/concerns/mark.rb +25 -0
  39. data/lib/kozo/concerns/track.rb +47 -0
  40. data/lib/kozo/configuration.rb +34 -7
  41. data/lib/kozo/dsl.rb +18 -12
  42. data/lib/kozo/error.rb +13 -0
  43. data/lib/kozo/logger.rb +8 -4
  44. data/lib/kozo/operation.rb +42 -0
  45. data/lib/kozo/operations/create.rb +29 -0
  46. data/lib/kozo/operations/destroy.rb +29 -0
  47. data/lib/kozo/operations/show.rb +12 -0
  48. data/lib/kozo/operations/update.rb +19 -0
  49. data/lib/kozo/options.rb +9 -1
  50. data/lib/kozo/parser.rb +35 -0
  51. data/lib/kozo/provider.rb +7 -1
  52. data/lib/kozo/providers/dummy/dependencies.rb +9 -0
  53. data/lib/kozo/providers/{null → dummy}/provider.rb +2 -6
  54. data/lib/kozo/providers/dummy/resource.rb +19 -0
  55. data/lib/kozo/providers/dummy/resources/dummy.rb +16 -0
  56. data/lib/kozo/providers/hcloud/dependencies.rb +13 -0
  57. data/lib/kozo/providers/hcloud/provider.rb +7 -1
  58. data/lib/kozo/providers/hcloud/resource.rb +46 -1
  59. data/lib/kozo/providers/hcloud/resources/server.rb +29 -0
  60. data/lib/kozo/providers/hcloud/resources/ssh_key.rb +6 -2
  61. data/lib/kozo/resource.rb +110 -3
  62. data/lib/kozo/state.rb +25 -0
  63. data/lib/kozo/type.rb +15 -0
  64. data/lib/kozo/types/boolean.rb +30 -0
  65. data/lib/kozo/types/date.rb +20 -0
  66. data/lib/kozo/types/float.rb +17 -0
  67. data/lib/kozo/types/hash.rb +20 -0
  68. data/lib/kozo/types/integer.rb +17 -0
  69. data/lib/kozo/types/string.rb +17 -0
  70. data/lib/kozo/types/time.rb +18 -0
  71. data/lib/kozo/version.rb +1 -1
  72. data/lib/kozo.rb +13 -7
  73. metadata +195 -31
  74. data/.github/dependabot.yml +0 -14
  75. data/.github/workflows/ci.yml +0 -81
  76. data/bin/bundle +0 -118
  77. data/bin/console +0 -7
  78. data/bin/rspec +0 -28
  79. data/bin/version +0 -62
  80. data/lib/kozo/backends/base.rb +0 -31
  81. data/lib/kozo/commands/base.rb +0 -21
  82. data/lib/kozo/container.rb +0 -35
  83. data/lib/kozo/environment.rb +0 -27
  84. data/lib/kozo/providers/null/resource.rb +0 -11
  85. data/lib/kozo/providers/null/resources/null.rb +0 -13
  86. data/log/.keep +0 -0
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ module Attributes
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ class_attribute :attribute_types, default: {}
9
+
10
+ def read_attribute(name)
11
+ value = instance_variable_get(:"@#{name}")
12
+
13
+ return value unless value.nil?
14
+
15
+ # Set default
16
+ instance_variable_set(:"@#{name}", (attribute_types[name][:default].dup || (attribute_types[name][:multiple] ? [] : nil)))
17
+ end
18
+
19
+ def write_attribute(name, value)
20
+ try(:track_change!, name, value)
21
+
22
+ value = if attribute_types[name][:multiple]
23
+ Array(value).map { |v| attribute_types[name][:type].cast(v) }
24
+ else
25
+ attribute_types[name][:type].cast(value)
26
+ end
27
+
28
+ instance_variable_set(:"@#{name}", value)
29
+ end
30
+ end
31
+
32
+ # rubocop:disable Metrics/BlockLength
33
+ class_methods do
34
+ def inherited(sub_class)
35
+ super
36
+
37
+ sub_class.attribute_types = attribute_types.clone
38
+ end
39
+
40
+ def attribute(name, **options)
41
+ name = name.to_sym
42
+ type = Type.lookup(options.fetch(:type, :string))
43
+
44
+ try(:track, name)
45
+
46
+ options = attribute_types[name] = {
47
+ multiple: !!options[:multiple],
48
+ attribute: !!options.fetch(:attribute, true),
49
+ argument: !!options.fetch(:argument, true),
50
+ type: type,
51
+ default: options[:default],
52
+ }
53
+
54
+ # Define getter
55
+ unless method_defined? name
56
+ define_method(name) { read_attribute(name) }
57
+ define_method(:"#{name}?") { !!read_attribute(name) }
58
+ end
59
+
60
+ # Set visibility to public if it's an attribute
61
+ options[:attribute] ? public(name) : private(name)
62
+
63
+ # Define setter
64
+ define_method(:"#{name}=") { |value| write_attribute(name, value) } unless method_defined? :"#{name}="
65
+
66
+ # Set visibility to public if it's an argument
67
+ options[:argument] ? public(:"#{name}=") : private(:"#{name}=")
68
+ end
69
+
70
+ def attribute_names
71
+ @attribute_names ||= attribute_types
72
+ .select { |_k, v| v[:attribute] }
73
+ .keys
74
+ end
75
+
76
+ def argument_names
77
+ @argument_names ||= attribute_types
78
+ .select { |_k, v| v[:argument] }
79
+ .keys
80
+ end
81
+ end
82
+ # rubocop:enable Metrics/BlockLength
83
+
84
+ def attributes
85
+ attribute_names
86
+ .map { |name| [name, read_attribute(name)] }
87
+ .to_h
88
+ end
89
+
90
+ def arguments
91
+ argument_names
92
+ .map { |name| [name, read_attribute(name)] }
93
+ .to_h
94
+ end
95
+
96
+ delegate :attribute_names, to: :class
97
+ delegate :argument_names, to: :class
98
+ end
99
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ module Mark
5
+ def mark_for_deletion!
6
+ @status = :delete
7
+ end
8
+
9
+ def marked_for_deletion?
10
+ @status == :delete
11
+ end
12
+
13
+ def mark_for_creation!
14
+ @status = :create
15
+ end
16
+
17
+ def marked_for_creation?
18
+ @status == :create
19
+ end
20
+
21
+ def unmark!
22
+ @status = nil
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ module Track
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ class_attribute :trackables, default: Set.new
9
+
10
+ attr_reader :changes
11
+
12
+ def changed?
13
+ changes.any?
14
+ end
15
+
16
+ def clear_changes
17
+ @changes = {}
18
+ end
19
+
20
+ def restore_changes
21
+ changes.each { |key, (from, _to)| send(:"#{key}=", from) }
22
+
23
+ clear_changes
24
+ end
25
+
26
+ def track_change!(name, value)
27
+ changes[name] = [send(name), value] unless send(name) == value
28
+ end
29
+ end
30
+
31
+ class_methods do
32
+ def track(name)
33
+ trackables << name
34
+
35
+ define_method(:"#{name}_change") { changes[name] } unless method_defined? :"#{name}_change"
36
+ define_method(:"#{name}_changed?") { !!changes[name] } unless method_defined? :"#{name}_changed?"
37
+ define_method(:"#{name}_was") { changes.dig(name, 0) } unless method_defined? :"#{name}_was"
38
+ end
39
+ end
40
+
41
+ def initialize(...)
42
+ @changes = {}
43
+
44
+ super
45
+ end
46
+ end
47
+ end
@@ -8,18 +8,45 @@ module Kozo
8
8
  def initialize(directory)
9
9
  @directory = directory
10
10
  @providers = {}
11
- @resources = {}
11
+ @resources = Set.new
12
12
  end
13
13
 
14
14
  def backend
15
- @backend ||= Backends::Local.new(directory)
15
+ @backend ||= Kozo
16
+ .container
17
+ .resolve("backend.local", self, directory)
16
18
  end
17
19
 
18
- def parse!
19
- dsl = DSL.new(self)
20
- Dir[File.join(directory, "*.kz")]
21
- .sort
22
- .each { |file| dsl.instance_eval(File.read(file)) }
20
+ def changes
21
+ @changes ||= begin
22
+ # Copy resources in state
23
+ changes = backend
24
+ .state
25
+ .resources
26
+ .map(&:dup)
27
+ .each(&:clear_changes)
28
+ .each do |resource|
29
+ configured = resources.find { |r| r.address == resource.address }
30
+
31
+ # Assign updated attributes (mark for update)
32
+ resource.assign_attributes(configured&.attributes&.except(:id) || resource.attributes.except(:id).transform_values { nil })
33
+
34
+ # Mark for deletion
35
+ resource.mark_for_deletion! unless configured
36
+ end
37
+
38
+ # Append resources not in state
39
+ changes += resources
40
+ .reject { |r| backend.state.resources.any? { |res| res.address == r.address } }
41
+ .map { |r| r.class.new(state_name: r.state_name, **r.arguments) }
42
+ .each(&:mark_for_creation!)
43
+
44
+ changes
45
+ end
46
+ end
47
+
48
+ def to_s
49
+ "directory: #{directory}"
23
50
  end
24
51
  end
25
52
  end
data/lib/kozo/dsl.rb CHANGED
@@ -13,7 +13,7 @@ module Kozo
13
13
  end
14
14
 
15
15
  def backend(type)
16
- backend = resolve(:backend, type)
16
+ backend = resolve(:backend, type, configuration)
17
17
 
18
18
  yield backend if block_given?
19
19
 
@@ -25,28 +25,34 @@ module Kozo
25
25
 
26
26
  yield provider if block_given?
27
27
 
28
- configuration.providers[provider.class.name] = provider
28
+ provider.setup
29
+
30
+ configuration.providers[provider.provider_name] = provider
29
31
  end
30
32
 
31
- def resource(type, name)
32
- resource = resolve(:resource, type, name)
33
- resource.provider = configuration.providers[resource.class.provider]
33
+ def resource(type, state_name)
34
+ resource = resolve(:resource, type)
35
+ resource.state_name = state_name
36
+
37
+ raise InvalidResource, "resource #{resource.address} already defined" if configuration.resources.include?(resource)
38
+
39
+ resource.provider = configuration.providers[resource.provider_name]
34
40
 
35
- Kozo.logger.fatal "Provider #{resource.class.provider}" unless resource.provider
41
+ raise InvalidResource, "provider #{resource.provider_name} not configured" unless resource.provider
36
42
 
37
43
  yield resource if block_given?
38
44
 
39
- configuration.resources[resource.class.name] = resource
45
+ configuration.resources << resource
40
46
  end
41
47
 
42
48
  private
43
49
 
44
- def resolve(resource, type, name = nil)
45
- Kozo.logger.debug "Initializing #{resource} #{type} #{name}"
50
+ def resolve(resource, type, *args)
51
+ Kozo.logger.debug "Initializing #{resource} #{type}#{" with options #{args.join(' ')}" if args.any?}"
46
52
 
47
- Kozo.container.resolve("#{resource}.#{type}")
48
- rescue Container::DependencyNotRegistered
49
- Kozo.logger.fatal "Unknown #{resource} type: #{type}"
53
+ Kozo.container.resolve("#{resource}.#{type}", *args)
54
+ rescue Dinja::Container::DependencyNotRegistered
55
+ raise InvalidResource, "unknown #{resource} type: #{type}"
50
56
  end
51
57
  end
52
58
  end
data/lib/kozo/error.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ class Error < StandardError; end
5
+
6
+ class InvalidResource < Error; end
7
+
8
+ class ExitError < Error; end
9
+
10
+ class UsageError < Error; end
11
+
12
+ class StateError < Error; end
13
+ end
data/lib/kozo/logger.rb CHANGED
@@ -12,18 +12,22 @@ module Kozo
12
12
  private
13
13
 
14
14
  def level
15
- ENV.fetch("LOG_LEVEL", "info")
15
+ Kozo.options.verbose? ? "debug" : ENV.fetch("LOG_LEVEL", "info")
16
16
  end
17
17
 
18
18
  def formatter
19
- level == "debug" ? ::Logger::Formatter.new : Formatter.new
19
+ Formatter.new
20
20
  end
21
21
 
22
22
  class Formatter < ::Logger::Formatter
23
23
  def call(severity, _time, _progname, msg)
24
- abort("#{File.basename($PROGRAM_NAME)}: #{msg[0].downcase}#{msg[1..]}") if severity == "FATAL"
24
+ abort("#{File.basename($PROGRAM_NAME)}: #{msg[0].downcase}#{msg[1..]}".white.on_red) if severity == "FATAL"
25
25
 
26
- "#{msg}\n"
26
+ msg = "#{msg}\n"
27
+ msg = msg.yellow if severity == "DEBUG"
28
+ msg = msg.red if severity == "ERROR"
29
+
30
+ msg
27
31
  end
28
32
  end
29
33
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ class Operation
5
+ attr_reader :resource
6
+
7
+ class_attribute :symbol, :display_symbol
8
+
9
+ def initialize(resource)
10
+ @resource = resource
11
+ end
12
+
13
+ def apply(_state)
14
+ raise NotImplementedError
15
+ end
16
+
17
+ def to_s
18
+ resource_to_s
19
+ end
20
+
21
+ protected
22
+
23
+ def resource_to_s
24
+ <<~DSL.chomp
25
+ #{"# #{resource.address}:".bold}
26
+ #{display_symbol} resource "#{resource.resource_name}", "#{resource.state_name}" do |r|
27
+ #{attributes_to_s}
28
+ end
29
+
30
+ DSL
31
+ end
32
+
33
+ def attributes_to_s
34
+ l = resource.attribute_names.map(&:length).max || 1
35
+
36
+ resource
37
+ .attributes
38
+ .map { |k, v| "#{resource.changes.key?(k) ? display_symbol : ' '} r.#{k.to_s.ljust(l)} = \"#{v.to_s.chomp.truncate(75)}\"" }
39
+ .join("\n ")
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ module Operations
5
+ class Create < Operation
6
+ self.symbol = :+
7
+ self.display_symbol = "+".green
8
+
9
+ def apply(state)
10
+ # Create resource in remote infrastructure
11
+ resource.create!
12
+
13
+ # Add resource to local state
14
+ state.resources << resource
15
+ end
16
+
17
+ protected
18
+
19
+ def attributes_to_s
20
+ l = resource.attribute_names.map(&:length).max || 1
21
+
22
+ resource
23
+ .attributes
24
+ .map { |k, v| "#{resource.changes.key?(k) ? display_symbol : ' '} r.#{k.to_s.ljust(l)} = #{v.blank? ? '(known after apply)' : "\"#{v.to_s.chomp.truncate(75)}\""}" }
25
+ .join("\n ")
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ module Operations
5
+ class Destroy < Operation
6
+ self.symbol = :-
7
+ self.display_symbol = "-".red
8
+
9
+ def apply(state)
10
+ # Destroy resource in remote infrastructure
11
+ resource.destroy!
12
+
13
+ # Delete resource from local state
14
+ state.resources.delete(resource)
15
+ end
16
+
17
+ protected
18
+
19
+ def attributes_to_s
20
+ l = resource.attribute_names.map(&:length).max || 1
21
+
22
+ resource
23
+ .attribute_names
24
+ .map { |k| "#{display_symbol} r.#{k.to_s.ljust(l)} = \"#{resource.send(:"#{k}_was").to_s.chomp.truncate(75)}\"" }
25
+ .join("\n ")
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ module Operations
5
+ class Show < Operation
6
+ self.symbol = nil
7
+ self.display_symbol = nil
8
+
9
+ def apply(state); end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ module Operations
5
+ class Update < Operation
6
+ self.symbol = :~
7
+ self.display_symbol = "~".yellow
8
+
9
+ def apply(state)
10
+ # Update resource in remote infrastructure
11
+ resource.update!
12
+
13
+ # Update resource in local state
14
+ state.resources.delete(resource)
15
+ state.resources << resource
16
+ end
17
+ end
18
+ end
19
+ end
data/lib/kozo/options.rb CHANGED
@@ -2,7 +2,11 @@
2
2
 
3
3
  module Kozo
4
4
  class Options
5
- attr_writer :directory, :verbose
5
+ attr_accessor :help
6
+
7
+ def directory=(path)
8
+ @directory = File.expand_path(path)
9
+ end
6
10
 
7
11
  def directory
8
12
  @directory ||= Dir.pwd
@@ -12,6 +16,10 @@ module Kozo
12
16
  directory.present?
13
17
  end
14
18
 
19
+ def verbose=(value)
20
+ @verbose = value.present?
21
+ end
22
+
15
23
  def verbose
16
24
  @verbose ||= false
17
25
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ class Parser
5
+ attr_reader :directory
6
+
7
+ def initialize(directory)
8
+ @directory = directory
9
+ end
10
+
11
+ def call
12
+ configuration = Configuration.new(directory)
13
+ dsl = DSL.new(configuration)
14
+
15
+ Dir[File.join(directory, "main.kz"), File.join(directory, "**", "*.kz")]
16
+ .uniq
17
+ .reject { |file| ignores.any? { |ignore| file.include?(ignore) } }
18
+ .each { |file| dsl.instance_eval(File.read(file)) }
19
+
20
+ configuration
21
+ end
22
+
23
+ private
24
+
25
+ def ignores
26
+ @ignores ||= begin
27
+ File
28
+ .readlines(File.join(directory, ".kzignore"))
29
+ .map(&:chomp)
30
+ rescue Errno::ENOENT
31
+ []
32
+ end
33
+ end
34
+ end
35
+ end
data/lib/kozo/provider.rb CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  module Kozo
4
4
  class Provider
5
- class_attribute :name
5
+ class_attribute :provider_name
6
+
7
+ def setup; end
8
+
9
+ def ==(other)
10
+ provider_name == other.provider_name
11
+ end
6
12
  end
7
13
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ register("provider.dummy") do
4
+ Kozo::Providers::Dummy::Provider.new
5
+ end
6
+
7
+ register("resource.dummy") do
8
+ Kozo::Providers::Dummy::Resources::Dummy.new
9
+ end
@@ -2,13 +2,9 @@
2
2
 
3
3
  module Kozo
4
4
  module Providers
5
- module Null
5
+ module Dummy
6
6
  class Provider < Kozo::Provider
7
- self.name = "null"
8
-
9
- def ==(_other)
10
- true
11
- end
7
+ self.provider_name = "dummy"
12
8
  end
13
9
  end
14
10
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ module Providers
5
+ module Dummy
6
+ class Resource < Kozo::Resource
7
+ self.provider_name = "dummy"
8
+
9
+ def refresh!; end
10
+
11
+ def create!; end
12
+
13
+ def destroy!; end
14
+
15
+ def update!; end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ module Providers
5
+ module Dummy
6
+ module Resources
7
+ class Dummy < Resource
8
+ self.resource_name = "dummy"
9
+
10
+ attribute :name
11
+ attribute :description
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ register("provider.hcloud") do
4
+ Kozo::Providers::HCloud::Provider.new
5
+ end
6
+
7
+ register("resource.hcloud_ssh_key") do
8
+ Kozo::Providers::HCloud::Resources::SSHKey.new
9
+ end
10
+
11
+ register("resource.hcloud_server") do
12
+ Kozo::Providers::HCloud::Resources::Server.new
13
+ end
@@ -1,12 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "hcloud"
4
+
3
5
  module Kozo
4
6
  module Providers
5
7
  module HCloud
6
8
  class Provider < Kozo::Provider
7
9
  attr_accessor :key
8
10
 
9
- self.name = "hcloud"
11
+ self.provider_name = "hcloud"
12
+
13
+ def setup
14
+ ::HCloud::Client.connection = ::HCloud::Client.new(access_token: key)
15
+ end
10
16
 
11
17
  def ==(other)
12
18
  key == other.key
@@ -4,7 +4,52 @@ module Kozo
4
4
  module Providers
5
5
  module HCloud
6
6
  class Resource < Kozo::Resource
7
- self.provider = "hcloud"
7
+ self.provider_name = "hcloud"
8
+
9
+ attribute :id, type: :integer
10
+
11
+ protected
12
+
13
+ def refresh
14
+ resource = resource_class.find(id)
15
+
16
+ attribute_names
17
+ .excluding(:id)
18
+ .each { |attr| send(:"#{attr}=", resource.send(attr)) }
19
+ end
20
+
21
+ def create
22
+ resource = resource_class.new(**attributes.except(:id))
23
+ resource.create
24
+
25
+ attribute_names
26
+ .each { |attr| send(:"#{attr}=", resource.send(attr)) }
27
+ end
28
+
29
+ def update
30
+ resource = resource_class.find(id)
31
+
32
+ attribute_names
33
+ .excluding(:id)
34
+ .each { |attr| resource.send(:"#{attr}=", send(attr)) }
35
+
36
+ resource.update
37
+
38
+ attribute_names
39
+ .excluding(:id)
40
+ .each { |attr| send(:"#{attr}=", resource.send(attr)) }
41
+ end
42
+
43
+ def destroy
44
+ resource = resource_class.find(id)
45
+ resource.delete
46
+ end
47
+
48
+ private
49
+
50
+ def resource_class
51
+ "HCloud::#{self.class.name.demodulize}".constantize
52
+ end
8
53
  end
9
54
  end
10
55
  end