matsuri 0.1.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 (157) hide show
  1. checksums.yaml +5 -5
  2. data/.envrc +1 -0
  3. data/.gitignore +2 -0
  4. data/.rubocop.yml +9 -0
  5. data/AGENTS.md +18 -0
  6. data/CHANGELOG.md +642 -0
  7. data/Gemfile +2 -0
  8. data/README.md +86 -8
  9. data/Rakefile +20 -0
  10. data/bin/console +1 -0
  11. data/bin/matsuri +6 -0
  12. data/bin/setup +2 -0
  13. data/docs/acceptors.md +181 -0
  14. data/docs/boot.md +101 -0
  15. data/docs/design/acceptors.md +157 -0
  16. data/docs/design/boot.md +131 -0
  17. data/docs/design/ssa.md +142 -0
  18. data/docs/design/stacks.md +153 -0
  19. data/docs/diff.md +85 -0
  20. data/docs/known-issues.md +19 -0
  21. data/docs/local-dev.md +173 -0
  22. data/docs/presenters.md +317 -0
  23. data/docs/resources.md +121 -0
  24. data/docs/setup.md +97 -0
  25. data/docs/ssa.md +149 -0
  26. data/docs/stacks.md +146 -0
  27. data/flake.lock +61 -0
  28. data/flake.nix +35 -0
  29. data/lib/matsuri/acceptor.rb +33 -0
  30. data/lib/matsuri/acceptors/image_tag.rb +15 -0
  31. data/lib/matsuri/cmd.rb +46 -0
  32. data/lib/matsuri/cmds/apply.rb +78 -0
  33. data/lib/matsuri/cmds/cli.rb +147 -0
  34. data/lib/matsuri/cmds/create.rb +87 -0
  35. data/lib/matsuri/cmds/delete.rb +86 -0
  36. data/lib/matsuri/cmds/diff.rb +105 -0
  37. data/lib/matsuri/cmds/generate.rb +24 -0
  38. data/lib/matsuri/cmds/kubectl.rb +16 -0
  39. data/lib/matsuri/cmds/recreate.rb +65 -0
  40. data/lib/matsuri/cmds/scale.rb +37 -0
  41. data/lib/matsuri/cmds/show.rb +123 -0
  42. data/lib/matsuri/cmds/status.rb +67 -0
  43. data/lib/matsuri/concerns/awaiting.rb +24 -0
  44. data/lib/matsuri/concerns/diffable.rb +135 -0
  45. data/lib/matsuri/concerns/export_manifest.rb +56 -0
  46. data/lib/matsuri/concerns/import_manifest.rb +64 -0
  47. data/lib/matsuri/concerns/pod_template.rb +37 -0
  48. data/lib/matsuri/concerns/pods/spread_pods.rb +47 -0
  49. data/lib/matsuri/concerns/pods/spread_topology.rb +63 -0
  50. data/lib/matsuri/concerns/registry_helpers.rb +59 -0
  51. data/lib/matsuri/config.rb +101 -0
  52. data/lib/matsuri/core_ext.rb +107 -0
  53. data/lib/matsuri/dsl/cluster/aggregated_cluster_role.rb +75 -0
  54. data/lib/matsuri/dsl/cluster/binding.rb +118 -0
  55. data/lib/matsuri/dsl/cluster/cluster_role.rb +41 -0
  56. data/lib/matsuri/dsl/cluster/role.rb +49 -0
  57. data/lib/matsuri/dsl/cluster/scope.rb +245 -0
  58. data/lib/matsuri/dsl/cluster/service_account.rb +76 -0
  59. data/lib/matsuri/dsl/concerns/default_binding.rb +34 -0
  60. data/lib/matsuri/dsl/concerns/manifest_set.rb +21 -0
  61. data/lib/matsuri/dsl/concerns/metadata.rb +56 -0
  62. data/lib/matsuri/dsl/concerns/rbac_rules.rb +39 -0
  63. data/lib/matsuri/initializer.rb +41 -0
  64. data/lib/matsuri/kubernetes/base.rb +107 -0
  65. data/lib/matsuri/kubernetes/config_map.rb +100 -0
  66. data/lib/matsuri/kubernetes/cron_job.rb +115 -0
  67. data/lib/matsuri/kubernetes/daemon_set.rb +62 -0
  68. data/lib/matsuri/kubernetes/deployment.rb +65 -0
  69. data/lib/matsuri/kubernetes/endpoints.rb +57 -0
  70. data/lib/matsuri/kubernetes/horizontal_pod_autoscaler.rb +116 -0
  71. data/lib/matsuri/kubernetes/imported_manifest.rb +61 -0
  72. data/lib/matsuri/kubernetes/ingress.rb +66 -0
  73. data/lib/matsuri/kubernetes/persistent_volume.rb +56 -0
  74. data/lib/matsuri/kubernetes/persistent_volume_claim.rb +41 -0
  75. data/lib/matsuri/kubernetes/pod.rb +355 -0
  76. data/lib/matsuri/kubernetes/replica_set.rb +46 -0
  77. data/lib/matsuri/kubernetes/secret.rb +86 -0
  78. data/lib/matsuri/kubernetes/service.rb +49 -0
  79. data/lib/matsuri/kubernetes/stateful_set.rb +75 -0
  80. data/lib/matsuri/kubernetes/storage_class.rb +45 -0
  81. data/lib/matsuri/platform.rb +38 -0
  82. data/lib/matsuri/presenter.rb +47 -0
  83. data/lib/matsuri/presenters/helpers.rb +39 -0
  84. data/lib/matsuri/presenters/prune_empty.rb +59 -0
  85. data/lib/matsuri/presenters/seed_image.rb +28 -0
  86. data/lib/matsuri/presenters/seed_replicas.rb +25 -0
  87. data/lib/matsuri/presenters/seed_resources.rb +29 -0
  88. data/lib/matsuri/registry.rb +267 -0
  89. data/lib/matsuri/shell_out.rb +119 -0
  90. data/lib/matsuri/stack.rb +276 -0
  91. data/lib/matsuri/task.rb +20 -0
  92. data/lib/matsuri/tasks/cluster.rb +79 -0
  93. data/lib/matsuri/tasks/docker.rb +25 -0
  94. data/lib/matsuri/tasks/hash_diff.rb +61 -0
  95. data/lib/matsuri/tasks/kubectl.rb +21 -0
  96. data/lib/matsuri/tasks/kubectl_ops/apply.rb +24 -0
  97. data/lib/matsuri/tasks/kubectl_ops/create.rb +27 -0
  98. data/lib/matsuri/tasks/kubectl_ops/delete.rb +21 -0
  99. data/lib/matsuri/tasks/kubectl_ops/diff.rb +47 -0
  100. data/lib/matsuri/tasks/kubectl_ops/get.rb +36 -0
  101. data/lib/matsuri/tasks/kubectl_ops/recreate.rb +35 -0
  102. data/lib/matsuri/tasks/kubectl_ops/replace.rb +29 -0
  103. data/lib/matsuri/tasks/kubectl_ops/rollout_status.rb +22 -0
  104. data/lib/matsuri/tasks/kubectl_ops/scale.rb +22 -0
  105. data/lib/matsuri/tasks/kubectl_ops/server_side_apply.rb +32 -0
  106. data/lib/matsuri/tasks/kubectl_ops/status.rb +20 -0
  107. data/lib/matsuri/tasks/kubectl_ops.rb +218 -0
  108. data/lib/matsuri/tasks/pod.rb +56 -0
  109. data/lib/matsuri/tasks/stacks/seed.rb +93 -0
  110. data/lib/matsuri/tasks/stacks.rb +31 -0
  111. data/lib/matsuri/transform/algebra.rb +52 -0
  112. data/lib/matsuri/transform/callable.rb +54 -0
  113. data/lib/matsuri/transform/combinators.rb +61 -0
  114. data/lib/matsuri/transform/env.rb +43 -0
  115. data/lib/matsuri/transform/kubernetes.rb +89 -0
  116. data/lib/matsuri/transform.rb +20 -0
  117. data/lib/matsuri/version.rb +3 -1
  118. data/lib/matsuri.rb +158 -2
  119. data/matsuri.gemspec +20 -7
  120. data/tests/bin/minikube-start.sh +27 -0
  121. data/tests/bin/probe-save-config.sh +46 -0
  122. data/tests/bin/probe-ssa.sh +206 -0
  123. data/tests/example/.gitignore +1 -0
  124. data/tests/example/Gemfile +7 -0
  125. data/tests/example/README.md +91 -0
  126. data/tests/example/bin/matsuri +14 -0
  127. data/tests/example/config/initializer.rb +12 -0
  128. data/tests/example/config/matsuri.rb +9 -0
  129. data/tests/example/config/platform.rb +39 -0
  130. data/tests/example/lib/acme/nodes/dedicated.rb +26 -0
  131. data/tests/example/lib/acme/nodes/preemptible.rb +17 -0
  132. data/tests/example/lib/acme/nodes/required_cores.rb +26 -0
  133. data/tests/example/lib/acme/nodes/resource_class.rb +26 -0
  134. data/tests/example/lib/acme/pods/cacerts.rb +15 -0
  135. data/tests/example/lib/acme/pods/healthcheck.rb +36 -0
  136. data/tests/example/lib/acme.rb +26 -0
  137. data/tests/example/platform/config_maps/billing-settings.rb +8 -0
  138. data/tests/example/platform/config_maps/cacerts.rb +9 -0
  139. data/tests/example/platform/deployments/billing-web.rb +5 -0
  140. data/tests/example/platform/deployments/checkout-web.rb +6 -0
  141. data/tests/example/platform/namespaces/billing-dev.yaml +7 -0
  142. data/tests/example/platform/namespaces/checkout-dev.yaml +7 -0
  143. data/tests/example/platform/pods/billing.rb +31 -0
  144. data/tests/example/platform/pods/checkout.rb +27 -0
  145. data/tests/example/platform/services/billing-web.rb +6 -0
  146. data/tests/example/platform/services/checkout-web.rb +4 -0
  147. data/tests/example/platform/stacks/billing.rb +8 -0
  148. data/tests/example/platform/stacks/checkout.rb +19 -0
  149. data/tests/k8s/save-config/deployment-declared.yaml +19 -0
  150. data/tests/k8s/save-config/deployment-seeded.yaml +20 -0
  151. data/tests/k8s/save-config/namespace.yaml +4 -0
  152. data/tests/k8s/ssa/declared-bumped.yaml +22 -0
  153. data/tests/k8s/ssa/declared.yaml +22 -0
  154. data/tests/k8s/ssa/namespace.yaml +4 -0
  155. data/tests/k8s/ssa/seed-via-create.yaml +24 -0
  156. metadata +288 -20
  157. data/.travis.yml +0 -4
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Vendored replacements for the ActiveSupport core_ext methods matsuri
4
+ # depends on. These are monkeypatches rather than refinements because
5
+ # definition files in downstream matsuri repos evaluate inside this
6
+ # process and call these methods ambiently; refinements would not reach
7
+ # them.
8
+
9
+ module Matsuri
10
+ module CoreExt
11
+ module_function
12
+
13
+ def deep_transform_keys(object, &block)
14
+ case object
15
+ when Hash then object.each_with_object({}) { |(k, v), h| h[yield(k)] = deep_transform_keys(v, &block) }
16
+ when Array then object.map { |e| deep_transform_keys(e, &block) }
17
+ else object
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ class Hash
24
+ def symbolize_keys
25
+ transform_keys { |key| key.to_sym rescue key }
26
+ end
27
+
28
+ def stringify_keys
29
+ transform_keys(&:to_s)
30
+ end
31
+
32
+ def deep_symbolize_keys
33
+ deep_transform_keys { |key| key.to_sym rescue key }
34
+ end
35
+
36
+ def deep_stringify_keys
37
+ deep_transform_keys(&:to_s)
38
+ end
39
+
40
+ def deep_transform_keys(&block)
41
+ Matsuri::CoreExt.deep_transform_keys(self, &block)
42
+ end
43
+ end
44
+
45
+ class Object
46
+ def try(method_name = nil, *args, &block)
47
+ if method_name.nil? && block
48
+ block.arity.zero? ? instance_eval(&block) : yield(self)
49
+ elsif respond_to?(method_name)
50
+ public_send(method_name, *args, &block)
51
+ end
52
+ end
53
+
54
+ def blank?
55
+ respond_to?(:empty?) ? !!empty? : !self
56
+ end
57
+
58
+ def present?
59
+ !blank?
60
+ end
61
+
62
+ def presence
63
+ self if present?
64
+ end
65
+ end
66
+
67
+ class NilClass
68
+ def try(*)
69
+ nil
70
+ end
71
+ end
72
+
73
+ class String
74
+ BLANK_RE = /\A[[:space:]]*\z/
75
+
76
+ def blank?
77
+ empty? || BLANK_RE.match?(self)
78
+ end
79
+
80
+ def camelize
81
+ sub(/^[a-z\d]*/) { |match| match.capitalize }
82
+ .gsub(%r{(?:_|(/))([a-z\d]*)}i) { "#{Regexp.last_match(1)}#{Regexp.last_match(2).capitalize}" }
83
+ .gsub('/', '::')
84
+ end
85
+ end
86
+
87
+ class Class
88
+ def class_attribute(*names)
89
+ names.each do |name|
90
+ ivar = :"@#{name}"
91
+
92
+ define_singleton_method(name) do
93
+ if instance_variable_defined?(ivar)
94
+ instance_variable_get(ivar)
95
+ elsif superclass.respond_to?(name)
96
+ superclass.public_send(name)
97
+ end
98
+ end
99
+
100
+ define_singleton_method("#{name}=") do |value|
101
+ instance_variable_set(ivar, value)
102
+ end
103
+
104
+ define_method(name) { self.class.public_send(name) }
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matsuri
4
+ module DSL
5
+ module Cluster
6
+ # Cluster resources here in Matsuri are a bit different as the are meant to
7
+ # be driven by the cluster dsl
8
+ class AggregatedClusterRole < Matsuri::DSL::Cluster::Role
9
+ attr_accessor :aggregation_selectors
10
+
11
+ let(:kind) { 'ClusterRole' }
12
+
13
+ let(:manifest) do
14
+ {
15
+ apiVersion: api_version,
16
+ kind: kind,
17
+ metadata: final_metadata,
18
+ aggregationRule: { clusterRoleSelectors: aggregation_selectors }
19
+ }
20
+ end
21
+
22
+ # Cluster Roles are not namespaced. Override from MetadataDsl concern
23
+ let(:default_metadata) { { name: name, labels: final_labels, annotations: final_annotations } }
24
+
25
+ def initialize(options = {}, &block)
26
+ self.aggregation_selectors = []
27
+ super
28
+ end
29
+
30
+ ### DSL Helper
31
+
32
+ # Match helper for creating selectors for aggregated cluster roles
33
+ # Examples:
34
+ # match 'example.com/aggregate-to-monitor' => true
35
+ # match 'example.com/aggregate-to-monitor',
36
+ def match(*args)
37
+ case
38
+ when args[0].is_a?(Hash) then match_labels(args[0])
39
+ when args.length.between?(2, 3) then match_expression(*args)
40
+ else
41
+ fail ArgumentError, <<-EOT
42
+ match helper requires one of the following formats
43
+
44
+ match key: value, key: value
45
+ match key, operator
46
+ match key, operator, value
47
+
48
+ Arguments received: #{args.inspect}
49
+ EOT
50
+ end
51
+ end
52
+
53
+ def match_labels(selectors = {})
54
+ aggregation_selectors << selectors
55
+ end
56
+
57
+ def match_expression(key, operator, value = nil)
58
+ aggregation_selectors << { key: key, operator: map_match_operator(operator), value: value }.compact
59
+ end
60
+
61
+ def map_match_operator(operator)
62
+ case operator
63
+ when 'In', 'in', :in then 'In'
64
+ when 'NotIn', 'not_in', :not_in then 'NotIn'
65
+ when 'Exists', 'exists', :exists then 'Exists'
66
+ when 'DoesNotExist', 'does_not_exist', :does_not_exist then 'DoesNotExist'
67
+ else
68
+ fail ArgumentError, "Invalid match expression operator: #{operator}"
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rlet'
4
+
5
+ module Matsuri
6
+ module DSL
7
+ module Cluster
8
+ # This DSL covers both RoleBinding and ClusterRoleBinding
9
+ # Defaults:
10
+ # - binding_type determines default for role_ref.kind unless overridden
11
+ # - role ref name defaults to the name of this binding
12
+ # - role ref kind defaults to equivalent kind for this binding
13
+ # - role ref api_group defaults to rbac.authorization.kubernetes.io
14
+ # - subject namespace defaults to namespace for this binding
15
+ # - subject api_group defaults to rbac.authorization.kubernetes.io for User and Group
16
+ # - subject api_group defaults to "" (core) for other kinds (ServiceAccounts)
17
+ class Binding
18
+ include Let
19
+ include Matsuri::Concerns::ExportManifest
20
+ include Matsuri::DSL::Concerns::Metadata
21
+ include Matsuri::DSL::Concerns::ManifestSet
22
+
23
+ attr_accessor :role_ref, :subjects
24
+
25
+ ### Parameters
26
+ let(:binding_type) { options[:type] }
27
+
28
+ ### Manifest Set
29
+ let(:rbac_manifests) { subjects.any? ? manifest : nil } # Return manifest only if there are subjects defined for binding
30
+
31
+ ### Manifest
32
+ let(:api_version) { Matsuri::Config.rbac_api_version }
33
+ let(:final_role_ref) { role_ref || default_role_ref }
34
+ let(:manifest) do
35
+ {
36
+ apiVersion: api_version,
37
+ kind: kind,
38
+ metadata: final_metadata,
39
+ roleRef: final_role_ref,
40
+ subjects: subjects
41
+ }
42
+ end
43
+
44
+ let(:kind) do
45
+ case binding_type
46
+ when :role, 'Role', 'role' then 'RoleBinding'
47
+ when :cluster_role, 'ClusterRole', 'cluster_role' then 'ClusterRoleBinding'
48
+ else
49
+ fail ArgumentError, "binding_type must be one of :role or :cluster_role, is: #{binding_type.inspect}"
50
+ end
51
+ end
52
+
53
+ let(:default_role_ref) { { kind: default_role_ref_kind, name: default_role_ref_name, apiGroup: default_role_ref_api_group } }
54
+ let(:default_role_ref_name) { name }
55
+ let(:default_role_ref_api_group) { 'rbac.authorization.k8s.io' }
56
+ let(:default_role_ref_kind) { map_binding_type_to_kind(binding_type) }
57
+
58
+ def initialize(options = {}, &block)
59
+ initialize_metadata_dsl(options)
60
+ self.subjects = []
61
+
62
+ configure(&block) if block
63
+ end
64
+
65
+ def configure(&block)
66
+ instance_eval(&block)
67
+ end
68
+
69
+ ### DSL Helpers
70
+ def reference(type, name = default_role_ref_name, api_group: default_role_ref_api_group)
71
+ self.role_ref = {
72
+ kind: map_binding_type_to_kind(type),
73
+ name: name,
74
+ apiGroup: api_group
75
+ }
76
+ end
77
+
78
+ def subject(name, kind:, namespace: nil, api_group: nil)
79
+ self.subjects << {
80
+ name: name,
81
+ kind: map_subject_type_to_kind(kind),
82
+ namespace: namespace,
83
+ apiGroup: api_group || default_subject_api_group(kind)
84
+ }.compact
85
+ end
86
+
87
+ ### Helpers
88
+ def map_binding_type_to_kind(type)
89
+ case type
90
+ when :role, 'Role', 'role' then 'Role'
91
+ when :cluster_role, 'ClusterRole', 'cluster_role' then 'ClusterRole'
92
+ else
93
+ fail ArgumentError, "binding type must be one of :role or :cluster_role, is: #{type.inspect}"
94
+ end
95
+ end
96
+
97
+ def map_subject_type_to_kind(type)
98
+ case type
99
+ when :user then 'User'
100
+ when :group then 'Group'
101
+ when :service_account then 'ServiceAccount'
102
+ else
103
+ fail ArgumentError, "subject type must be one of :user, :group, :service_account, is: #{type.inspect}"
104
+ end
105
+ end
106
+
107
+ def default_subject_api_group(type)
108
+ case type
109
+ when :user, :group then 'rbac.authorization.k8s.io'
110
+ when :service_account then ''
111
+ else
112
+ fail ArgumentError, "subject type must be one of :user, :group, :service_account, is: #{type.inspect}"
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matsuri
4
+ module DSL
5
+ module Cluster
6
+ # Cluster resources here in Matsuri are a bit different as the are meant to
7
+ # be driven by the cluster dsl
8
+ class ClusterRole < Matsuri::DSL::Cluster::Role
9
+ let(:kind) { 'ClusterRole' }
10
+
11
+ let(:manifest) do
12
+ {
13
+ apiVersion: api_version,
14
+ kind: kind,
15
+ metadata: final_metadata,
16
+ rules: rules
17
+ }
18
+ end
19
+
20
+ # Cluster Roles are not namespaced. Override from MetadataDsl concern
21
+ let(:default_metadata) { { name: name, labels: final_labels, annotations: final_annotations } }
22
+
23
+ ### DSL Helper
24
+
25
+ # Use these to add urls to the rules
26
+ # nonResourceURLs apply only to ClusterRoles
27
+ def urls(urls, verbs: nil, api_groups: nil)
28
+ rule api_groups: api_groups, urls: urls, verbs: verbs
29
+ end
30
+
31
+ # Helper to aggregate cluster role to admin, edit, view, and other similar tags
32
+ def aggregate_to(*tags)
33
+ tags.flatten.each do |tag|
34
+ label "rbac.authorization.k8s.io/aggregate-to-#{tag}", 'true'
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rlet'
4
+
5
+ module Matsuri
6
+ module DSL
7
+ module Cluster
8
+ # Cluster resources here in Matsuri are a bit different as the are meant to
9
+ # be driven by the cluster dsl
10
+ class Role
11
+ include Let
12
+ include Matsuri::Concerns::ExportManifest
13
+ include Matsuri::DSL::Concerns::Metadata
14
+ include Matsuri::DSL::Concerns::RbacRules
15
+ include Matsuri::DSL::Concerns::DefaultBinding
16
+ include Matsuri::DSL::Concerns::ManifestSet
17
+
18
+ let(:api_version) { Matsuri::Config.rbac_api_version }
19
+ let(:kind) { 'Role' }
20
+
21
+ ### Manifest Set
22
+ let(:rbac_manifests) { [manifest, default_binding.rbac_manifests].compact }
23
+
24
+ ### Manifest
25
+
26
+ let(:manifest) do
27
+ {
28
+ apiVersion: api_version,
29
+ kind: kind,
30
+ metadata: final_metadata,
31
+ rules: rules
32
+ }
33
+ end
34
+
35
+ def initialize(options = {}, &block)
36
+ initialize_metadata_dsl(options)
37
+ initialize_rbac_rules_dsl
38
+
39
+ configure(&block) if block
40
+ end
41
+
42
+ def configure(&block)
43
+ instance_eval(&block)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+
@@ -0,0 +1,245 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rlet'
4
+ require 'matsuri/core_ext'
5
+
6
+ module Matsuri
7
+ module DSL
8
+ module Cluster
9
+ # Non K8S object. This class encapsulates the idea of a scope for the DSL. Currently,
10
+ # the only attribute accepted by the scope is namespace. This is also the
11
+ # entry point for the DSL.
12
+ class Scope
13
+ include Let
14
+ include Matsuri::DSL::Concerns::ManifestSet
15
+
16
+ # @TODO see if kubectl reconcile will also handle the general resources
17
+ # If not, we will have to reconcile rbac resources and apply the rest
18
+ attr_accessor :definitions, :options
19
+
20
+ let(:current_namespace) { options[:namespace] }
21
+ let(:source_file) { parent_scope[:source_file] }
22
+ let(:skip?) { options[:skip] }
23
+
24
+ let(:parent_scope) { options[:scoped_options] || {} }
25
+
26
+ let(:scoped_options) do
27
+ {
28
+ namespace: current_namespace,
29
+ source_file: source_file,
30
+ skip: skip?
31
+ }
32
+ end
33
+
34
+ def initialize(options = {}, &block)
35
+ self.definitions = []
36
+ self.options = options
37
+ instance_eval(&block) if block
38
+ end
39
+
40
+ # Load and evaluate from a filename.
41
+ # Set the filename and line number so # that errors are correctly reported
42
+ # Inject the relative source path so manifests can be tracked back to a source file
43
+ def self.from_filename(filename, scoped_options = {})
44
+ relative_source = filename.sub(/^#{File.join(Matsuri::Config.base_path, '/')}/, '')
45
+ child_scope = scoped_options.merge(source_file: relative_source)
46
+ # Always return the newly created scope regardless of return value from evaluating
47
+ # the imported source file
48
+ new(scoped_options: child_scope).tap { |s| s.instance_eval(File.read(filename), filename, 1) }
49
+ end
50
+
51
+ # This could be used by the DSL, but we really should not
52
+ def import(filename)
53
+ definitions << self.class.from_filename(filename, scoped_options)
54
+ end
55
+
56
+ # Load and evaluate from a class.
57
+ # Set the filename and line number so # that errors are correctly reported
58
+ # Inject the relative source path so manifests can be tracked back to a source file
59
+ def self.from_class(klass, scoped_options = {})
60
+ filename, _ = klass.method(:scope_manifest).source_location
61
+ relative_source = filename.sub(/^#{File.join(Matsuri::Config.base_path, '/')}/, '')
62
+ child_scope = scoped_options.merge(source_file: relative_source)
63
+
64
+ # Create a new scope and pass it to the class
65
+ new_scope = new(scoped_options: child_scope)
66
+ klass.scope_manifest(new_scope)
67
+
68
+ new_scope
69
+ end
70
+
71
+ # This could be used by the DSL, but we really should not
72
+ def import_class(klass)
73
+ definitions << self.class.from_class(klass, scoped_options)
74
+ end
75
+
76
+ ### Manifest sets
77
+ let(:rbac_manifests) { definitions.map(&descend_tree.(:rbac_manifests)) }
78
+ let(:cluster_manifests) { definitions.map(&descend_tree.(:cluster_manifests)) }
79
+ let(:descend_tree) { ->(m) { ->(x) { x.respond_to?(m) ? x.send(m) : x.map(&m) } } }
80
+
81
+ ### DSL methods
82
+ def scope(options = {}, &block)
83
+ definitions << self.class.new(options.merge(scoped_options: scoped_options), &block)
84
+ end
85
+
86
+ def namespace(name, &block)
87
+ scope(namespace: name, &block)
88
+ end
89
+
90
+ # Examples:
91
+ # role 'role-reader', namespace: 'default', api_groups: '', resources: :pods, verbs: %w[get watch list]
92
+ #
93
+ # role 'deployer', namespace: 'default' do
94
+ # rule api_groups: '', resources: 'pods', verbs: %w[get watch list]
95
+ # rule api_groups: '', resources: 'deployments, verbs: %w[get watch list]
96
+ #
97
+ # # `resources` are short-hand for the `rule` helper
98
+ # resources :pods, verbs: %w[get watch list]
99
+ # resources :pods, verbs: %w[get watch list], names: 'toolbox'
100
+ # resources :deployments, verbs: %w[patch update], names: %w[legalio-web legalio-resque legalio-resque-algolia]
101
+ #
102
+ # # Creates a single RoleBinding with the same name as the Role, in the same namespace
103
+ # # All bind_to are aggregated into subjects for that RoleBinding
104
+ # bind_to 'deployer', kind: :service_account # namespace defaults to role namespace
105
+ # bind_to 'deployer', kind: :service_account, namespace: 'qa0'
106
+ # bind_to 'alice@example.com', kind: :user
107
+ # bind_to 'sysadmins', kind: :group
108
+ # end
109
+ def role(name, options = {}, &block)
110
+ final_options = { name: name }.merge(options).merge(scoped_options: scoped_options)
111
+ require_namespace!('role', final_options)
112
+
113
+ definitions << Matsuri::DSL::Cluster::Role.new(final_options).tap do |role|
114
+ if options[:resources].present? && options[:verbs].present?
115
+ role.resources(options[:resources], names: options[:resource_names], verbs: options[:verbs], api_groups: options[:api_groups])
116
+ end
117
+
118
+ role.configure(&block)
119
+ end
120
+ end
121
+
122
+ # cluster_role 'traefik-ingress-controller' do
123
+ # # labels 'example.com/addon', 'traefik'
124
+ # # aggregate_to :admin, :edit
125
+ # # urls '/api', verbs: :get
126
+ #
127
+ # resources %w[services endpoints secrets], verbs: %w[get list watch]
128
+ # resources :ingresses, verbs: %w[get list watch], api_groups: :extensions
129
+ #
130
+ # # Creates a single ClusterRoleBinding with the same name as the ClusterRole
131
+ # # All bind_to are aggregated into subjects for that ClusterRoleBinding
132
+ # bind_to 'traefik-ingress-controller', kind: :service_account, namespace: 'kube-system'
133
+ # end
134
+ def cluster_role(name, options = {}, &block)
135
+ forbid_namespace!('cluster_role', options)
136
+ final_options = { name: name }.merge(options).merge(scoped_options: scoped_options)
137
+
138
+ definitions << Matsuri::DSL::Cluster::ClusterRole.new(final_options).tap do |role|
139
+ if options[:resources].present? && options[:verbs].present?
140
+ role.resources(options[:resources], names: options[:resource_names], verbs: options[:verbs], api_groups: options[:api_groups])
141
+ end
142
+
143
+ role.configure(&block)
144
+ end
145
+ end
146
+
147
+ # Defines a cluster role aggregated from other roles using label selectors
148
+ # Examples:
149
+ # aggregated_cluster_role 'platform-admin' do
150
+ # # Example using match labels
151
+ # match 'example.com/aggregate-to-admin' => 'true',
152
+ # 'rails-app' => 'frontend'
153
+ #
154
+ # # Example using match expressions
155
+ # match 'legal.io/aggregate-to-admin', :in, 'true'
156
+ # match 'legal.io/aggregate-to-admin', :in, %w[v1 v2 v3]
157
+ # match 'legal.io/aggregate-to-admin', :not_in, 'true'
158
+ # match 'legal.io/aggregate-to-admin', :not_in, 'true'
159
+ # match 'legal.io/aggregate', :exists
160
+ # match 'legal.io/aggregate', :does_not_exist
161
+ # end
162
+ def aggregated_cluster_role(name, options = {}, &block)
163
+ forbid_namespace!('aggregated_cluster_role', options)
164
+
165
+ final_options = { name: name }.merge(options).merge(scoped_options: scoped_options)
166
+ definitions << Matsuri::DSL::Cluster::AggregatedClusterRole.new(final_options, &block)
167
+ end
168
+
169
+ # Examples
170
+ # bind :role, 'failover-operator', namespace: :default do
171
+ # subject 'mongodb-operator', kind: :service_account, namespace: :default
172
+ # subject 'redis-operator', kind: :service_account, namespace: :default
173
+ # end
174
+ #
175
+ # namespace :default do
176
+ # bind :role, 'failover-operator', do
177
+ # subject 'mongodb-operator', kind: :service_account, namespace: :default
178
+ # subject 'redis-operator', kind: :service_account, namespace: :default
179
+ # end
180
+ # end
181
+ #
182
+ # bind :cluster_role, 'traefik-ingress-controller' do
183
+ # subject 'traefik-ingress-controller', kind: :service_account, namespace: 'kube-system'
184
+ # end
185
+ def bind(type, name, options = {}, &block)
186
+ case type
187
+ when :role then bind_role(name, options, &block)
188
+ when :cluster_role then bind_cluster_role(name, options, &block)
189
+ else
190
+ fail ArgumentError, "bind type must be :role or :cluster_role. Is: #{type}"
191
+ end
192
+ end
193
+
194
+ def bind_role(name, options = {}, &block)
195
+ final_options = { name: name }.merge(options).merge(type: :role, scoped_options: scoped_options)
196
+ require_namespace!('bind_role', final_options)
197
+
198
+ definitions << Matsuri::DSL::Cluster::Binding.new(final_options, &block)
199
+ end
200
+
201
+ def bind_cluster_role(name, options = {}, &block)
202
+ forbid_namespace!('bind_cluster_role', options)
203
+ final_options = { name: name }.merge(options).merge(type: :cluster_role, scoped_options: scoped_options)
204
+
205
+ definitions << Matsuri::DSL::Cluster::Binding.new(final_options, &block)
206
+ end
207
+
208
+ # Examples:
209
+ # service_account 'traefik-ingress-controller', namespace: 'kube-system'
210
+ #
211
+ # service_account 'deployer' do
212
+ # image_pull_secret 'gcp-private-regsitry'
213
+ #
214
+ # secret 'gcloud-service-account'
215
+ # secret 'aws-access-token'
216
+ # end
217
+ #
218
+ # service_account 'guest' do
219
+ # automount_token false
220
+ # end
221
+ def service_account(name, options = {}, &block)
222
+ final_options = { name: name }.merge(options).merge(type: :role, scoped_options: scoped_options)
223
+ require_namespace!('service_account', final_options)
224
+
225
+ definitions << Matsuri::DSL::Cluster::ServiceAccount.new(final_options, &block)
226
+ end
227
+
228
+ private
229
+
230
+ def require_namespace!(helper_name, final_options)
231
+ return if final_options[:namespace].present? || current_namespace.present?
232
+
233
+ fail ArgumentError,
234
+ "#{helper_name} requires namespace to be declared. Either pass namespace as an option or declare it inside " \
235
+ 'a namespace scope.'
236
+ end
237
+
238
+ def forbid_namespace!(helper_name, options)
239
+ fail ArgumentError, "#{helper_name} cannot be invoked inside a namespaced scope" unless current_namespace.nil?
240
+ fail ArgumentError, "#{helper_name} cannot be invoked with a namespace" unless options[:namespace].nil?
241
+ end
242
+ end
243
+ end
244
+ end
245
+ end