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.
- checksums.yaml +5 -5
- data/.envrc +1 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +9 -0
- data/AGENTS.md +18 -0
- data/CHANGELOG.md +642 -0
- data/Gemfile +2 -0
- data/README.md +86 -8
- data/Rakefile +20 -0
- data/bin/console +1 -0
- data/bin/matsuri +6 -0
- data/bin/setup +2 -0
- data/docs/acceptors.md +181 -0
- data/docs/boot.md +101 -0
- data/docs/design/acceptors.md +157 -0
- data/docs/design/boot.md +131 -0
- data/docs/design/ssa.md +142 -0
- data/docs/design/stacks.md +153 -0
- data/docs/diff.md +85 -0
- data/docs/known-issues.md +19 -0
- data/docs/local-dev.md +173 -0
- data/docs/presenters.md +317 -0
- data/docs/resources.md +121 -0
- data/docs/setup.md +97 -0
- data/docs/ssa.md +149 -0
- data/docs/stacks.md +146 -0
- data/flake.lock +61 -0
- data/flake.nix +35 -0
- data/lib/matsuri/acceptor.rb +33 -0
- data/lib/matsuri/acceptors/image_tag.rb +15 -0
- data/lib/matsuri/cmd.rb +46 -0
- data/lib/matsuri/cmds/apply.rb +78 -0
- data/lib/matsuri/cmds/cli.rb +147 -0
- data/lib/matsuri/cmds/create.rb +87 -0
- data/lib/matsuri/cmds/delete.rb +86 -0
- data/lib/matsuri/cmds/diff.rb +105 -0
- data/lib/matsuri/cmds/generate.rb +24 -0
- data/lib/matsuri/cmds/kubectl.rb +16 -0
- data/lib/matsuri/cmds/recreate.rb +65 -0
- data/lib/matsuri/cmds/scale.rb +37 -0
- data/lib/matsuri/cmds/show.rb +123 -0
- data/lib/matsuri/cmds/status.rb +67 -0
- data/lib/matsuri/concerns/awaiting.rb +24 -0
- data/lib/matsuri/concerns/diffable.rb +135 -0
- data/lib/matsuri/concerns/export_manifest.rb +56 -0
- data/lib/matsuri/concerns/import_manifest.rb +64 -0
- data/lib/matsuri/concerns/pod_template.rb +37 -0
- data/lib/matsuri/concerns/pods/spread_pods.rb +47 -0
- data/lib/matsuri/concerns/pods/spread_topology.rb +63 -0
- data/lib/matsuri/concerns/registry_helpers.rb +59 -0
- data/lib/matsuri/config.rb +101 -0
- data/lib/matsuri/core_ext.rb +107 -0
- data/lib/matsuri/dsl/cluster/aggregated_cluster_role.rb +75 -0
- data/lib/matsuri/dsl/cluster/binding.rb +118 -0
- data/lib/matsuri/dsl/cluster/cluster_role.rb +41 -0
- data/lib/matsuri/dsl/cluster/role.rb +49 -0
- data/lib/matsuri/dsl/cluster/scope.rb +245 -0
- data/lib/matsuri/dsl/cluster/service_account.rb +76 -0
- data/lib/matsuri/dsl/concerns/default_binding.rb +34 -0
- data/lib/matsuri/dsl/concerns/manifest_set.rb +21 -0
- data/lib/matsuri/dsl/concerns/metadata.rb +56 -0
- data/lib/matsuri/dsl/concerns/rbac_rules.rb +39 -0
- data/lib/matsuri/initializer.rb +41 -0
- data/lib/matsuri/kubernetes/base.rb +107 -0
- data/lib/matsuri/kubernetes/config_map.rb +100 -0
- data/lib/matsuri/kubernetes/cron_job.rb +115 -0
- data/lib/matsuri/kubernetes/daemon_set.rb +62 -0
- data/lib/matsuri/kubernetes/deployment.rb +65 -0
- data/lib/matsuri/kubernetes/endpoints.rb +57 -0
- data/lib/matsuri/kubernetes/horizontal_pod_autoscaler.rb +116 -0
- data/lib/matsuri/kubernetes/imported_manifest.rb +61 -0
- data/lib/matsuri/kubernetes/ingress.rb +66 -0
- data/lib/matsuri/kubernetes/persistent_volume.rb +56 -0
- data/lib/matsuri/kubernetes/persistent_volume_claim.rb +41 -0
- data/lib/matsuri/kubernetes/pod.rb +355 -0
- data/lib/matsuri/kubernetes/replica_set.rb +46 -0
- data/lib/matsuri/kubernetes/secret.rb +86 -0
- data/lib/matsuri/kubernetes/service.rb +49 -0
- data/lib/matsuri/kubernetes/stateful_set.rb +75 -0
- data/lib/matsuri/kubernetes/storage_class.rb +45 -0
- data/lib/matsuri/platform.rb +38 -0
- data/lib/matsuri/presenter.rb +47 -0
- data/lib/matsuri/presenters/helpers.rb +39 -0
- data/lib/matsuri/presenters/prune_empty.rb +59 -0
- data/lib/matsuri/presenters/seed_image.rb +28 -0
- data/lib/matsuri/presenters/seed_replicas.rb +25 -0
- data/lib/matsuri/presenters/seed_resources.rb +29 -0
- data/lib/matsuri/registry.rb +267 -0
- data/lib/matsuri/shell_out.rb +119 -0
- data/lib/matsuri/stack.rb +276 -0
- data/lib/matsuri/task.rb +20 -0
- data/lib/matsuri/tasks/cluster.rb +79 -0
- data/lib/matsuri/tasks/docker.rb +25 -0
- data/lib/matsuri/tasks/hash_diff.rb +61 -0
- data/lib/matsuri/tasks/kubectl.rb +21 -0
- data/lib/matsuri/tasks/kubectl_ops/apply.rb +24 -0
- data/lib/matsuri/tasks/kubectl_ops/create.rb +27 -0
- data/lib/matsuri/tasks/kubectl_ops/delete.rb +21 -0
- data/lib/matsuri/tasks/kubectl_ops/diff.rb +47 -0
- data/lib/matsuri/tasks/kubectl_ops/get.rb +36 -0
- data/lib/matsuri/tasks/kubectl_ops/recreate.rb +35 -0
- data/lib/matsuri/tasks/kubectl_ops/replace.rb +29 -0
- data/lib/matsuri/tasks/kubectl_ops/rollout_status.rb +22 -0
- data/lib/matsuri/tasks/kubectl_ops/scale.rb +22 -0
- data/lib/matsuri/tasks/kubectl_ops/server_side_apply.rb +32 -0
- data/lib/matsuri/tasks/kubectl_ops/status.rb +20 -0
- data/lib/matsuri/tasks/kubectl_ops.rb +218 -0
- data/lib/matsuri/tasks/pod.rb +56 -0
- data/lib/matsuri/tasks/stacks/seed.rb +93 -0
- data/lib/matsuri/tasks/stacks.rb +31 -0
- data/lib/matsuri/transform/algebra.rb +52 -0
- data/lib/matsuri/transform/callable.rb +54 -0
- data/lib/matsuri/transform/combinators.rb +61 -0
- data/lib/matsuri/transform/env.rb +43 -0
- data/lib/matsuri/transform/kubernetes.rb +89 -0
- data/lib/matsuri/transform.rb +20 -0
- data/lib/matsuri/version.rb +3 -1
- data/lib/matsuri.rb +158 -2
- data/matsuri.gemspec +20 -7
- data/tests/bin/minikube-start.sh +27 -0
- data/tests/bin/probe-save-config.sh +46 -0
- data/tests/bin/probe-ssa.sh +206 -0
- data/tests/example/.gitignore +1 -0
- data/tests/example/Gemfile +7 -0
- data/tests/example/README.md +91 -0
- data/tests/example/bin/matsuri +14 -0
- data/tests/example/config/initializer.rb +12 -0
- data/tests/example/config/matsuri.rb +9 -0
- data/tests/example/config/platform.rb +39 -0
- data/tests/example/lib/acme/nodes/dedicated.rb +26 -0
- data/tests/example/lib/acme/nodes/preemptible.rb +17 -0
- data/tests/example/lib/acme/nodes/required_cores.rb +26 -0
- data/tests/example/lib/acme/nodes/resource_class.rb +26 -0
- data/tests/example/lib/acme/pods/cacerts.rb +15 -0
- data/tests/example/lib/acme/pods/healthcheck.rb +36 -0
- data/tests/example/lib/acme.rb +26 -0
- data/tests/example/platform/config_maps/billing-settings.rb +8 -0
- data/tests/example/platform/config_maps/cacerts.rb +9 -0
- data/tests/example/platform/deployments/billing-web.rb +5 -0
- data/tests/example/platform/deployments/checkout-web.rb +6 -0
- data/tests/example/platform/namespaces/billing-dev.yaml +7 -0
- data/tests/example/platform/namespaces/checkout-dev.yaml +7 -0
- data/tests/example/platform/pods/billing.rb +31 -0
- data/tests/example/platform/pods/checkout.rb +27 -0
- data/tests/example/platform/services/billing-web.rb +6 -0
- data/tests/example/platform/services/checkout-web.rb +4 -0
- data/tests/example/platform/stacks/billing.rb +8 -0
- data/tests/example/platform/stacks/checkout.rb +19 -0
- data/tests/k8s/save-config/deployment-declared.yaml +19 -0
- data/tests/k8s/save-config/deployment-seeded.yaml +20 -0
- data/tests/k8s/save-config/namespace.yaml +4 -0
- data/tests/k8s/ssa/declared-bumped.yaml +22 -0
- data/tests/k8s/ssa/declared.yaml +22 -0
- data/tests/k8s/ssa/namespace.yaml +4 -0
- data/tests/k8s/ssa/seed-via-create.yaml +24 -0
- metadata +288 -20
- 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
|