kozo 0.2.0 → 0.3.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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/CHANGELOG.md +15 -3
  4. data/Gemfile +1 -3
  5. data/Gemfile.lock +44 -51
  6. data/README.md +2 -0
  7. data/kozo.gemspec +2 -2
  8. data/lib/core_ext/boolean.rb +8 -0
  9. data/lib/core_ext/date.rb +15 -0
  10. data/lib/core_ext/enumerable.rb +27 -0
  11. data/lib/core_ext/float.rb +15 -0
  12. data/lib/core_ext/hash.rb +23 -0
  13. data/lib/core_ext/integer.rb +15 -0
  14. data/lib/core_ext/nil_class.rb +8 -0
  15. data/lib/core_ext/string.rb +11 -0
  16. data/lib/core_ext/symbol.rb +11 -0
  17. data/lib/core_ext/time.rb +16 -0
  18. data/lib/kozo/backend.rb +2 -13
  19. data/lib/kozo/backends/git.rb +1 -1
  20. data/lib/kozo/backends/local.rb +12 -4
  21. data/lib/kozo/cli.rb +11 -3
  22. data/lib/kozo/command.rb +6 -15
  23. data/lib/kozo/commands/apply.rb +2 -2
  24. data/lib/kozo/commands/import.rb +4 -2
  25. data/lib/kozo/commands/plan.rb +16 -7
  26. data/lib/kozo/commands/state/delete.rb +39 -0
  27. data/lib/kozo/commands/state/list.rb +21 -0
  28. data/lib/kozo/commands/state/show.rb +30 -0
  29. data/lib/kozo/commands/state/upgrade.rb +45 -0
  30. data/lib/kozo/commands/state.rb +2 -38
  31. data/lib/kozo/concerns/attributes.rb +59 -32
  32. data/lib/kozo/concerns/read_write.rb +45 -0
  33. data/lib/kozo/configuration.rb +28 -17
  34. data/lib/kozo/dsl.rb +15 -0
  35. data/lib/kozo/error.rb +13 -2
  36. data/lib/kozo/inflector.rb +11 -0
  37. data/lib/kozo/logger.rb +13 -7
  38. data/lib/kozo/operation.rb +0 -25
  39. data/lib/kozo/operations/create.rb +12 -6
  40. data/lib/kozo/operations/destroy.rb +12 -6
  41. data/lib/kozo/operations/show.rb +17 -0
  42. data/lib/kozo/operations/update.rb +17 -0
  43. data/lib/kozo/options.rb +24 -0
  44. data/lib/kozo/providers/dummy/resources/dummy.rb +11 -0
  45. data/lib/kozo/providers/hcloud/provider.rb +1 -1
  46. data/lib/kozo/providers/hcloud/resource.rb +30 -7
  47. data/lib/kozo/providers/hcloud/resources/server.rb +16 -8
  48. data/lib/kozo/providers/hcloud/resources/ssh_key.rb +6 -1
  49. data/lib/kozo/reference.rb +58 -0
  50. data/lib/kozo/resource.rb +35 -7
  51. data/lib/kozo/state.rb +11 -5
  52. data/lib/kozo/type.rb +3 -3
  53. data/lib/kozo/types/boolean.rb +0 -4
  54. data/lib/kozo/types/date.rb +0 -4
  55. data/lib/kozo/types/float.rb +0 -4
  56. data/lib/kozo/types/hash.rb +0 -4
  57. data/lib/kozo/types/integer.rb +0 -4
  58. data/lib/kozo/types/reference.rb +17 -0
  59. data/lib/kozo/types/string.rb +0 -4
  60. data/lib/kozo/types/time.rb +0 -4
  61. data/lib/kozo/upgrade/1_initial.rb +9 -0
  62. data/lib/kozo/upgrade/2_remove_kozo_version.rb +11 -0
  63. data/lib/kozo/upgrade.rb +15 -0
  64. data/lib/kozo/version.rb +1 -1
  65. data/lib/kozo.rb +7 -1
  66. metadata +42 -19
  67. data/lib/kozo/concerns/mark.rb +0 -25
@@ -7,17 +7,26 @@ module Kozo
7
7
 
8
8
  attr_reader :operations
9
9
 
10
- def initialize(*_args)
10
+ def initialize(configuration, *_args)
11
+ super(configuration)
12
+
11
13
  @operations = []
12
14
  end
13
15
 
14
16
  def start
