kube_cluster 0.2.0 → 0.2.1

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +43 -0
  3. data/.github/workflows/tag-gem-version-bump.yml +47 -0
  4. data/.gitignore +2 -0
  5. data/Gemfile.lock +48 -52
  6. data/bin/console +3 -0
  7. data/bin/dev +4 -0
  8. data/docker-compose.yml +26 -0
  9. data/examples/01-basic-redis-pod/manifest.rb +60 -0
  10. data/examples/database/manifest.rb +238 -0
  11. data/examples/version2/demo.rb +87 -0
  12. data/examples/version2/helpers.rb +18 -0
  13. data/examples/version2/my_app.rb +45 -0
  14. data/examples/version2/postgresql.rb +81 -0
  15. data/examples/version2/ruby_on_rails.rb +31 -0
  16. data/examples/web-app/manifest.rb +215 -0
  17. data/flake.lock +3 -3
  18. data/flake.nix +6 -0
  19. data/kube_cluster.gemspec +3 -1
  20. data/lib/kube/cli/cluster.rb +41 -0
  21. data/lib/kube/cluster/connection.rb +18 -0
  22. data/lib/kube/cluster/instance.rb +21 -0
  23. data/lib/kube/cluster/manifest/middleware/annotations.rb +32 -0
  24. data/lib/kube/cluster/manifest/middleware/hpa_for_deployment.rb +109 -0
  25. data/lib/kube/cluster/manifest/middleware/ingress_for_service.rb +89 -0
  26. data/lib/kube/cluster/manifest/middleware/labels.rb +59 -0
  27. data/lib/kube/cluster/manifest/middleware/namespace.rb +31 -0
  28. data/lib/kube/cluster/manifest/middleware/pod_anti_affinity.rb +61 -0
  29. data/lib/kube/cluster/manifest/middleware/resource_preset.rb +64 -0
  30. data/lib/kube/cluster/manifest/middleware/security_context.rb +84 -0
  31. data/lib/kube/cluster/manifest/middleware/service_for_deployment.rb +69 -0
  32. data/lib/kube/cluster/manifest/middleware.rb +178 -0
  33. data/lib/kube/cluster/manifest/stack.rb +56 -0
  34. data/lib/kube/cluster/manifest.rb +76 -0
  35. data/lib/kube/cluster/resource/dirty_tracking.rb +113 -0
  36. data/lib/kube/cluster/resource/persistence.rb +67 -0
  37. data/lib/kube/cluster/resource.rb +21 -0
  38. data/lib/kube/cluster/version.rb +1 -1
  39. data/lib/kube/cluster.rb +13 -7
  40. data/lib/kube/errors.rb +57 -0
  41. metadata +63 -17
  42. data/Rakefile +0 -11
  43. data/TREE_PLAN.md +0 -513
  44. data/bin/generate-command-schema-v1 +0 -44
  45. data/data/kubectl-command-tree-v1-minimal.json +0 -125
  46. data/data/kubectl-command-tree-v1.json +0 -1469
  47. data/examples/quick-repl/docker-compose.yml +0 -52
  48. data/exe/kube_cluster +0 -6
  49. data/lib/kube/cluster/command_node.rb +0 -89
  50. data/lib/kube/cluster/ctl.rb +0 -33
  51. data/lib/kube/cluster/query_builder.rb +0 -35
  52. data/lib/kube/cluster/resource_selector.rb +0 -19
  53. data/lib/kube/cluster/tree_node.rb +0 -51
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "manifest/stack"
4
+ require_relative "manifest/middleware"
5
+ require_relative "manifest/middleware/namespace"
6
+ require_relative "manifest/middleware/labels"
7
+ require_relative "manifest/middleware/annotations"
8
+ require_relative "manifest/middleware/resource_preset"
9
+ require_relative "manifest/middleware/security_context"
10
+ require_relative "manifest/middleware/pod_anti_affinity"
11
+ require_relative "manifest/middleware/service_for_deployment"
12
+ require_relative "manifest/middleware/ingress_for_service"
13
+ require_relative "manifest/middleware/hpa_for_deployment"
14
+
15
+ module Kube
16
+ module Cluster
17
+ # A Manifest subclass that runs resources through a middleware stack
18
+ # on enumeration. Manifests represent files — resources pass through
19
+ # middleware before rendering or saving.
20
+ #
21
+ # class MyApp < Kube::Cluster::Manifest
22
+ # stack do
23
+ # use Middleware::Namespace, "production"
24
+ # use Middleware::Labels, app: "web-app"
25
+ # use Middleware::ResourcePreset
26
+ # end
27
+ # end
28
+ #
29
+ # app = MyApp.new
30
+ # app << Kube::Schema["Deployment"].new { ... }
31
+ # app.to_yaml # resources have been transformed by the stack
32
+ #
33
+ class Manifest < Kube::Schema::Manifest
34
+ # Declare a middleware stack at the class level.
35
+ #
36
+ # stack do
37
+ # use Middleware::ResourcePreset
38
+ # use Middleware::SecurityContext
39
+ # end
40
+ #
41
+ def self.stack(&block)
42
+ @stack = Stack.new(&block)
43
+ end
44
+
45
+ # Enumerate resources after passing them through the middleware
46
+ # stack. The entire manifest is passed to the stack so that
47
+ # generative middleware can introduce new resources that
48
+ # subsequent stages will see and process.
49
+ #
50
+ # Every method that reads the manifest (to_yaml, to_a, map,
51
+ # select, etc.) goes through here.
52
+ def each(&block)
53
+ return enum_for(:each) unless block
54
+
55
+ stack = self.class.instance_variable_get(:@stack)
56
+ if stack
57
+ stack.call(@resources).each(&block)
58
+ else
59
+ @resources.each(&block)
60
+ end
61
+ end
62
+
63
+ # Override to_yaml so it renders through the middleware stack.
64
+ # The parent class accesses @resources directly, bypassing each.
65
+ def to_yaml
66
+ map { |r| r.to_yaml }.join("")
67
+ end
68
+
69
+ # Override to_a so it returns middleware-processed resources.
70
+ # The parent class returns @resources.dup directly.
71
+ def to_a
72
+ map(&:itself)
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kube
4
+ module Cluster
5
+ class Resource < Kube::Schema::Resource
6
+ module DirtyTracking
7
+ def changed?
8
+ to_h != @clean
9
+ end
10
+
11
+ def changed
12
+ diff_keys(to_h, @clean)
13
+ end
14
+
15
+ def changes
16
+ build_changes(to_h, @clean)
17
+ end
18
+
19
+ def changes_applied
20
+ snapshot!
21
+ end
22
+
23
+ # Data suitable for a strategic-merge patch: only the
24
+ # keys/sub-trees that differ from the clean snapshot.
25
+ def patch_data
26
+ deep_diff(to_h, @clean)
27
+ end
28
+
29
+ def respond_to_missing?(name, include_private = false)
30
+ if name.end_with?("_changed?")
31
+ true
32
+ else
33
+ super
34
+ end
35
+ end
36
+
37
+ def method_missing(name, *args, &block)
38
+ if name.end_with?("_changed?")
39
+ attr = name.to_s.delete_suffix("_changed?").to_sym
40
+ old_val = @clean[attr]
41
+ new_val = to_h[attr]
42
+ old_val != new_val
43
+ else
44
+ super
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def snapshot!
51
+ @clean = deep_dup(to_h)
52
+ end
53
+
54
+ def deep_diff(current, original)
55
+ Hash.new.tap do |result|
56
+ merged_keys = current.keys | original.keys
57
+
58
+ merged_keys.each do |key|
59
+ cur_val = current[key]
60
+ orig_val = original[key]
61
+
62
+ if cur_val.is_a?(Hash) && orig_val.is_a?(Hash)
63
+ nested = deep_diff(cur_val, orig_val)
64
+
65
+ if nested.empty?
66
+ next
67
+ else
68
+ result[key] = nested
69
+ end
70
+ elsif cur_val != orig_val
71
+ result[key] = [orig_val, cur_val]
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ def diff_keys(current, original)
78
+ Set.new.tap do |keys|
79
+ merged_keys = (current.keys | original.keys)
80
+
81
+ merged_keys.each do |key|
82
+ if current[key] != original[key]
83
+ keys << key
84
+ end
85
+ end
86
+ end.to_a
87
+ end
88
+
89
+ def build_changes(current, original)
90
+ Hash.new.tap do |hash|
91
+ merged_keys = current.keys | original.keys
92
+
93
+ merged_keys.each do |key|
94
+ if current[key] == original[key]
95
+ next
96
+ else
97
+ hash[key] = [original[key], current[key]]
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ def deep_dup(obj)
104
+ case obj
105
+ when Hash then obj.each_with_object({}) { |(k, v), h| h[k] = deep_dup(v) }
106
+ when Array then obj.map { |v| deep_dup(v) }
107
+ else obj
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "open3"
5
+
6
+ module Kube
7
+ module Cluster
8
+ class Resource < Kube::Schema::Resource
9
+ module Persistence
10
+ def apply
11
+ JSON.generate(deep_stringify_keys(to_h)).then do |json|
12
+ kubectl("apply", "-f", "-", stdin: json)
13
+ reload
14
+ true
15
+ end
16
+ end
17
+
18
+ def patch(type: "strategic")
19
+ if persisted?
20
+ diff = patch_data
21
+
22
+ if diff.empty?
23
+ false
24
+ else
25
+ json = JSON.generate(deep_stringify_keys(diff))
26
+ kubectl("patch", resource_type, name, *ns_flags, "--type", type, "-p", json)
27
+ reload
28
+ true
29
+ end
30
+ else
31
+ raise Kube::CommandError, "cannot patch a resource without a name"
32
+ end
33
+ end
34
+
35
+ def delete
36
+ if persisted?
37
+ kubectl("delete", resource_type, name, *ns_flags)
38
+ true
39
+ else
40
+ raise Kube::CommandError, "cannot delete a resource without a name"
41
+ end
42
+ end
43
+
44
+ def reload
45
+ if persisted?
46
+ tap do
47
+ kubectl("get", resource_type, name, *ns_flags, "-o", "json").then do |json|
48
+ JSON.parse(json).then do |hash|
49
+ @data = BlackHoleStruct.new(hash)
50
+ snapshot!
51
+ end
52
+ end
53
+ end
54
+ else
55
+ raise Kube::CommandError, "cannot reload a resource without a name"
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def kubectl(*args)
62
+ @cluster.connection.ctl.run(args.join(" "))
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "resource/dirty_tracking"
4
+ require_relative "resource/persistence"
5
+
6
+ module Kube
7
+ module Cluster
8
+ class Resource < Kube::Schema::Resource
9
+ include DirtyTracking
10
+ include Persistence
11
+
12
+ attr_accessor :cluster
13
+
14
+ def initialize(hash = {}, &block)
15
+ @cluster = hash.delete(:cluster)
16
+ super
17
+ snapshot!
18
+ end
19
+ end
20
+ end
21
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Kube
4
4
  module Cluster
