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,6 +7,23 @@ module Kozo
7
7
  self.display_symbol = nil
8
8
 
9
9
  def apply(state); end
10
+
11
+ def to_s
12
+ l = resource.attribute_names.map(&:length).max || 1
13
+
14
+ attrs = resource
15
+ .attributes
16
+ .map { |k, v| " r.#{k.to_s.ljust(l)} = #{v.as_s.indent(2)[2..]}" }
17
+ .join("\n")
18
+
19
+ <<~DSL.chomp
20
+ #{"# #{resource.address}:".bold}
21
+ resource "#{resource.resource_name}", "#{resource.state_name}" do |r|
22
+ #{attrs}
23
+ end
24
+
25
+ DSL
26
+ end
10
27
  end
11
28
  end
12
29
  end
@@ -14,6 +14,23 @@ module Kozo
14
14
  state.resources.delete(resource)
15
15
  state.resources << resource
16
16
  end
17
+
18
+ def to_s
19
+ l = resource.attribute_names.map(&:length).max || 1
20
+
21
+ attrs = resource
22
+ .attributes
23
+ .map { |k, v| " #{resource.changes.key?(k) ? display_symbol : ' '} r.#{k.to_s.ljust(l)} = #{resource.changes.key?(k) ? "#{resource.changes[k].first.as_s.indent(4)[4..]} -> #{v.as_s.indent(4)[4..]}" : v.as_s.indent(4)[4..]}" }
24
+ .join("\n")
25
+
26
+ <<~DSL.chomp
27
+ #{"# #{resource.address}:".bold}
28
+ #{display_symbol} resource "#{resource.resource_name}", "#{resource.state_name}" do |r|
29
+ #{attrs}
30
+ end
31
+
32
+ DSL
33
+ end
17
34
  end
18
35
  end
19
36
  end
data/lib/kozo/options.rb CHANGED
@@ -28,6 +28,30 @@ module Kozo
28
28
  verbose.present?
29
29
  end
30
30
 
31
+ def debug=(value)
32
+ @debug = value.present?
33
+ end
34
+
35
+ def debug
36
+ @debug ||= false
37
+ end
38
+
39
+ def debug?
40
+ debug.present?
41
+ end
42
+
43
+ def dry_run=(value)
44
+ @dry_run = value.present?
45
+ end
46
+
47
+ def dry_run
48
+ @dry_run ||= false
49
+ end
50
+
51
+ def dry_run?
52
+ dry_run.present?
53
+ end
54
+
31
55
  def [](key)
32
56
  send(key)
33
57
  end
@@ -7,8 +7,19 @@ module Kozo
7
7
  class Dummy < Resource
8
8
  self.resource_name = "dummy"
9
9
 
10
+ self.creatable_attribute_names = [:name, :labels, :description]
11
+ self.updatable_attribute_names = [:name, :labels, :description]
12
+
10
13
  attribute :name
11
14
  attribute :description
15
+
16
+ attribute :labels, multiple: true
17
+
18
+ readonly
19
+
20
+ attribute :location
21
+
22
+ attribute :locked, type: :boolean, default: false
12
23
  end
13
24
  end
14
25
  end
@@ -11,7 +11,7 @@ module Kozo
11
11
  self.provider_name = "hcloud"
12
12
 
13
13
  def setup
14
- ::HCloud::Client.connection = ::HCloud::Client.new(access_token: key)
14
+ ::HCloud::Client.connection = ::HCloud::Client.new(access_token: key, logger: Kozo.debug_logger)
15
15
  end
16
16
 
17
17
  def ==(other)
@@ -6,43 +6,66 @@ module Kozo
6
6
  class Resource < Kozo::Resource
7
7
  self.provider_name = "hcloud"
8
8
 
9
+ readonly
10
+
9
11
  attribute :id, type: :integer
10
12
 
13
+ readwrite
14
+
11
15
  protected
12
16
 
13
17
  def refresh
18
+ raise NotPersisted, "resource is not yet persisted" unless id
19
+
14
20
  resource = resource_class.find(id)
15
21
 
16
- attribute_names
22
+ # Set local attributes from remote resource
23
+ readable_attribute_names
17
24
  .excluding(:id)
18
25
  .each { |attr| send(:"#{attr}=", resource.send(attr)) }
26
+ rescue ::HCloud::Errors::NotFound => e
27
+ raise StateError, "#{address}: #{e.message}"
19
28
  end
20
29
 
21
30
  def create
22
- resource = resource_class.new(**attributes.except(:id))
31
+ resource = resource_class.new(creatable_attributes)
23
32
  resource.create
24
33
 
25
- attribute_names
34
+ # Set local attributes from remote resource
35
+ readable_attribute_names
26
36
  .each { |attr| send(:"#{attr}=", resource.send(attr)) }
37
+ rescue ::HCloud::Errors::UniquenessError => e
38
+ raise ResourceError, "#{address}: #{e.message}"
27
39
  end
28
40
 
29
41
  def update
42
+ raise NotPersisted unless id
43
+
30
44
  resource = resource_class.find(id)
31
45
 
32
- attribute_names
33
- .excluding(:id)
46
+ # Set remote attributes from local resource
47
+ updatable_attribute_names
34
48
  .each { |attr| resource.send(:"#{attr}=", send(attr)) }
35
49
 
36
50
  resource.update
37
51
 
38
- attribute_names
52
+ readable_attribute_names
39
53
  .excluding(:id)
40
54
  .each { |attr| send(:"#{attr}=", resource.send(attr)) }
55
+ rescue ::HCloud::Errors::NotFound => e
56
+ raise StateError, "#{address}: #{e.message}"
57
+ rescue ::HCloud::Errors::UniquenessError => e
58
+ raise ResourceError, "#{address}: #{e.message}"
41
59
  end
42
60
 
43
61
  def destroy
44
- resource = resource_class.find(id)
62
+ # Use rid, because id will have been blanked
63
+ raise NotPersisted unless rid
64
+
65
+ resource = resource_class.find(rid)
45
66
  resource.delete
67
+ rescue ::HCloud::Errors::NotFound => e
68
+ raise StateError, "#{address}: #{e.message}"
46
69
  end
47
70
 
48
71
  private
@@ -7,21 +7,29 @@ module Kozo
7
7
  class Server < Resource
8
8
  self.resource_name = "hcloud_server"
9
9
 
10
+ self.creatable_attribute_names = [:name, :image, :server_type, :location, :datacenter, :user_data, :ssh_keys, :labels]
11
+ self.updatable_attribute_names = [:name, :ssh_keys, :labels]
12
+
10
13
  attribute :name
11
- attribute :image
12
- attribute :server_type
13
- attribute :location
14
- attribute :datacenter
15
14
 
16
- attribute :user_data
15
+ attribute :image, wrapped: true
16
+ attribute :server_type, wrapped: true
17
+ attribute :location, wrapped: true
18
+ attribute :datacenter, wrapped: true
17
19
 
18
20
  attribute :labels, type: :hash
19
21
 
20
- attribute :ssh_keys, multiple: true
22
+ readonly
23
+
24
+ attribute :locked, type: :boolean
21
25
 
22
- attribute :locked, argument: false, type: :boolean
26
+ attribute :created, type: :time
27
+
28
+ writeonly
29
+
30
+ attribute :user_data
23
31
 
24
- attribute :created, argument: false, type: :time
32
+ attribute :ssh_keys, multiple: true, type: :reference
25
33
  end
26
34
  end
27
35
  end
@@ -7,11 +7,16 @@ module Kozo
7
7
  class SSHKey < Resource
8
8
  self.resource_name = "hcloud_ssh_key"
9
9
 
10
+ self.creatable_attribute_names = [:name, :public_key, :labels]
11
+ self.updatable_attribute_names = [:name, :labels]
12
+
10
13
  attribute :name
11
14
  attribute :public_key
12
15
  attribute :labels, type: :hash
13
16
 
14
- attribute :created, type: :time, argument: false
17
+ readonly
18
+
19
+ attribute :created, type: :time
15
20
  end
16
21
  end
17
22
  end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ class Reference
5
+ attr_reader :resource_class, :state_name, :id
6
+
7
+ def initialize(resource_class: nil, id: nil)
8
+ @resource_class = resource_class
9
+ @id = id
10
+ end
11
+
12
+ def method_missing(method_name, *_arguments)
13
+ # Raise NoMethodError when `state_name` was already set (usually because of a programming error)
14
+ return super if @state_name
15
+
16
+ @state_name = method_name.to_s
17
+
18
+ self
19
+ end
20
+
21
+ def resolve(configuration)
22
+ raise StateError, "no state name specified" unless state_name
23
+
24
+ resource = configuration
25
+ .state
26
+ .resources
27
+ .find { |r| r.address == address }
28
+
29
+ raise StateError, "no such resource address: #{address}" unless resource
30
+
31
+ @id = resource.id
32
+ end
33
+
34
+ def respond_to_missing?(_method_name, _include_private = false)
35
+ true
36
+ end
37
+
38
+ def to_h
39
+ {
40
+ id: id,
41
+ }
42
+ end
43
+
44
+ def as_h
45
+ id
46
+ end
47
+
48
+ def as_s
49
+ id&.as_s || "(known after apply)"
50
+ end
51
+
52
+ private
53
+
54
+ def address
55
+ "#{resource_class.resource_name}.#{state_name}"
56
+ end
57
+ end
58
+ end
data/lib/kozo/resource.rb CHANGED
@@ -3,24 +3,36 @@
3
3
  module Kozo
4
4
  class Resource
5
5
  include Attributes
6
-
7
6
  include Assignment
8
- include Mark
9
7
  include Track
8
+ include ReadWrite
10
9
 
11
10
  attr_accessor :provider, :state_name
12
11
 
12
+ readonly
13
+
13
14
  attribute :id
14
15
 
16
+ readwrite
17
+
15
18
  class_attribute :resource_name, :provider_name
16
19
 
20
+ class_attribute :creatable_attribute_names,
21
+ :updatable_attribute_names,
22
+ default: []
23
+
17
24
  def address
18
25
  "#{resource_name}.#{state_name}"
19
26
  end
20
27
 
28
+ # Catchall for id, also works when the resource will be deleted
29
+ def rid
30
+ id || id_was
31
+ end
32
+
21
33
  def ==(other)
22
34
  self.class == other.class &&
23
- id == other.id &&
35
+ rid == other.rid &&
24
36
  state_name == other.state_name
25
37
  end
26
38
 
@@ -30,10 +42,26 @@ module Kozo
30
42
  [self.class, id].hash
31
43
  end
32
44
 
45
+ def to_dsl(prefix = nil)
46
+ l = attribute_names.map(&:length).max || 1
47
+
48
+ attribute_dsl = attributes
49
+ .map { |k, v| " #{changes.key?(k) ? prefix : ' '}r.#{k.to_s.ljust(l)} = #{v.as_s.indent(4)[4..]}" }
50
+ .join("\n")
51
+
52
+ <<~DSL.chomp
53
+ #{"# #{address}:".bold}
54
+ #{prefix}resource "#{resource_name}", "#{state_name}" do |r|
55
+ #{attribute_dsl}
56
+ end
57
+
58
+ DSL
59
+ end
60
+
33
61
  def to_h
34
62
  {
35
63
  meta: meta,
36
- data: attributes,
64
+ data: readable_attributes,
37
65
  }
38
66
  end
39
67
 
@@ -62,7 +90,7 @@ module Kozo
62
90
  def create!
63
91
  Kozo.logger.info "#{address}: creating resource"
64
92
 
65
- create
93
+ create unless Kozo.options.dry_run?
66
94
 
67
95
  Kozo.logger.info "#{address}: created resource"
68
96
  end
@@ -73,7 +101,7 @@ module Kozo
73
101
  def update!
74
102
  Kozo.logger.info "#{address}: updating resource"
75
103
 
76
- update
104
+ update unless Kozo.options.dry_run?
77
105
 
78
106
  Kozo.logger.info "#{address}: updated resource"
79
107
  end
@@ -84,7 +112,7 @@ module Kozo
84
112
  def destroy!
85
113
  Kozo.logger.info "#{address}: destroying resource"
86
114
 
87
- destroy
115
+ destroy unless Kozo.options.dry_run?
88
116
 
89
117
  Kozo.logger.info "#{address}: destroyed resource"
90
118
  end
data/lib/kozo/state.rb CHANGED
@@ -2,12 +2,19 @@
2
2
 
3
3
  module Kozo
4
4
  class State
5
- VERSION = 1
5
+ VERSION = 2
6
6
 
7
- attr_accessor :resources
7
+ attr_accessor :resources, :version
8
8
 
9
- def initialize(resources = nil)
10
- @resources = Set.new(resources)
9
+ def initialize(resources = [], version = VERSION, verify: true)
10
+ @resources = Array(resources)
11
+ @version = version
12
+
13
+ raise StateError, "unexpected version in state: got #{version}, expected #{State::VERSION}\nRun `#{File.basename($PROGRAM_NAME)} state upgrade` to upgrade your state file" unless compatible? || !verify
14
+ end
15
+
16
+ def compatible?
17
+ version == State::VERSION
11
18
  end
12
19
 
13
20
  def ==(other)
@@ -17,7 +24,6 @@ module Kozo
17
24
  def to_h
18
25
  {
19
26
  version: VERSION,
20
- kozo_version: Kozo::VERSION,
21
27
  resources: resources.map(&:to_h),
22
28
  }
23
29
  end
data/lib/kozo/type.rb CHANGED
@@ -8,8 +8,8 @@ module Kozo
8
8
  raise ArgumentError, "No such type: #{type}"
9
9
  end
10
10
 
11
- def self.cast(value); end
12
-
13
- def self.serialize(value); end
11
+ def self.cast(value)
12
+ value
13
+ end
14
14
  end
15
15
  end
@@ -21,10 +21,6 @@ module Kozo
21
21
 
22
22
  !FALSE_VALUES.include?(value)
23
23
  end
24
-
25
- def self.serialize(value)
26
- value
27
- end
28
24
  end
29
25
  end
30
26
  end
@@ -11,10 +11,6 @@ module Kozo
11
11
  rescue ::Date::Error => e
12
12
  raise ArgumentError, e
13
13
  end
14
-
15
- def self.serialize(value)
16
- value&.iso8601
17
- end
18
14
  end
19
15
  end
20
16
  end
@@ -8,10 +8,6 @@ module Kozo
8
8
 
9
9
  send :Float, value
10
10
  end
11
-
12
- def self.serialize(value)
13
- value
14
- end
15
11
  end
16
12
  end
17
13
  end
@@ -11,10 +11,6 @@ module Kozo
11
11
  rescue TypeError, NoMethodError => e
12
12
  raise ArgumentError, e
13
13
  end
14
-
15
- def self.serialize(value)
16
- value
17
- end
18
14
  end
19
15
  end
20
16
  end
@@ -8,10 +8,6 @@ module Kozo
8
8
 
9
9
  send :Integer, value
10
10
  end
11
-
12
- def self.serialize(value)
13
- value
14
- end
15
11
  end
16
12
  end
17
13
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ module Types
5
+ class Reference < Type
6
+ def self.cast(value)
7
+ return value if value.is_a? Kozo::Reference
8
+ return unless value.is_a? Resource
9
+
10
+ # TODO: infer configuration
11
+ Kozo::Reference
12
+ .new(resource_class: value.class, id: value.id)
13
+ .send(value.state_name)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -8,10 +8,6 @@ module Kozo
8
8
 
9
9
  send :String, value
10
10
  end
11
-
12
- def self.serialize(value)
13
- value
14
- end
15
11
  end
16
12
  end
17
13
  end
@@ -9,10 +9,6 @@ module Kozo
9
9
 
10
10
  ::Time.parse(value)
11
11
  end
12
-
13
- def self.serialize(value)
14
- value&.iso8601
15
- end
16
12
  end
17
13
  end
18
14
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ class Upgrade
5
+ class Initial < Kozo::Upgrade
6
+ def upgrade; end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ class Upgrade
5
+ class RemoveKozoVersion < Kozo::Upgrade
6
+ def upgrade
7
+ state
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ class Upgrade
5
+ attr_reader :state
6
+
7
+ def initialize(state)
8
+ @state = state
9
+ end
10
+
11
+ def upgrade
12
+ raise NotImplementedError
13
+ end
14
+ end
15
+ end
data/lib/kozo/version.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  module Kozo
4
4
  module Version
5
5
  MAJOR = 0
6
- MINOR = 2
6
+ MINOR = 3
7
7
  PATCH = 0
8
8
  PRE = nil
9
9
 
data/lib/kozo.rb CHANGED
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/all"
4
- require "active_model"
5
4
  require "colorize"
6
5
  require "dinja"
7
6
  require "zeitwerk"
8
7
 
8
+ require "kozo/inflector"
9
+
9
10
  require "byebug" if ENV["ENV"] == "development"
10
11
 
11
12
  module Kozo
@@ -28,8 +29,13 @@ module Kozo
28
29
  @logger ||= Logger.new
29
30
  end
30
31
 
32
+ def debug_logger
33
+ @debug_logger ||= Logger::Debug.new
34
+ end
35
+
31
36
  def setup
32
37
  @loader = Zeitwerk::Loader.for_gem
38
+ loader.inflector = Kozo::Inflector.new(__FILE__)
33
39
 
34
40
  # Register inflections
35
41
  instance_eval(File.read(root.join("config/inflections.rb")))