15
- @operations += configuration.changes.filter_map do |resource|
16
- if resource.marked_for_creation?
17
- Operations::Create.new(resource)
18
- elsif resource.marked_for_deletion?
19
- Operations::Destroy.new(resource)
17
+ Refresh.new(configuration).start
18
+
19
+ @operations = changes.filter_map do |resource|
20
+ if !resource.id?
21
+ if resource.id_changed?
22
+ # ID changed to nil (resource should be destroyed)
23
+ Operations::Destroy.new(resource)
24
+ else
25
+ # ID is nil (resource should be created)
26
+ Operations::Create.new(resource)
27
+ end
20
28
  elsif resource.changed?
29
+ # Resource should be updated
21
30
  Operations::Update.new(resource)
22
31
  end
23
32
  end
@@ -30,7 +39,7 @@ module Kozo
30
39
 
31
40
  return Kozo.logger.info "\nNo actions have to be performed." if operations.empty?
32
41
 
33
- Kozo.logger.info "\nKozo will perform the following actions:"
42
+ Kozo.logger.info "\nKozo will perform the following actions:\n"
34
43
 
35
44
  operations.each { |o| Kozo.logger.info o.to_s }
36
45
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ module Commands
5
+ class State
6
+ class Delete < State
7
+ self.description = "Delete a resource from the state"
8
+
9
+ attr_reader :address
10
+
11
+ def initialize(configuration, *args)
12
+ @configuration = configuration
13
+ @address = args.shift
14
+
15
+ raise UsageError, "address not specified" unless address
16
+ end
17
+
18
+ def start
19
+ resource = state
20
+ .resources
21
+ .find { |r| r.address == address }
22
+
23
+ raise StateError, "no such resource address: #{address}" unless resource
24
+
25
+ state
26
+ .resources
27
+ .delete_if { |r| r.address == address }
28
+
29
+ Kozo.logger.info resource.address
30
+
31
+ # Write state
32
+ configuration
33
+ .backend
34
+ .state = state
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ module Commands
5
+ class State
6
+ class List < State
7
+ self.description = "List resources in the state"
8
+
9
+ def initialize(configuration, *_args)
10
+ @configuration = configuration
11
+ end
12
+
13
+ def start
14
+ state
15
+ .resources
16
+ .each { |r| Kozo.logger.info r.address }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ module Commands
5
+ class State
6
+ class Show < State
7
+ self.description = "Show a resource in the state"
8
+
9
+ attr_reader :address
10
+
11
+ def initialize(configuration, *args)
12
+ @configuration = configuration
13
+ @address = args.shift
14
+
15
+ raise UsageError, "address not specified" unless address
16
+ end
17
+
18
+ def start
19
+ resource = state
20
+ .resources
21
+ .find { |r| r.address == address }
22
+
23
+ raise StateError, "no such resource address: #{address}" unless resource
24
+
25
+ Kozo.logger.info Operations::Show.new(resource).to_s
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ module Commands
5
+ class State
6
+ class Upgrade < State
7
+ self.description = "Upgrade the state file to the latest version"
8
+
9
+ def initialize(configuration, *_args)
10
+ @configuration = configuration
11
+ end
12
+
13
+ def start
14
+ return Kozo.logger.info "State file already up to date." if state.compatible?
15
+
16
+ ((state.version + 1)..Kozo::State::VERSION).each do |version|
17
+ file = Dir[Kozo.root.join("lib/kozo/upgrade/#{version}_*")].first
18
+
19
+ raise NotImplementedError, "upgrade file to version #{version} not found" unless file
20
+
21
+ Kozo.logger.info "Upgrading state file to version #{version}"
22
+
23
+ klass = File
24
+ .basename(file, ".rb")
25
+ .delete_prefix("#{version}_")
26
+ .prepend("kozo/upgrade/")
27
+ .camelize
28
+ .constantize
29
+
30
+ # Write state
31
+ configuration.backend.state = klass
32
+ .new(state)
33
+ .upgrade
34
+ end
35
+ end
36
+
37
+ def state
38
+ @state ||= configuration
39
+ .backend
40
+ .state(verify: false)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -7,7 +7,7 @@ module Kozo
7
7
 
8
8
  attr_reader :subcommand
9
9
 
10
- def initialize(*args)
10
+ def initialize(configuration, *args)
11
11
  subcommand = args.shift
12
12
 
13
13
  raise UsageError unless subcommand
@@ -16,49 +16,13 @@ module Kozo
16
16
 
17
17
  raise UsageError, "unknown subcommand: state #{subcommand}" unless klass
18
18
 
19
- @subcommand = klass.new(*args)
19
+ @subcommand = klass.new(configuration, *args)
20
20
  end