5
- VERSION = "0.2.0"
5
+ VERSION = "0.2.1"
6
6
  end
7
7
  end
data/lib/kube/cluster.rb CHANGED
@@ -1,16 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "kube/schema"
4
+ require_relative "../kube/errors"
3
5
  require_relative "cluster/version"
4
- require_relative "cluster/tree_node"
5
- require_relative "cluster/resource_selector"
6
- require_relative "cluster/query_builder"
7
- require_relative "cluster/command_node"
8
- require_relative "cluster/ctl"
6
+ require_relative "cluster/connection"
7
+ require_relative "cluster/instance"
8
+ require_relative "cluster/resource"
9
+ require_relative "cluster/manifest"
10
+ require 'kube/ctl'
9
11
 
10
12
  module Kube
13
+ def self.cluster
14
+ Cluster
15
+ end
16
+
11
17
  module Cluster
12
- def self.ctl
13
- Ctl.new
18
+ def self.connect(kubeconfig:)
19
+ Instance.new(kubeconfig: kubeconfig)
14
20
  end
15
21
  end
16
22
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file defines cluster-specific errors in the Kube namespace.
4
+
5
+ module Kube
6
+ # Base error class for the Kube namespace.
7
+ class Error < StandardError; end
8
+ # Raised when a kubectl command fails.
9
+ #
10
+ # begin
11
+ # resource.patch
12
+ # rescue Kube::CommandError => e
13
+ # e.subcommand # => "patch"
14
+ # e.stderr # => "Error from server (NotFound): ..."
15
+ # e.exit_code # => 1
16
+ # e.reason # => "NotFound" (parsed from stderr, or nil)
17
+ # end
18
+ #
19
+ class CommandError < Error
20
+ attr_reader :subcommand, :stderr, :exit_code, :reason
21
+
22
+ def initialize(message = nil, subcommand: nil, stderr: nil, exit_code: nil, reason: nil)
23
+ @subcommand = subcommand
24
+ @stderr = stderr
25
+ @exit_code = exit_code
26
+ @reason = reason || parse_reason(stderr)
27
+ super(message || build_message)
28
+ end
29
+
30
+ def self.from_kubectl(subcommand:, stderr:, exit_code:)
31
+ new(
32
+ subcommand: subcommand,
33
+ stderr: stderr,
34
+ exit_code: exit_code
35
+ )
36
+ end
37
+
38
+ private
39
+
40
+ def build_message
41
+ "kubectl #{@subcommand} failed (exit #{@exit_code}): #{@stderr}"
42
+ end
43
+
44
+ # Attempts to extract the reason from kubectl stderr.
45
+ # kubectl errors typically look like:
46
+ # Error from server (NotFound): deployments.apps "foo" not found
47
+ # Error from server (Forbidden): ...
48
+ # error: the server doesn't have a resource type "foo"
49
+ def parse_reason(stderr)
50
+ return nil if stderr.nil? || stderr.empty?
51
+
52
+ if stderr =~ /\((\w+)\)/
53
+ $1
54
+ end
55
+ end
56
+ end
57
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kube_cluster
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan K
@@ -57,56 +57,102 @@ dependencies:
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: '1.0'
60
+ version: 1.2.0
61
61
  type: :runtime
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: '1.0'
67
+ version: 1.2.0
68
+ - !ruby/object:Gem::Dependency
69
+ name: kube_kit
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">"
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">"
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: kube_kubectl
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: 2.0.0
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: 2.0.0
68
96
  description: 'OOP abstraction that allows you to deploy and manage kubernetes resources
