kozo 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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