21
21
 
22
22
  def start
23
23
  subcommand
24
24
  .start
25
25
  end
26
-
27
- class List < State
28
- self.description = "List resources in the state"
29
-
30
- def initialize(*_args); end
31
-
32
- def start
33
- state
34
- .resources
35
- .each { |r| Kozo.logger.info r.address }
36
- end
37
- end
38
-
39
- class Show < State
40
- self.description = "Show a resource in the state"
41
-
42
- attr_reader :address
43
-
44
- def initialize(*args)
45
- address = args.shift
46
-
47
- raise UsageError, "address not specified" unless address
48
-
49
- @address = address
50
- end
51
-
52
- def start
53
- resource = state
54
- .resources
55
- .find { |r| r.address == address }
56
-
57
- raise StateError, "no such resource address: #{address}" unless resource
58
-
59
- Kozo.logger.info Operations::Show.new(resource).to_s
60
- end
61
- end
62
26
  end
63
27
  end
64
28
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rubocop:disable Metrics/BlockLength
3
4
  module Kozo
4
5
  module Attributes
5
6
  extend ActiveSupport::Concern
@@ -17,19 +18,54 @@ module Kozo
17
18
  end
18
19
 
19
20
  def write_attribute(name, value)
21
+ attribute_types[name] => { multiple:, type:, wrapped: }
22
+
20
23
  try(:track_change!, name, value)
21
24
 
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
25
+ # Unwrap value(s) (call #name on receiver)
26
+ value = value.send_wrap { |v| v.try(:name) || v } if wrapped
27
+
28
+ # Cast value(s)
29
+ value = value.send_wrap { |v| type.cast(v) }
30
+
31
+ # Convert value(s) to array
32
+ value = Array(value) if multiple
27
33
 
34
+ # Set value(s)
28
35
  instance_variable_set(:"@#{name}", value)
29
36
  end
37
+
38
+ def attributes
39
+ attribute_names
40
+ .to_h { |name| [name, read_attribute(name)] }
41
+ end
42
+
43
+ def readable_attributes
44
+ readable_attribute_names
45
+ .to_h { |name| [name, read_attribute(name)] }
46
+ end
47
+
48
+ def writeable_attributes
49
+ writeable_attribute_names
50
+ .to_h { |name| [name, read_attribute(name)] }
51
+ end
52
+
53
+ def creatable_attributes
54
+ creatable_attribute_names
55
+ .to_h { |name| [name, read_attribute(name).send_wrap(:as_h)] }
56
+ end
57
+
58
+ def updatable_attributes
59
+ updatable_attribute_names
60
+ .to_h { |name| [name, read_attribute(name).send_wrap(:as_h)] }
61
+ end
62
+
63
+ delegate :attribute_names,
64
+ :readable_attribute_names,
65
+ :writeable_attribute_names,
66
+ to: :class
30
67
  end
31
68
 
32
- # rubocop:disable Metrics/BlockLength
33
69
  class_methods do
34
70
  def inherited(sub_class)
35
71
  super
@@ -43,12 +79,13 @@ module Kozo
43
79
 
44
80
  try(:track, name)
45
81
 