69
97
  using Ruby.
70
98
 
71
99
  '
72
100
  email:
73
101
  - nathankidd@hey.com
74
- executables:
75
- - kube_cluster
102
+ executables: []
76
103
  extensions: []
77
104
  extra_rdoc_files: []
78
105
  files:
79
106
  - ".envrc"
107
+ - ".github/workflows/release.yml"
108
+ - ".github/workflows/tag-gem-version-bump.yml"
80
109
  - ".gitignore"
81
110
  - ".rubocop.yml"
82
111
  - Gemfile
83
112
  - Gemfile.lock
84
113
  - LICENSE
85
114
  - README.md
86
- - Rakefile
87
- - TREE_PLAN.md
88
115
  - bin/console
89
- - bin/generate-command-schema-v1
116
+ - bin/dev
90
117
  - bin/increment-version
91
118
  - bin/release-gem
92
119
  - bin/setup
93
120
  - bin/tag-version
94
121
  - bin/test
95
- - data/kubectl-command-tree-v1-minimal.json
96
- - data/kubectl-command-tree-v1.json
97
- - examples/quick-repl/docker-compose.yml
98
- - exe/kube_cluster
122
+ - docker-compose.yml
123
+ - examples/01-basic-redis-pod/manifest.rb
124
+ - examples/database/manifest.rb
125
+ - examples/version2/demo.rb
126
+ - examples/version2/helpers.rb
127
+ - examples/version2/my_app.rb
128
+ - examples/version2/postgresql.rb
129
+ - examples/version2/ruby_on_rails.rb
130
+ - examples/web-app/manifest.rb
99
131
  - flake.lock
