From 5d5646b473a917194d13e8bc522c72a897034cc1 Mon Sep 17 00:00:00 2001 From: Matt Conway Date: Thu, 17 Jun 2021 15:39:08 -0400 Subject: [PATCH] add context to projectmappings to allow for small modifications (e.g. name, namespace) without having to replace the entire template Made resource_templates into a map, and made override merging of it (and context) be additive in nature so one can add a new mapping with a template without having to replace the existing ones --- README.md | 45 +--- helm/helmv2/templates/projectmapping.yaml | 9 +- helm/kubetruth/crds/projectmapping.yaml | 9 +- helm/kubetruth/values.yaml | 13 +- lib/kubetruth/config.rb | 21 +- lib/kubetruth/etl.rb | 15 +- lib/kubetruth/kubeapi.rb | 13 +- lib/kubetruth/template.rb | 38 ++- .../apply_resource/creates_a_resource.yml | 254 ++++++++++++++++++ .../can_get_project_mappings.yml | 29 +- spec/kubetruth/config_spec.rb | 64 ++++- spec/kubetruth/etl_spec.rb | 28 +- spec/kubetruth/kubeapi_spec.rb | 29 +- spec/kubetruth/template_spec.rb | 54 ++++ 14 files changed, 496 insertions(+), 125 deletions(-) create mode 100644 spec/fixtures/vcr/Kubetruth_KubeApi/apply_resource/creates_a_resource.yml diff --git a/README.md b/README.md index c283192..ac4920d 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ Parameterize the helm install with `--set appSettings.**` to control how kubetru | projectMappings.root.key_selector | A regexp to limit the keys acted against (client-side). Supplies any named matches for template evaluation | string | "" | no | | projectMappings.root.skip | Skips the generation of resources for the selected projects | flag | false | no | | projectMappings.root.included_projects | Include the parameters from other projects into the selected ones. This can be recursive in a depth first fashion, so if A imports B and B imports C, then A will get B's and C's parameters. For key conflicts, if A includes B and B includes C, then the precendence is A overrides B overrides C. If A includes \[B, C], then the precendence is A overrides C overrides B. | list | [] | no | +| projectMappings.root.context | Additional variables made available to the resource templates. Can also be templates | string | [default](helm/kubetruth/values.yaml#L93-L129) | no | | projectMappings.root.resource_templates | The templates to use in generating kubernetes resources (ConfigMap/Secrets/other) | string | [default](helm/kubetruth/values.yaml#L93-L129) | no | | projectMappings..* | Define override mappings to override settings from the root selector for specific projects. When doing this on the command-line (e.g. for `helm install`), it may be more convenient to use `--values ` instead of `--set` for large data sets | map | {} | no | @@ -136,16 +137,19 @@ them wakes it up from a polling sleep. This makes it quick and easy to test out configuration changes without having a short polling interval. To customize how the kubernetes resources are generated, edit the -`resource_templatse` property in the ProjectMappings. These templates are +`resource_templates` property in the ProjectMappings. These templates are processed using the [Liquid template language](https://shopify.github.io/liquid/), and can reference the following liquid variables: * `project` - The project name * `project_heirarchy` - The `included_projects` tree this project includes (useful to debug when using complex `included_projects`) + * `debug` - Indicates if kubetruth is operating in debug (logging) mode. * `parameters` - The CloudTruth parameters from the project * `parameter_origins` - The projects each parameter originates from (useful to debug when using complex `included_projects`) - * `debug` - Indicates if kubetruth is operating in debug (logging) mode. + * `secrets` - The CloudTruth secrets from the project + * `secret_origins` - The projects each secret originates from (useful to debug when using complex `included_projects`) + * `context` - A hash of context variables supplied from ProjectMappings (useful to override portions of templates without having to replace them completely in an override) In addition to the built in liquid filters, kubetruth also define a few custom ones: @@ -227,21 +231,9 @@ metadata: spec: scope: override project_selector: funkyProject - resource_templates: - - | - apiVersion: v1 - kind: ConfigMap - metadata: - namespace: notSoFunkyNamespace - name: notSoFunkyConfigMap - - - | - apiVersion: v1 - kind: Secret - metadata: - namespace: notSoFunkyNamespace - name: notSoFunkySecret - + context: + resource_name: notSoFunkyConfigMap + resource_namespace: notSoFunkyNamespace EOF ``` @@ -274,24 +266,7 @@ kubetruth-root root ^service 27m $ kubectl describe pm kubetruth-root Name: kubetruth-root Namespace: default -Labels: ... -Annotations: ... -API Version: kubetruth.cloudtruth.com/v1 -Kind: ProjectMapping -Metadata: - -Spec: - resource_templates: - - | - - - | - - included_projects: - key_selector: - project_selector: - scope: root - skip: false -Events: + ``` ## Development diff --git a/helm/helmv2/templates/projectmapping.yaml b/helm/helmv2/templates/projectmapping.yaml index bd29d0b..1801e94 100644 --- a/helm/helmv2/templates/projectmapping.yaml +++ b/helm/helmv2/templates/projectmapping.yaml @@ -32,9 +32,14 @@ spec: items: type: string description: Include the parameters from other projects into the selected ones. This can be recursive in a depth first fashion, so if A imports B and B imports C, then A will get B's and C's parameters. For key conflicts, if A includes B and B includes C, then the precendence is A overrides B overrides C. If A includes [B, C], then the precendence is A overrides C overrides B. + context: + type: object + additionalProperties: + type: string + description: Context variables that can be used by templates. The values can also be templates resource_templates: - type: array - items: + type: object + additionalProperties: type: string description: The templates to use in generating kubernetes resources additionalPrinterColumns: diff --git a/helm/kubetruth/crds/projectmapping.yaml b/helm/kubetruth/crds/projectmapping.yaml index e59aad3..3854678 100644 --- a/helm/kubetruth/crds/projectmapping.yaml +++ b/helm/kubetruth/crds/projectmapping.yaml @@ -34,9 +34,14 @@ spec: items: type: string description: Include the parameters from other projects into the selected ones. This can be recursive in a depth first fashion, so if A imports B and B imports C, then A will get B's and C's parameters. For key conflicts, if A includes B and B includes C, then the precendence is A overrides B overrides C. If A includes [B, C], then the precendence is A overrides C overrides B. + context: + type: object + additionalProperties: + type: string + description: Context variables that can be used by templates. The values can also be templates resource_templates: - type: array - items: + type: object + additionalProperties: type: string description: The templates to use in generating kubernetes resources required: diff --git a/helm/kubetruth/values.yaml b/helm/kubetruth/values.yaml index 06a6ae5..a8df2bd 100644 --- a/helm/kubetruth/values.yaml +++ b/helm/kubetruth/values.yaml @@ -89,13 +89,17 @@ projectMappings: key_selector: "" skip: false included_projects: [] + context: + resource_name: "{{ project | dns_safe }}" + resource_namespace: "" resource_templates: - - | + configmap: | {%- if parameters.size > 0 %} apiVersion: v1 kind: ConfigMap metadata: - name: "{{ project | dns_safe }}" + name: "{{ context.resource_name }}" + namespace: "{{ context.resource_namespace }}" labels: version: "{{ parameters | sort | to_json | sha256 | slice: 0, 7 }}" annotations: @@ -109,12 +113,13 @@ projectMappings: {%- endfor %} {%- endif %} - - | + secret: | {%- if secrets.size > 0 %} apiVersion: v1 kind: Secret metadata: - name: "{{ project | dns_safe }}" + name: "{{ context.resource_name }}" + namespace: "{{ context.resource_namespace }}" labels: version: "{{ secrets | sort | to_json | sha256 | slice: 0, 7 }}" annotations: diff --git a/lib/kubetruth/config.rb b/lib/kubetruth/config.rb index 4e9add2..d10f921 100644 --- a/lib/kubetruth/config.rb +++ b/lib/kubetruth/config.rb @@ -13,6 +13,7 @@ class DuplicateSelection < Kubetruth::Error; end :key_selector, :skip, :included_projects, + :context, :resource_templates, keyword_init: true ) do @@ -23,17 +24,17 @@ def initialize(*args, **kwargs) def convert_types(hash) selector_key_pattern = /_selector$/ - template_key_pattern = /_templates?$/ + template_key_pattern = /_template$/ hash.merge(hash) do |k, v| case k when selector_key_pattern Regexp.new(v) when template_key_pattern - if k.ends_with?('s') - v.collect {|t| Kubetruth::Template.new(t) } - else - Kubetruth::Template.new(v) - end + Kubetruth::Template.new(v) + when /^resource_templates$/ + Hash[v.collect {|k, t| [k.to_s, Kubetruth::Template.new(t)] }] + when /^context$/ + Kubetruth::Template::TemplateHashDrop.new(v) else v end @@ -48,6 +49,7 @@ def convert_types(hash) key_selector: '', skip: false, included_projects: [], + context: {}, resource_templates: [] }.freeze @@ -66,7 +68,12 @@ def load config = DEFAULT_SPEC.merge(root_mapping) @root_spec = ProjectSpec.new(**config) - @override_specs = overrides.collect { |o| ProjectSpec.new(**config.merge(o)) } + logger.debug { "ProjectSpec for root mapping: #{@root_spec.inspect}"} + @override_specs = overrides.collect do |o| + spec = ProjectSpec.new(**config.deep_merge(o)) + logger.debug { "ProjectSpec for override mapping: #{spec.inspect}"} + spec + end config end end diff --git a/lib/kubetruth/etl.rb b/lib/kubetruth/etl.rb index 8a55b3c..0a54b0e 100644 --- a/lib/kubetruth/etl.rb +++ b/lib/kubetruth/etl.rb @@ -120,8 +120,9 @@ def apply config_origins = Hash[param_origins_parts[true] || []] secret_origins = Hash[param_origins_parts[false] || []] - project.spec.resource_templates.each_with_index do |template, i| - logger.debug { "Processing template #{i}/#{project.spec.resource_templates.size}" } + project.spec.resource_templates.each_with_index do |pair, i| + template_name, template = *pair + logger.debug { "Processing template '#{template_name}' (#{i}/#{project.spec.resource_templates.size})" } resource_yml = template.render( project: project.name, project_heirarchy: project.heirarchy, @@ -129,7 +130,8 @@ def apply parameters: config_param_hash, parameter_origins: config_origins, secrets: secret_param_hash, - secret_origins: secret_origins + secret_origins: secret_origins, + context: project.spec.context ) parsed_yml = YAML.safe_load(resource_yml) if parsed_yml @@ -148,8 +150,13 @@ def params_to_hash(param_list) def kube_apply(parsed_yml) kind = parsed_yml["kind"] - namespace = parsed_yml["metadata"]["namespace"] || kubeapi.namespace name = parsed_yml["metadata"]["name"] + namespace = parsed_yml["metadata"]["namespace"] + if namespace.blank? + namespace = parsed_yml["metadata"]["namespace"] = kubeapi.namespace + end + + kubeapi.set_managed(parsed_yml) ident = "'#{namespace}:#{kind}:#{name}'" logger.info("Applying kubernetes resource #{ident}") diff --git a/lib/kubetruth/kubeapi.rb b/lib/kubetruth/kubeapi.rb index abf01fa..5ec385c 100644 --- a/lib/kubetruth/kubeapi.rb +++ b/lib/kubetruth/kubeapi.rb @@ -15,7 +15,6 @@ def initialize(namespace: nil, token: nil, api_url: nil) token_path = '/var/run/secrets/kubernetes.io/serviceaccount/token' @namespace = namespace.present? ? namespace : File.read(namespace_path).chomp - @labels = {MANAGED_LABEL_KEY => MANAGED_LABEL_VALUE} @auth_options = {} if token @@ -64,14 +63,14 @@ def ensure_namespace(ns = namespace) end def under_management?(resource) - labels = resource&.metadata&.labels - labels.nil? ? false : resource.metadata.labels[MANAGED_LABEL_KEY] == MANAGED_LABEL_VALUE + labels = resource&.[]("metadata")&.[]("labels") + labels.nil? ? false : resource["metadata"]["labels"][MANAGED_LABEL_KEY] == MANAGED_LABEL_VALUE end def set_managed(resource) - resource.metadata ||= {} - resource.metadata.labels ||= {} - resource.metadata.labels[MANAGED_LABEL_KEY] = MANAGED_LABEL_VALUE + resource["metadata"] ||= {} + resource["metadata"]["labels"] ||= {} + resource["metadata"]["labels"][MANAGED_LABEL_KEY] = MANAGED_LABEL_VALUE end def get_resource(resource_name, name, namespace=nil) @@ -80,9 +79,7 @@ def get_resource(resource_name, name, namespace=nil) def apply_resource(resource) resource = Kubeclient::Resource.new(resource) if resource.is_a? Hash - set_managed(resource) resource_name = resource.kind.downcase.pluralize - resource.metadata.namespace ||= self.namespace client.apply_entity(resource_name, resource, field_manager: "kubetruth") end diff --git a/lib/kubetruth/template.rb b/lib/kubetruth/template.rb index 4661a96..6ed6e25 100644 --- a/lib/kubetruth/template.rb +++ b/lib/kubetruth/template.rb @@ -10,6 +10,30 @@ class Template class Error < ::StandardError end + class TemplateHashDrop < Liquid::Drop + + attr_reader :source + + def initialize(template_hash) + @source = template_hash.stringify_keys + @parsed = {} + end + + def liquid_method_missing(key) + if @source.has_key?(key) + @parsed[key] ||= Template.new(@source[key]) + @parsed[key].render(@context) + else + super + end + end + + def inspect + {source: @source, parsed: @parsed}.inspect + end + + end + module CustomLiquidFilters # From kubernetes error message @@ -93,7 +117,7 @@ def initialize(template_source) INDENT = (" " * 2) - def render(**kwargs) + def render(*args, **kwargs) begin logger.debug do @@ -104,7 +128,7 @@ def render(**kwargs) msg end - result = @liquid.render!(kwargs.stringify_keys, strict_variables: true, strict_filters: true) + result = @liquid.render!(*args, kwargs.stringify_keys, strict_variables: true, strict_filters: true) logger.debug do msg = "Rendered template:\n" @@ -115,6 +139,16 @@ def render(**kwargs) result rescue Liquid::Error => e + indent = " " + msg = "Template failed to render:\n" + @source.lines.each {|l| msg << (indent * 2) << l } + msg << indent << "with error message:\n" << (indent * 2) << "#{e.message}" + if e.is_a?(Liquid::UndefinedVariable) + msg << "\n" << indent << "and variable context:\n" + msg << (indent * 2) << kwargs.inspect + end + raise Error, msg + raise Error, "Template failed to render: #{e.message}" end end diff --git a/spec/fixtures/vcr/Kubetruth_KubeApi/apply_resource/creates_a_resource.yml b/spec/fixtures/vcr/Kubetruth_KubeApi/apply_resource/creates_a_resource.yml new file mode 100644 index 0000000..562cf7e --- /dev/null +++ b/spec/fixtures/vcr/Kubetruth_KubeApi/apply_resource/creates_a_resource.yml @@ -0,0 +1,254 @@ +--- +http_interactions: +- request: + method: get + uri: https://127.0.0.1:60761/api/v1 + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*" + User-Agent: + - rest-client/2.1.0 (darwin20 x86_64) ruby/2.7.3p183 + Authorization: + - Bearer + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Host: + - 127.0.0.1:60761 + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, private + Content-Type: + - application/json + X-Kubernetes-Pf-Flowschema-Uid: + - c6d9dbd7-dd7c-4844-9970-a2c737a707e7 + X-Kubernetes-Pf-Prioritylevel-Uid: + - 3bc95a2c-9c78-4c16-8ee2-9c0b68ead8f6 + Date: + - Thu, 17 Jun 2021 19:03:34 GMT + Transfer-Encoding: + - chunked + body: + encoding: UTF-8 + string: '{"kind":"APIResourceList","groupVersion":"v1","resources":[{"name":"bindings","singularName":"","namespaced":true,"kind":"Binding","verbs":["create"]},{"name":"componentstatuses","singularName":"","namespaced":false,"kind":"ComponentStatus","verbs":["get","list"],"shortNames":["cs"]},{"name":"configmaps","singularName":"","namespaced":true,"kind":"ConfigMap","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["cm"],"storageVersionHash":"qFsyl6wFWjQ="},{"name":"endpoints","singularName":"","namespaced":true,"kind":"Endpoints","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["ep"],"storageVersionHash":"fWeeMqaN/OA="},{"name":"events","singularName":"","namespaced":true,"kind":"Event","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["ev"],"storageVersionHash":"r2yiGXH7wu8="},{"name":"limitranges","singularName":"","namespaced":true,"kind":"LimitRange","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["limits"],"storageVersionHash":"EBKMFVe6cwo="},{"name":"namespaces","singularName":"","namespaced":false,"kind":"Namespace","verbs":["create","delete","get","list","patch","update","watch"],"shortNames":["ns"],"storageVersionHash":"Q3oi5N2YM8M="},{"name":"namespaces/finalize","singularName":"","namespaced":false,"kind":"Namespace","verbs":["update"]},{"name":"namespaces/status","singularName":"","namespaced":false,"kind":"Namespace","verbs":["get","patch","update"]},{"name":"nodes","singularName":"","namespaced":false,"kind":"Node","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["no"],"storageVersionHash":"XwShjMxG9Fs="},{"name":"nodes/proxy","singularName":"","namespaced":false,"kind":"NodeProxyOptions","verbs":["create","delete","get","patch","update"]},{"name":"nodes/status","singularName":"","namespaced":false,"kind":"Node","verbs":["get","patch","update"]},{"name":"persistentvolumeclaims","singularName":"","namespaced":true,"kind":"PersistentVolumeClaim","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["pvc"],"storageVersionHash":"QWTyNDq0dC4="},{"name":"persistentvolumeclaims/status","singularName":"","namespaced":true,"kind":"PersistentVolumeClaim","verbs":["get","patch","update"]},{"name":"persistentvolumes","singularName":"","namespaced":false,"kind":"PersistentVolume","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["pv"],"storageVersionHash":"HN/zwEC+JgM="},{"name":"persistentvolumes/status","singularName":"","namespaced":false,"kind":"PersistentVolume","verbs":["get","patch","update"]},{"name":"pods","singularName":"","namespaced":true,"kind":"Pod","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["po"],"categories":["all"],"storageVersionHash":"xPOwRZ+Yhw8="},{"name":"pods/attach","singularName":"","namespaced":true,"kind":"PodAttachOptions","verbs":["create","get"]},{"name":"pods/binding","singularName":"","namespaced":true,"kind":"Binding","verbs":["create"]},{"name":"pods/eviction","singularName":"","namespaced":true,"group":"policy","version":"v1beta1","kind":"Eviction","verbs":["create"]},{"name":"pods/exec","singularName":"","namespaced":true,"kind":"PodExecOptions","verbs":["create","get"]},{"name":"pods/log","singularName":"","namespaced":true,"kind":"Pod","verbs":["get"]},{"name":"pods/portforward","singularName":"","namespaced":true,"kind":"PodPortForwardOptions","verbs":["create","get"]},{"name":"pods/proxy","singularName":"","namespaced":true,"kind":"PodProxyOptions","verbs":["create","delete","get","patch","update"]},{"name":"pods/status","singularName":"","namespaced":true,"kind":"Pod","verbs":["get","patch","update"]},{"name":"podtemplates","singularName":"","namespaced":true,"kind":"PodTemplate","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"storageVersionHash":"LIXB2x4IFpk="},{"name":"replicationcontrollers","singularName":"","namespaced":true,"kind":"ReplicationController","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["rc"],"categories":["all"],"storageVersionHash":"Jond2If31h0="},{"name":"replicationcontrollers/scale","singularName":"","namespaced":true,"group":"autoscaling","version":"v1","kind":"Scale","verbs":["get","patch","update"]},{"name":"replicationcontrollers/status","singularName":"","namespaced":true,"kind":"ReplicationController","verbs":["get","patch","update"]},{"name":"resourcequotas","singularName":"","namespaced":true,"kind":"ResourceQuota","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["quota"],"storageVersionHash":"8uhSgffRX6w="},{"name":"resourcequotas/status","singularName":"","namespaced":true,"kind":"ResourceQuota","verbs":["get","patch","update"]},{"name":"secrets","singularName":"","namespaced":true,"kind":"Secret","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"storageVersionHash":"S6u1pOWzb84="},{"name":"serviceaccounts","singularName":"","namespaced":true,"kind":"ServiceAccount","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["sa"],"storageVersionHash":"pbx9ZvyFpBE="},{"name":"serviceaccounts/token","singularName":"","namespaced":true,"group":"authentication.k8s.io","version":"v1","kind":"TokenRequest","verbs":["create"]},{"name":"services","singularName":"","namespaced":true,"kind":"Service","verbs":["create","delete","get","list","patch","update","watch"],"shortNames":["svc"],"categories":["all"],"storageVersionHash":"0/CO1lhkEBI="},{"name":"services/proxy","singularName":"","namespaced":true,"kind":"ServiceProxyOptions","verbs":["create","delete","get","patch","update"]},{"name":"services/status","singularName":"","namespaced":true,"kind":"Service","verbs":["get","patch","update"]}]} + + ' + recorded_at: Thu, 17 Jun 2021 19:03:34 GMT +- request: + method: get + uri: https://127.0.0.1:60761/api/v1/namespaces/kubetruth-test-ns-arns + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*" + User-Agent: + - rest-client/2.1.0 (darwin20 x86_64) ruby/2.7.3p183 + Authorization: + - Bearer + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Host: + - 127.0.0.1:60761 + response: + status: + code: 404 + message: Not Found + headers: + Cache-Control: + - no-cache, private + Content-Type: + - application/json + X-Kubernetes-Pf-Flowschema-Uid: + - c6d9dbd7-dd7c-4844-9970-a2c737a707e7 + X-Kubernetes-Pf-Prioritylevel-Uid: + - 3bc95a2c-9c78-4c16-8ee2-9c0b68ead8f6 + Date: + - Thu, 17 Jun 2021 19:03:34 GMT + Content-Length: + - '224' + body: + encoding: UTF-8 + string: '{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"namespaces + \"kubetruth-test-ns-arns\" not found","reason":"NotFound","details":{"name":"kubetruth-test-ns-arns","kind":"namespaces"},"code":404} + + ' + recorded_at: Thu, 17 Jun 2021 19:03:34 GMT +- request: + method: post + uri: https://127.0.0.1:60761/api/v1/namespaces + body: + encoding: UTF-8 + string: '{"metadata":{"name":"kubetruth-test-ns-arns","labels":{"app.kubernetes.io/managed-by":"kubetruth"}},"kind":"Namespace","apiVersion":"v1"}' + headers: + Accept: + - "*/*" + User-Agent: + - rest-client/2.1.0 (darwin20 x86_64) ruby/2.7.3p183 + Content-Type: + - application/json + Authorization: + - Bearer + Content-Length: + - '137' + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Host: + - 127.0.0.1:60761 + response: + status: + code: 201 + message: Created + headers: + Cache-Control: + - no-cache, private + Content-Type: + - application/json + X-Kubernetes-Pf-Flowschema-Uid: + - c6d9dbd7-dd7c-4844-9970-a2c737a707e7 + X-Kubernetes-Pf-Prioritylevel-Uid: + - 3bc95a2c-9c78-4c16-8ee2-9c0b68ead8f6 + Date: + - Thu, 17 Jun 2021 19:03:34 GMT + Content-Length: + - '565' + body: + encoding: UTF-8 + string: '{"kind":"Namespace","apiVersion":"v1","metadata":{"name":"kubetruth-test-ns-arns","uid":"3c1871a4-a7fb-4940-9547-401fdaa1d0db","resourceVersion":"113447","creationTimestamp":"2021-06-17T19:03:34Z","labels":{"app.kubernetes.io/managed-by":"kubetruth"},"managedFields":[{"manager":"rest-client","operation":"Update","apiVersion":"v1","time":"2021-06-17T19:03:34Z","fieldsType":"FieldsV1","fieldsV1":{"f:metadata":{"f:labels":{".":{},"f:app.kubernetes.io/managed-by":{}}},"f:status":{"f:phase":{}}}}]},"spec":{"finalizers":["kubernetes"]},"status":{"phase":"Active"}} + + ' + recorded_at: Thu, 17 Jun 2021 19:03:34 GMT +- request: + method: get + uri: https://127.0.0.1:60761/api/v1/namespaces/kubetruth-test-ns-arns/configmaps/rspec-examplegroups-kubetruthkubeapi-applyresourcecreates-a-resource + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*" + User-Agent: + - rest-client/2.1.0 (darwin20 x86_64) ruby/2.7.3p183 + Authorization: + - Bearer + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Host: + - 127.0.0.1:60761 + response: + status: + code: 404 + message: Not Found + headers: + Cache-Control: + - no-cache, private + Content-Type: + - application/json + X-Kubernetes-Pf-Flowschema-Uid: + - c6d9dbd7-dd7c-4844-9970-a2c737a707e7 + X-Kubernetes-Pf-Prioritylevel-Uid: + - 3bc95a2c-9c78-4c16-8ee2-9c0b68ead8f6 + Date: + - Thu, 17 Jun 2021 19:03:34 GMT + Content-Length: + - '316' + body: + encoding: UTF-8 + string: '{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"configmaps + \"rspec-examplegroups-kubetruthkubeapi-applyresourcecreates-a-resource\" not + found","reason":"NotFound","details":{"name":"rspec-examplegroups-kubetruthkubeapi-applyresourcecreates-a-resource","kind":"configmaps"},"code":404} + + ' + recorded_at: Thu, 17 Jun 2021 19:03:34 GMT +- request: + method: patch + uri: https://127.0.0.1:60761/api/v1/namespaces/kubetruth-test-ns-arns/configmaps/rspec-examplegroups-kubetruthkubeapi-applyresourcecreates-a-resource?fieldManager=kubetruth&force=true + body: + encoding: UTF-8 + string: '{"apiVersion":"v1","kind":"ConfigMap","metadata":{"namespace":"kubetruth-test-ns-arns","name":"rspec-examplegroups-kubetruthkubeapi-applyresourcecreates-a-resource"},"data":{"bar":"baz"}}' + headers: + Accept: + - "*/*" + User-Agent: + - rest-client/2.1.0 (darwin20 x86_64) ruby/2.7.3p183 + Content-Type: + - application/apply-patch+yaml + Authorization: + - Bearer + Content-Length: + - '187' + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Host: + - 127.0.0.1:60761 + response: + status: + code: 201 + message: Created + headers: + Cache-Control: + - no-cache, private + Content-Type: + - application/json + X-Kubernetes-Pf-Flowschema-Uid: + - c6d9dbd7-dd7c-4844-9970-a2c737a707e7 + X-Kubernetes-Pf-Prioritylevel-Uid: + - 3bc95a2c-9c78-4c16-8ee2-9c0b68ead8f6 + Date: + - Thu, 17 Jun 2021 19:03:34 GMT + Content-Length: + - '472' + body: + encoding: UTF-8 + string: '{"kind":"ConfigMap","apiVersion":"v1","metadata":{"name":"rspec-examplegroups-kubetruthkubeapi-applyresourcecreates-a-resource","namespace":"kubetruth-test-ns-arns","uid":"af9b6bae-8be7-4193-9873-fdc953b77113","resourceVersion":"113451","creationTimestamp":"2021-06-17T19:03:34Z","managedFields":[{"manager":"kubetruth","operation":"Apply","apiVersion":"v1","time":"2021-06-17T19:03:34Z","fieldsType":"FieldsV1","fieldsV1":{"f:data":{"f:bar":{}}}}]},"data":{"bar":"baz"}} + + ' + recorded_at: Thu, 17 Jun 2021 19:03:34 GMT +- request: + method: get + uri: https://127.0.0.1:60761/api/v1/namespaces/kubetruth-test-ns-arns/configmaps/rspec-examplegroups-kubetruthkubeapi-applyresourcecreates-a-resource + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*" + User-Agent: + - rest-client/2.1.0 (darwin20 x86_64) ruby/2.7.3p183 + Authorization: + - Bearer + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Host: + - 127.0.0.1:60761 + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, private + Content-Type: + - application/json + X-Kubernetes-Pf-Flowschema-Uid: + - c6d9dbd7-dd7c-4844-9970-a2c737a707e7 + X-Kubernetes-Pf-Prioritylevel-Uid: + - 3bc95a2c-9c78-4c16-8ee2-9c0b68ead8f6 + Date: + - Thu, 17 Jun 2021 19:03:34 GMT + Content-Length: + - '472' + body: + encoding: UTF-8 + string: '{"kind":"ConfigMap","apiVersion":"v1","metadata":{"name":"rspec-examplegroups-kubetruthkubeapi-applyresourcecreates-a-resource","namespace":"kubetruth-test-ns-arns","uid":"af9b6bae-8be7-4193-9873-fdc953b77113","resourceVersion":"113451","creationTimestamp":"2021-06-17T19:03:34Z","managedFields":[{"manager":"kubetruth","operation":"Apply","apiVersion":"v1","time":"2021-06-17T19:03:34Z","fieldsType":"FieldsV1","fieldsV1":{"f:data":{"f:bar":{}}}}]},"data":{"bar":"baz"}} + + ' + recorded_at: Thu, 17 Jun 2021 19:03:34 GMT +recorded_with: VCR 6.0.0 diff --git a/spec/fixtures/vcr/Kubetruth_KubeApi/custom_resource/can_get_project_mappings.yml b/spec/fixtures/vcr/Kubetruth_KubeApi/custom_resource/can_get_project_mappings.yml index 4ff1c06..8f76c65 100644 --- a/spec/fixtures/vcr/Kubetruth_KubeApi/custom_resource/can_get_project_mappings.yml +++ b/spec/fixtures/vcr/Kubetruth_KubeApi/custom_resource/can_get_project_mappings.yml @@ -31,7 +31,7 @@ http_interactions: X-Kubernetes-Pf-Prioritylevel-Uid: - 3bc95a2c-9c78-4c16-8ee2-9c0b68ead8f6 Date: - - Wed, 16 Jun 2021 20:26:15 GMT + - Thu, 17 Jun 2021 19:22:14 GMT Content-Length: - '346' body: @@ -39,7 +39,7 @@ http_interactions: string: '{"kind":"APIResourceList","apiVersion":"v1","groupVersion":"kubetruth.cloudtruth.com/v1","resources":[{"name":"projectmappings","singularName":"projectmapping","namespaced":true,"kind":"ProjectMapping","verbs":["delete","deletecollection","get","list","patch","create","update","watch"],"shortNames":["pm"],"storageVersionHash":"UqtD9M7id/A="}]} ' - recorded_at: Wed, 16 Jun 2021 20:26:15 GMT + recorded_at: Thu, 17 Jun 2021 19:22:14 GMT - request: method: get uri: https://127.0.0.1:60761/apis/kubetruth.cloudtruth.com/v1/namespaces/kubetruth-test-ns/projectmappings @@ -71,27 +71,30 @@ http_interactions: X-Kubernetes-Pf-Prioritylevel-Uid: - 3bc95a2c-9c78-4c16-8ee2-9c0b68ead8f6 Date: - - Wed, 16 Jun 2021 20:26:15 GMT + - Thu, 17 Jun 2021 19:22:14 GMT Transfer-Encoding: - chunked body: encoding: UTF-8 - string: '{"apiVersion":"kubetruth.cloudtruth.com/v1","items":[{"apiVersion":"kubetruth.cloudtruth.com/v1","kind":"ProjectMapping","metadata":{"annotations":{"meta.helm.sh/release-name":"kubetruth-test-app","meta.helm.sh/release-namespace":"kubetruth-test-ns"},"creationTimestamp":"2021-06-16T20:26:14Z","generation":1,"labels":{"app.kubernetes.io/instance":"kubetruth-test-app","app.kubernetes.io/managed-by":"Helm","app.kubernetes.io/name":"kubetruth","app.kubernetes.io/version":"0.4.1","helm.sh/chart":"kubetruth-0.4.1"},"managedFields":[{"apiVersion":"kubetruth.cloudtruth.com/v1","fieldsType":"FieldsV1","fieldsV1":{"f:metadata":{"f:annotations":{".":{},"f:meta.helm.sh/release-name":{},"f:meta.helm.sh/release-namespace":{}},"f:labels":{".":{},"f:app.kubernetes.io/instance":{},"f:app.kubernetes.io/managed-by":{},"f:app.kubernetes.io/name":{},"f:app.kubernetes.io/version":{},"f:helm.sh/chart":{}}},"f:spec":{".":{},"f:included_projects":{},"f:key_selector":{},"f:project_selector":{},"f:resource_templates":{},"f:scope":{},"f:skip":{}}},"manager":"Go-http-client","operation":"Update","time":"2021-06-16T20:26:14Z"}],"name":"kubetruth-test-app-root","namespace":"kubetruth-test-ns","resourceVersion":"59424","uid":"1cfc520b-12e6-4ca5-b34a-1bde48cd7d57"},"spec":{"included_projects":[],"key_selector":"","project_selector":"","resource_templates":["{%- + string: '{"apiVersion":"kubetruth.cloudtruth.com/v1","items":[{"apiVersion":"kubetruth.cloudtruth.com/v1","kind":"ProjectMapping","metadata":{"annotations":{"meta.helm.sh/release-name":"kubetruth-test-app","meta.helm.sh/release-namespace":"kubetruth-test-ns"},"creationTimestamp":"2021-06-17T19:22:14Z","generation":1,"labels":{"app.kubernetes.io/instance":"kubetruth-test-app","app.kubernetes.io/managed-by":"Helm","app.kubernetes.io/name":"kubetruth","app.kubernetes.io/version":"0.4.1","helm.sh/chart":"kubetruth-0.4.1"},"managedFields":[{"apiVersion":"kubetruth.cloudtruth.com/v1","fieldsType":"FieldsV1","fieldsV1":{"f:metadata":{"f:annotations":{".":{},"f:meta.helm.sh/release-name":{},"f:meta.helm.sh/release-namespace":{}},"f:labels":{".":{},"f:app.kubernetes.io/instance":{},"f:app.kubernetes.io/managed-by":{},"f:app.kubernetes.io/name":{},"f:app.kubernetes.io/version":{},"f:helm.sh/chart":{}}},"f:spec":{".":{},"f:context":{".":{},"f:resource_name":{},"f:resource_namespace":{}},"f:included_projects":{},"f:key_selector":{},"f:project_selector":{},"f:resource_templates":{".":{},"f:configmap":{},"f:secret":{}},"f:scope":{},"f:skip":{}}},"manager":"Go-http-client","operation":"Update","time":"2021-06-17T19:22:14Z"}],"name":"kubetruth-test-app-root","namespace":"kubetruth-test-ns","resourceVersion":"114379","uid":"5f7a3067-3cd1-4caa-9144-60b8f4fadc75"},"spec":{"context":{"resource_name":"{{ + project | dns_safe }}","resource_namespace":""},"included_projects":[],"key_selector":"","project_selector":"","resource_templates":{"configmap":"{%- if parameters.size \u003e 0 %}\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: - \"{{ project | dns_safe }}\"\n labels:\n version: \"{{ parameters | sort - | to_json | sha256 | slice: 0, 7 }}\"\n annotations:\n kubetruth/project_heirarchy: - |\n {{ project_heirarchy | to_yaml | indent: 6 | lstrip }}\n kubtruth/parameter_origins: + \"{{ context.resource_name }}\"\n namespace: \"{{ context.resource_namespace + }}\"\n labels:\n version: \"{{ parameters | sort | to_json | sha256 | + slice: 0, 7 }}\"\n annotations:\n kubetruth/project_heirarchy: |\n {{ + project_heirarchy | to_yaml | indent: 6 | lstrip }}\n kubtruth/parameter_origins: |\n {{ parameter_origins | to_yaml | indent: 6 | lstrip }}\ndata:\n {%- - for parameter in parameters %}\n {{ parameter[0] | stringify }}: {{ parameter[1] - | stringify }}\n {%- endfor %}\n{%- endif %}\n","{%- if secrets.size \u003e - 0 %}\napiVersion: v1\nkind: Secret\nmetadata:\n name: \"{{ project | dns_safe + for parameter in parameters %}\n {{ parameter[0] | key_safe | stringify }}: + {{ parameter[1] | stringify }}\n {%- endfor %}\n{%- endif %}\n","secret":"{%- + if secrets.size \u003e 0 %}\napiVersion: v1\nkind: Secret\nmetadata:\n name: + \"{{ context.resource_name }}\"\n namespace: \"{{ context.resource_namespace }}\"\n labels:\n version: \"{{ secrets | sort | to_json | sha256 | slice: 0, 7 }}\"\n annotations:\n kubetruth/project_heirarchy: |\n {{ project_heirarchy | to_yaml | indent: 6 | lstrip }}\n kubtruth/parameter_origins: |\n {{ secret_origins | to_yaml | indent: 6 | lstrip }}\ndata:\n {%- for secret - in secrets %}\n {{ secret[0] | stringify }}: {{ secret[1] | encode64 | stringify - }}\n {%- endfor %}\n{%- endif %}\n"],"scope":"root","skip":false}}],"kind":"ProjectMappingList","metadata":{"continue":"","resourceVersion":"59435"}} + in secrets %}\n {{ secret[0] | key_safe | stringify }}: {{ secret[1] | encode64 + | stringify }}\n {%- endfor %}\n{%- endif %}\n"},"scope":"root","skip":false}}],"kind":"ProjectMappingList","metadata":{"continue":"","resourceVersion":"114391"}} ' - recorded_at: Wed, 16 Jun 2021 20:26:15 GMT + recorded_at: Thu, 17 Jun 2021 19:22:14 GMT recorded_with: VCR 6.0.0 diff --git a/spec/kubetruth/config_spec.rb b/spec/kubetruth/config_spec.rb index 532a5df..bed3aea 100644 --- a/spec/kubetruth/config_spec.rb +++ b/spec/kubetruth/config_spec.rb @@ -16,15 +16,18 @@ module Kubetruth spec = described_class::ProjectSpec.new( scope: "root", project_selector: "foo", - resource_templates: ["bar"], + context: {"name1" => "template1"}, + resource_templates: {"name1" => "template1"}, skip: true ) expect(spec.scope).to be_an_instance_of(String) expect(spec.scope).to eq("root") expect(spec.project_selector).to be_an_instance_of(Regexp) expect(spec.project_selector).to eq(/foo/) - expect(spec.resource_templates.first).to be_an_instance_of(Template) - expect(spec.resource_templates.first.source).to eq("bar") + expect(spec.context).to be_an_instance_of(Template::TemplateHashDrop) + expect(spec.context.liquid_method_missing("name1")).to eq("template1") + expect(spec.resource_templates["name1"]).to be_an_instance_of(Template) + expect(spec.resource_templates["name1"].source).to eq("template1") expect(spec.skip).to equal(true) end @@ -75,25 +78,62 @@ module Kubetruth key_selector: "key_selector", skip: true, included_projects: ["included_projects"], - resource_templates: ["resource_templates"] + context: {"name1" => "template1"}, + resource_templates: {"name1" => "template1"} }, { scope: "override", project_selector: "project_overrides:project_selector", - resource_templates: ["project_overrides:resource_templates"] + context: {"name1" => "override_template1"}, + resource_templates: {"name1" => "override_template1"} } ] config = described_class.new(data) config.load expect(config.instance_variable_get(:@config)).to_not eq(Kubetruth::Config::DEFAULT_SPEC) expect(config.root_spec).to be_an_instance_of(Kubetruth::Config::ProjectSpec) - expect(config.root_spec.resource_templates.first).to be_an_instance_of(Kubetruth::Template) - expect(config.root_spec.resource_templates.first.source).to eq("resource_templates") + expect(config.root_spec.context).to be_an_instance_of(Kubetruth::Template::TemplateHashDrop) + expect(config.root_spec.context.liquid_method_missing("name1")).to eq("template1") + expect(config.root_spec.resource_templates["name1"]).to be_an_instance_of(Kubetruth::Template) + expect(config.root_spec.resource_templates["name1"].source).to eq("template1") expect(config.root_spec.key_selector).to eq(/key_selector/) expect(config.override_specs.size).to eq(1) expect(config.override_specs.first).to be_an_instance_of(Kubetruth::Config::ProjectSpec) - expect(config.override_specs.first.resource_templates.first).to be_an_instance_of(Kubetruth::Template) - expect(config.override_specs.first.resource_templates.first.source).to eq("project_overrides:resource_templates") + expect(config.override_specs.first.context).to be_an_instance_of(Kubetruth::Template::TemplateHashDrop) + expect(config.override_specs.first.context.liquid_method_missing("name1")).to eq("override_template1") + expect(config.override_specs.first.resource_templates["name1"]).to be_an_instance_of(Kubetruth::Template) + expect(config.override_specs.first.resource_templates["name1"].source).to eq("override_template1") + end + + it "does deep merges on hash types" do + data = [ + { + scope: "root", + context: {"name1" => "template1", "name2" => "template2"}, + resource_templates: {"name1" => "template1", "name2" => "template2"} + }, + { + scope: "override", + context: {"name1" => "override_template1", "name3" => "override_template3"}, + resource_templates: {"name1" => "override_template1", "name3" => "override_template3"} + } + ] + config = described_class.new(data) + config.load + + expect(config.root_spec.context.liquid_method_missing("name1")).to eq("template1") + expect(config.root_spec.context.liquid_method_missing("name2")).to eq("template2") + expect(config.root_spec.context.liquid_method_missing("name3")).to be_nil + expect(config.root_spec.resource_templates["name1"].source).to eq("template1") + expect(config.root_spec.resource_templates["name2"].source).to eq("template2") + expect(config.root_spec.resource_templates["name3"]).to be_nil + + expect(config.override_specs.first.context.liquid_method_missing("name1")).to eq("override_template1") + expect(config.override_specs.first.context.liquid_method_missing("name2")).to eq("template2") + expect(config.override_specs.first.context.liquid_method_missing("name3")).to eq("override_template3") + expect(config.override_specs.first.resource_templates["name1"].source).to eq("override_template1") + expect(config.override_specs.first.resource_templates["name2"].source).to eq("template2") + expect(config.override_specs.first.resource_templates["name3"].source).to eq("override_template3") end end @@ -129,11 +169,11 @@ module Kubetruth end it "returns the matching override specs" do - config = described_class.new([{scope: "override", project_selector: "fo+", resource_templates: ["foocm"]}]) + config = described_class.new([{scope: "override", project_selector: "fo+", resource_templates: {"name1" => "template1"}}]) spec = config.spec_for_project("foo") expect(spec).to_not equal(config.root_spec) - expect(spec.resource_templates.first).to be_an_instance_of(Kubetruth::Template) - expect(spec.resource_templates.first.source).to eq("foocm") + expect(spec.resource_templates["name1"]).to be_an_instance_of(Kubetruth::Template) + expect(spec.resource_templates["name1"].source).to eq("template1") end it "raises for multiple matching specs" do diff --git a/spec/kubetruth/etl_spec.rb b/spec/kubetruth/etl_spec.rb index d329e43..9c548ee 100644 --- a/spec/kubetruth/etl_spec.rb +++ b/spec/kubetruth/etl_spec.rb @@ -15,6 +15,7 @@ def kubeapi allow(kapi).to receive(:get_resource).and_return(Kubeclient::Resource.new) allow(kapi).to receive(:apply_resource) allow(kapi).to receive(:under_management?).and_return(true) + allow(kapi).to receive(:set_managed) allow(kapi).to receive(:ensure_namespace) allow(kapi).to receive(:namespace).and_return("default") allow(kapi).to receive(:get_project_mappings).and_return([]) @@ -163,6 +164,7 @@ class ForceExit < Exception; end parsed_yml = YAML.load(resource_yml) expect(@kubeapi).to receive(:ensure_namespace).with(@kubeapi.namespace) expect(@kubeapi).to receive(:get_resource).with("configmaps", "group1", @kubeapi.namespace).and_raise(Kubeclient::ResourceNotFoundError.new(1, "", 2)) + expect(@kubeapi).to receive(:set_managed) expect(@kubeapi).to_not receive(:under_management?) expect(@kubeapi).to receive(:apply_resource).with(parsed_yml) etl.kube_apply(parsed_yml) @@ -181,6 +183,7 @@ class ForceExit < Exception; end parsed_yml = YAML.load(resource_yml) resource = Kubeclient::Resource.new(parsed_yml.merge(data: {param1: "oldvalue"})) expect(@kubeapi).to receive(:get_resource).with("configmaps", "group1", @kubeapi.namespace).and_return(resource) + expect(@kubeapi).to receive(:set_managed) expect(@kubeapi).to receive(:under_management?).and_return(true) expect(@kubeapi).to receive(:apply_resource).with(parsed_yml) etl.kube_apply(parsed_yml) @@ -199,6 +202,7 @@ class ForceExit < Exception; end parsed_yml = YAML.load(resource_yml) resource = Kubeclient::Resource.new(parsed_yml) expect(@kubeapi).to receive(:get_resource).with("configmaps", "group1", @kubeapi.namespace).and_return(resource) + expect(@kubeapi).to receive(:set_managed) expect(@kubeapi).to receive(:under_management?).and_return(false) expect(@kubeapi).to_not receive(:apply_resource) etl.kube_apply(parsed_yml) @@ -211,14 +215,16 @@ class ForceExit < Exception; end kind: ConfigMap metadata: name: "group1" + namespace: "ns1" data: "param1": "value1" EOF parsed_yml = YAML.load(resource_yml) resource = Kubeclient::Resource.new(parsed_yml) - expect(@kubeapi).to receive(:get_resource).with("configmaps", "group1", @kubeapi.namespace).and_return(resource) + expect(@kubeapi).to receive(:get_resource).with("configmaps", "group1", "ns1").and_return(resource) + expect(@kubeapi).to receive(:set_managed) # test double, so doesn't actually set the label expect(@kubeapi).to receive(:under_management?).and_return(true) - expect(@kubeapi).to_not receive(:apply_resource).with(parsed_yml) + expect(@kubeapi).to_not receive(:apply_resource) etl.kube_apply(parsed_yml) expect(Logging.contents).to match(/Skipping update for identical kubernetes resource/) end @@ -236,6 +242,7 @@ class ForceExit < Exception; end parsed_yml = YAML.load(resource_yml) expect(@kubeapi).to receive(:ensure_namespace).with("ns1") expect(@kubeapi).to receive(:get_resource).with("configmaps", "group1", "ns1").and_raise(Kubeclient::ResourceNotFoundError.new(1, "", 2)) + expect(@kubeapi).to receive(:set_managed) expect(@kubeapi).to_not receive(:under_management?) expect(@kubeapi).to receive(:apply_resource).with(parsed_yml) etl.kube_apply(parsed_yml) @@ -282,9 +289,9 @@ class ForceExit < Exception; end expect(etl.load_config.root_spec.resource_templates.size).to eq(2) expect(Project).to receive(:names).and_return(["proj1"]) - etl.load_config.root_spec.resource_templates.each_with_index do |t, i| - yml = YAML.dump({"key#{i}" => "value#{i}"}) - expect(t).to receive(:render).and_return(yml) + etl.load_config.root_spec.resource_templates.each do |name, tmpl| + yml = YAML.dump({"key_#{name}" => "value_#{name}"}) + expect(tmpl).to receive(:render).and_return(yml) expect(etl).to receive(:kube_apply).with(YAML.safe_load(yml)) end @@ -292,10 +299,10 @@ class ForceExit < Exception; end end it "skips empty templates" do - etl.load_config.root_spec.resource_templates = [Template.new("\n\n \n")] + etl.load_config.root_spec.resource_templates = {"name1" => Template.new("\n\n \n")} expect(Project).to receive(:names).and_return(["proj1"]) - tmpl = etl.load_config.root_spec.resource_templates.first + tmpl = etl.load_config.root_spec.resource_templates.values.first expect(tmpl).to receive(:render).and_call_original expect(etl).to_not receive(:kube_apply) @@ -343,7 +350,7 @@ class ForceExit < Exception; end expect(Project).to receive(:names).and_return(["proj1", "proj2", "proj3"]) allow(etl).to receive(:kube_apply) - expect(etl.load_config.root_spec.resource_templates.first).to receive(:render) do |*args, **kwargs| + expect(etl.load_config.root_spec.resource_templates.values.first).to receive(:render) do |*args, **kwargs| expect(kwargs[:project]).to eq("proj1") expect(kwargs[:project_heirarchy]).to eq({"proj1"=>{"proj2"=>{}}}) expect(kwargs[:parameter_origins]).to eq({"param1"=>"proj1 (proj2)"}) @@ -371,11 +378,11 @@ class ForceExit < Exception; end end - it "renders templates with context" do + it "renders templates with variables" do expect(Project).to receive(:names).and_return(["proj1"]) allow(etl).to receive(:kube_apply) - expect(etl.load_config.root_spec.resource_templates.first).to receive(:render) do |*args, **kwargs| + expect(etl.load_config.root_spec.resource_templates.values.first).to receive(:render) do |*args, **kwargs| expect(kwargs[:project]).to eq("proj1") expect(kwargs[:project_heirarchy]).to eq(Project.all["proj1"].heirarchy) expect(kwargs[:debug]).to eq(etl.logger.debug?) @@ -383,6 +390,7 @@ class ForceExit < Exception; end expect(kwargs[:parameter_origins]).to eq({"param1"=>"proj1"}) expect(kwargs[:secrets]).to eq({"param2"=>"value2"}) expect(kwargs[:secret_origins]).to eq({"param2"=>"proj1"}) + expect(kwargs[:context]).to match(hash_including(:resource_name, :resource_namespace)) "" end diff --git a/spec/kubetruth/kubeapi_spec.rb b/spec/kubetruth/kubeapi_spec.rb index 2ec0cc4..7334610 100644 --- a/spec/kubetruth/kubeapi_spec.rb +++ b/spec/kubetruth/kubeapi_spec.rb @@ -186,19 +186,7 @@ def apiserver; "https://127.0.0.1"; end describe "apply_resource" do - it "creates a resource using client namespace" do - expect { kubeapi.get_resource("configmaps", @spec_name) }.to raise_error(Kubeclient::ResourceNotFoundError) - - resource = Kubeclient::Resource.new(apiVersion: "v1", kind: "ConfigMap", metadata: {name: @spec_name}, data: {bar: "baz"}) - kubeapi.apply_resource(resource) - - fetched_resource = kubeapi.get_resource("configmaps", @spec_name) - expect(fetched_resource.metadata.namespace).to eq(kubeapi.namespace) - expect(fetched_resource.metadata.name).to eq(@spec_name) - expect(fetched_resource.data.to_h).to eq({bar: "baz"}) - end - - it "creates a resource with supplied namespace" do + it "creates a resource" do kapi = described_class.new(namespace: "#{namespace}-arns", token: token, api_url: apiserver) kapi.ensure_namespace ns = kapi.namespace @@ -216,7 +204,7 @@ def apiserver; "https://127.0.0.1"; end it "creates a resource from hash" do expect { kubeapi.get_resource("configmaps", @spec_name) }.to raise_error(Kubeclient::ResourceNotFoundError) - resource = { apiVersion: "v1", kind: "ConfigMap", metadata: { name: @spec_name }, data: { bar: "baz" } } + resource = { apiVersion: "v1", kind: "ConfigMap", metadata: { namespace: kubeapi.namespace, name: @spec_name }, data: { bar: "baz" } } kubeapi.apply_resource(resource) fetched_resource = kubeapi.get_resource("configmaps", @spec_name) @@ -225,21 +213,10 @@ def apiserver; "https://127.0.0.1"; end expect(fetched_resource.data.to_h).to eq({bar: "baz"}) end - it "sets up management when creating a resource" do - expect { kubeapi.get_resource("configmaps", @spec_name) }.to raise_error(Kubeclient::ResourceNotFoundError) - - resource = Kubeclient::Resource.new(apiVersion: "v1", kind: "ConfigMap", metadata: {name: @spec_name}, data: {bar: "baz"}) - kubeapi.apply_resource(resource) - - fetched_resource = kubeapi.get_resource("configmaps", @spec_name) - expect(fetched_resource.metadata.name).to eq(@spec_name) - expect(fetched_resource.metadata.labels.to_h).to match(hash_including(KubeApi::MANAGED_LABEL_KEY.to_sym => KubeApi::MANAGED_LABEL_VALUE)) - end - it "creates other types of resources" do expect { kubeapi.get_resource("secrets", @spec_name) }.to raise_error(Kubeclient::ResourceNotFoundError) - resource = Kubeclient::Resource.new(apiVersion: "v1", kind: "Secret", metadata: {name: @spec_name}, data: {bar: Base64.strict_encode64("baz")}) + resource = Kubeclient::Resource.new(apiVersion: "v1", kind: "Secret", metadata: {namespace: kubeapi.namespace, name: @spec_name}, data: {bar: Base64.strict_encode64("baz")}) kubeapi.apply_resource(resource) fetched_resource = kubeapi.get_resource("secrets", @spec_name) diff --git a/spec/kubetruth/template_spec.rb b/spec/kubetruth/template_spec.rb index 9d3d34f..efa5adf 100644 --- a/spec/kubetruth/template_spec.rb +++ b/spec/kubetruth/template_spec.rb @@ -158,6 +158,53 @@ module Kubetruth end + describe Kubetruth::Template::TemplateHashDrop do + + it "can be inspected" do + drop = described_class.new({}) + expect(drop.inspect).to eq("{:source=>{}, :parsed=>{}}") + end + + it "fails for missing key" do + drop = described_class.new({}) + top = Template.new("{{ctx.badkey}}") + expect { top.render(ctx: drop) }.to raise_error(Kubetruth::Template::Error, /undefined method badkey/) + end + + it "only parses template on first use" do + drop = described_class.new({tmpl: "{% if true %}hi{% endif %}"}) + hash = drop.instance_variable_get(:@parsed) + expect(hash["tmpl"]).to be_nil + drop.liquid_method_missing("tmpl") + expect(hash["tmpl"]).to be_an_instance_of(Template) + end + + it "runs nested template" do + drop = described_class.new("tmpl" => "{% if true %}hi{% endif %}") + top = Template.new("{{ctx.tmpl}}") + expect(top.render(ctx: drop)).to eq("hi") + end + + it "allows symbols for keys" do + drop = described_class.new(tmpl: "{% if true %}hi{% endif %}") + top = Template.new("{{ctx.tmpl}}") + expect(top.render(ctx: drop)).to eq("hi") + end + + it "nested template can reference top level vars" do + drop = described_class.new(tmpl: "{{hum}}") + top = Template.new("{{ctx.tmpl}}") + expect(top.render(ctx: drop, hum: "foo")).to eq("foo") + end + + it "nested template can set top level vars" do + drop = described_class.new(tmpl: '{% assign foo = "bar" %}') + top = Template.new("{{ctx.tmpl}}{{foo}}") + expect(top.render(ctx: drop)).to eq("bar") + end + + end + describe "regexp match" do it "sets matchdata to nil for missing matches" do @@ -196,6 +243,13 @@ module Kubetruth expect { described_class.new("{{foo | nofilter}}").render(foo: "bar") }.to raise_error(Kubetruth::Template::Error) end + it "does procs" do + top = Template.new("{{lambda}}") + i = 3 + expect(top.render(lambda: ->() { i += 2 } )).to eq("5") + expect(top.render(lambda: ->() { i += 2 } )).to eq("7") + end + end end