46
- options = attribute_types[name] = {
82
+ attribute_types[name] = {
47
83
  multiple: !!options[:multiple],
48
- attribute: !!options.fetch(:attribute, true),
49
- argument: !!options.fetch(:argument, true),
84
+ wrapped: !!options.fetch(:wrapped, false),
50
85
  type: type,
51
86
  default: options[:default],
87
+ read: read?,
88
+ write: write?,
52
89
  }
53
90
 
54
91
  # Define getter
@@ -57,43 +94,33 @@ module Kozo
57
94
  define_method(:"#{name}?") { !!read_attribute(name) }
58
95
  end
59
96
 
60
- # Set visibility to public if it's an attribute
61
- options[:attribute] ? public(name) : private(name)
97
+ # Set getter visibility to private if it's writeonly
98
+ private(name) if writeonly?
62
99
 
63
100
  # Define setter
64
101
  define_method(:"#{name}=") { |value| write_attribute(name, value) } unless method_defined? :"#{name}="
65
102
 
66
- # Set visibility to public if it's an argument
67
- options[:argument] ? public(:"#{name}=") : private(:"#{name}=")
103
+ # Set setter visibility to private if it's readonly
104
+ private(:"#{name}=") if readonly?
68
105
  end
69
106
 
70
107
  def attribute_names
71
108
  @attribute_names ||= attribute_types
72
- .select { |_k, v| v[:attribute] }
73
109
  .keys
74
110
  end
75
111
 
76
- def argument_names
77
- @argument_names ||= attribute_types
78
- .select { |_k, v| v[:argument] }
112
+ def readable_attribute_names
113
+ attribute_types
114
+ .select { |_k, v| v[:read] }
79
115
  .keys
80
116
  end
81
- end
82
- # rubocop:enable Metrics/BlockLength
83
117
 
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
118
+ def writeable_attribute_names
119
+ attribute_types
120
+ .select { |_k, v| v[:write] }
121
+ .keys
122
+ end
94
123
  end
95
-
96
- delegate :attribute_names, to: :class
97
- delegate :argument_names, to: :class
98
124
  end
99
125
  end
126
+ # rubocop:enable Metrics/BlockLength
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ module ReadWrite
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ class_attribute :mode, default: :readwrite
9
+ end
10
+
11
+ class_methods do
12
+ def readwrite
13
+ self.mode = :readwrite
14
+ end
15
+
16
+ def readwrite?
17
+ mode == :readwrite
18
+ end
19
+
20
+ def readonly
21
+ self.mode = :read
22
+ end
23
+
24
+ def readonly?
25
+ mode == :read
26
+ end
27
+
28
+ def read?
29
+ mode == :read || mode == :readwrite
30
+ end
31
+
32
+ def writeonly
33
+ self.mode = :write
34
+ end
35
+
36
+ def writeonly?
37
+ mode == :write
38
+ end
39
+
40
+ def write?
41
+ mode == :write || mode == :readwrite
42
+ end
43
+ end
44
+ end
45
+ end
@@ -11,42 +11,53 @@ module Kozo
11
11
  @resources = Set.new
12
12
  end
13
13
 
14
- def backend
15
- @backend ||= Kozo
16
- .container
17
- .resolve("backend.local", self, directory)
18
- end
19
-
20
14
  def changes
21
15
  @changes ||= begin
22
16
  # Copy resources in state
23
- changes = backend
24
- .state
17
+ changes = state
25
18
  .resources
26
19
  .map(&:dup)
27
20
  .each(&:clear_changes)
28
21
  .each do |resource|
22
+ # Find resource in configuration
29
23
  configured = resources.find { |r| r.address == resource.address }
30
24
 
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
25
+ if configured
26
+ # Assign updated attributes (mark for update)
27
+ resource.assign_attributes(configured&.readable_attributes&.slice(*configured&.updatable_attribute_names))
28
+ else
29
+ # Set attributes to nil (mark for destruction)
30
+ resource.assign_attributes(resource.readable_attributes.transform_values { nil })
31
+ end
36
32
  end
37
33
 
38
- # Append resources not in state
34
+ # Append resources not in state (mark for creation)
39
35
  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!)
36
+ .reject { |r| state.resources.any? { |res| res.address == r.address } }
37
+ .map { |r| r.class.new(state_name: r.state_name, **r.writeable_attributes) }
38
+
39
+ # Resolve references
40
+ changes
41
+ .each { |c| c.writeable_attributes.each_value { |r| r.send_wrap(:try, :resolve, self) } }
43
42
 
44
43
  changes
45
44
  end
46
45
  end
47
46
 
47
+ def backend
48
+ @backend ||= Kozo
49
+ .container
50
+ .resolve("backend.local", self, directory)
51
+ end
52
+
53
+ delegate :state, to: :backend
54
+
48
55
  def to_s
49
56
  "directory: #{directory}"
50
57
  end
58
+
59
+ def inspect
60
+ "#<#{self.class.name} #{self}>"
61
+ end
51
62
  end
52
63
  end
data/lib/kozo/dsl.rb CHANGED
@@ -45,6 +45,21 @@ module Kozo
45
45
  configuration.resources << resource
46
46
  end
47
47
 
48
+ def method_missing(method_name, *arguments, &block)
49
+ resource_class = Kozo
50
+ .container
51
+ .resolve("resource.#{method_name}")
52
+ .class
53
+
54
+ Reference.new(resource_class: resource_class)
55
+ rescue Dinja::Container::DependencyNotRegistered
56
+ super
57
+ end
58
+
59
+ def respond_to_missing?(method_name, include_private = false)
60
+ Kozo.container.resolve!("resource.#{method_name}").present? || super
61
+ end
62
+
48
63
  private
49
64
 
50
65
  def resolve(resource, type, *args)
data/lib/kozo/error.rb CHANGED
@@ -1,13 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kozo
4
+ # Base class
4
5
  class Error < StandardError; end