100
132
  - flake.nix
101
133
  - kube_cluster.gemspec
134
+ - lib/kube/cli/cluster.rb
102
135
  - lib/kube/cluster.rb
103
- - lib/kube/cluster/command_node.rb
104
- - lib/kube/cluster/ctl.rb
105
- - lib/kube/cluster/query_builder.rb
106
- - lib/kube/cluster/resource_selector.rb
107
- - lib/kube/cluster/tree_node.rb
136
+ - lib/kube/cluster/connection.rb
137
+ - lib/kube/cluster/instance.rb
138
+ - lib/kube/cluster/manifest.rb
139
+ - lib/kube/cluster/manifest/middleware.rb
140
+ - lib/kube/cluster/manifest/middleware/annotations.rb
141
+ - lib/kube/cluster/manifest/middleware/hpa_for_deployment.rb
142
+ - lib/kube/cluster/manifest/middleware/ingress_for_service.rb
143
+ - lib/kube/cluster/manifest/middleware/labels.rb
144
+ - lib/kube/cluster/manifest/middleware/namespace.rb
145
+ - lib/kube/cluster/manifest/middleware/pod_anti_affinity.rb
146
+ - lib/kube/cluster/manifest/middleware/resource_preset.rb
147
+ - lib/kube/cluster/manifest/middleware/security_context.rb
148
+ - lib/kube/cluster/manifest/middleware/service_for_deployment.rb
149
+ - lib/kube/cluster/manifest/stack.rb
150
+ - lib/kube/cluster/resource.rb
151
+ - lib/kube/cluster/resource/dirty_tracking.rb
152
+ - lib/kube/cluster/resource/persistence.rb
108
153
  - lib/kube/cluster/version.rb
109
154
  - lib/kube/cluster/version.rb.erb
155
+ - lib/kube/errors.rb
110
156
  homepage: https://github.com/general-intelligence-systems/kube_cluster
111
157
  licenses:
112
158
  - Apache-2.0
data/Rakefile DELETED
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rake/testtask"
4
-
5
- Rake::TestTask.new(:test) do |t|
6
- t.libs << "test"
7
- t.libs << "lib"
8
- t.test_files = FileList["test/**/*_test.rb"]
9
- end
10
-
11
- task default: :test