kozo 0.2.0 → 0.3.0

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