5
6
 
6
- class InvalidResource < Error; end
7
-
7
+ # Raised when usage is printed
8
8
  class ExitError < Error; end
9
9
 
10
+ # Raised when CLI arguments are invalid
10
11
  class UsageError < Error; end
11
12
 
13
+ # Raised when a resource definition was invalid
14
+ class InvalidResource < Error; end
15
+
16
+ # Raised when a resource cannot match constraints (uniqueness, etc.)
17
+ class ResourceError < Error; end
18
+
19
+ # Raised when state invalid or resource not found in state
12
20
  class StateError < Error; end
21
+
22
+ # Raised when performing an operation on a resource that was not persisted yet
23
+ class NotPersisted < Error; end
13
24
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ class Inflector < Zeitwerk::GemInflector
5
+ def camelize(basename, abspath)
6
+ return super(Regexp.last_match(2), abspath) if basename =~ /\A(\d+)_(.*)/ && abspath.include?("kozo/upgrade")
7
+
8
+ super
9
+ end
10
+ end
11
+ end
data/lib/kozo/logger.rb CHANGED
@@ -12,16 +12,12 @@ module Kozo
12
12
  private
13
13
 
14
14
  def level
15
- Kozo.options.verbose? ? "debug" : ENV.fetch("LOG_LEVEL", "info")
15
+ Kozo.options.verbose? ? "debug" : "info"
16
16
  end
17
17
 
18
18
  def formatter
19
- Formatter.new
20
- end
21
-
22
- class Formatter < ::Logger::Formatter
23
- def call(severity, _time, _progname, msg)
24
- abort("#{File.basename($PROGRAM_NAME)}: #{msg[0].downcase}#{msg[1..]}".white.on_red) if severity == "FATAL"
19
+ proc do |severity, _time, _progname, msg|
20
+ abort("#{File.basename($PROGRAM_NAME)}: #{msg}".white.on_red) if severity == "FATAL"
25
21
 
26
22
  msg = "#{msg}\n"
27
23
  msg = msg.yellow if severity == "DEBUG"
@@ -30,5 +26,15 @@ module Kozo
30
26
  msg
31
27
  end
32
28
  end
29
+
30
+ class Debug < Logger
31
+ def level
32
+ Kozo.options.debug? ? "debug" : "fatal"
33
+ end
34
+
35
+ def formatter
36
+ ->(_severity, _time, _progname, msg) { msg.magenta }
37
+ end
38
+ end
33
39
  end
34
40
  end
@@ -13,30 +13,5 @@ module Kozo
13
13
  def apply(_state)
14
14
  raise NotImplementedError
15
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
16
  end
42
17
  end
@@ -14,15 +14,21 @@ module Kozo
14
14
  state.resources << resource
15
15
  end
16
16
 
17
- protected
18
-
19
- def attributes_to_s
17
+ def to_s
20
18
  l = resource.attribute_names.map(&:length).max || 1
21
19
 
22
- resource
20
+ attrs = resource
23
21
  .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 ")
22
+ .map { |k, v| " #{resource.changes.key?(k) ? display_symbol : ' '} r.#{k.to_s.ljust(l)} = #{v.blank? && resource.writeable_attribute_names.exclude?(k) ? '(known after apply)' : (v.as_s.indent(4)[4..]).to_s}" }
23
+ .join("\n")
24
+
25
+ <<~DSL.chomp
26
+ #{"# #{resource.address}:".bold}
27
+ #{display_symbol} resource "#{resource.resource_name}", "#{resource.state_name}" do |r|
28
+ #{attrs}
29
+ end
30
+
31
+ DSL
26
32
  end
27
33
  end
28
34
  end
@@ -14,15 +14,21 @@ module Kozo
14
14
  state.resources.delete(resource)
15
15
  end
16
16
 
17
- protected
18
-
19
- def attributes_to_s
17
+ def to_s
20
18
  l = resource.attribute_names.map(&:length).max || 1
21
19
 
22
- resource
20
+ attrs = resource
23
21
  .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 ")
22
+ .map { |k| " #{display_symbol} r.#{k.to_s.ljust(l)} = #{resource.send(:"#{k}_was").as_s.indent(4)[4..]}" }
23
+ .join("\n")
24
+
25
+ <<~DSL.chomp
26
+ #{"# #{resource.address}:".bold}
27
+ #{display_symbol} resource "#{resource.resource_name}", "#{resource.state_name}" do |r|
28
+ #{attrs}
29
+ end
30
+
31
+ DSL
26
32
  end
27
33
  end
28
34
  end