diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fcf5d3a33..ce2eb0e80 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,64 +6,46 @@ jobs: ruby-tests: runs-on: ubuntu-latest - name: "Tests - Ruby ${{ matrix.ruby }} with k8s ${{ matrix.kubernetes_version }}" + name: "Tests - Ruby ${{ matrix.ruby }} with Kubernetes ${{ matrix.kubernetes_version }}" strategy: fail-fast: false matrix: ruby: - # Use unique Ruby versions, or GitHub gets confused when building the rest of the matrix - - '3.1.2' # With k8s 1.23 - - '3.0.3' # With k8s 1.23 - - '3.0.2' # With k8s 1.22 - - '3.0.1' # With k8s 1.21 - - '3.0' # With k8s 1.20 - - '2.7' # With k8s 1.19 + - "3.1.2" + - "3.0.4" + - "2.7.6" + kubernetes_version: + - "1.24.6" + - "1.23.12" + - "1.22.15" include: - # Match kind images with chosen version https://github.com/kubernetes-sigs/kind/releases - - ruby: '3.1.2' - kind_version: 'v0.11.1' - kubernetes_version: '1.23.0' - kind_image: 'kindest/node:v1.23.0@sha256:49824ab1727c04e56a21a5d8372a402fcd32ea51ac96a2706a12af38934f81ac' - - ruby: '3.0.3' - kind_version: 'v0.11.1' - kubernetes_version: '1.23.0' - kind_image: 'kindest/node:v1.23.0@sha256:49824ab1727c04e56a21a5d8372a402fcd32ea51ac96a2706a12af38934f81ac' - - ruby: '3.0.2' - kind_version: 'v0.11.1' - kubernetes_version: '1.22.0' - kind_image: 'kindest/node:v1.22.0@sha256:b8bda84bb3a190e6e028b1760d277454a72267a5454b57db34437c34a588d047' - - ruby: '3.0.1' - kind_version: 'v0.11.1' - kubernetes_version: '1.21.1' - kind_image: 'kindest/node:v1.21.1@sha256:69860bda5563ac81e3c0057d654b5253219618a22ec3a346306239bba8cfa1a6' - - ruby: '3.0' - kind_version: 'v0.11.1' - kubernetes_version: '1.20.7' - kind_image: 'kindest/node:v1.20.7@sha256:cbeaf907fc78ac97ce7b625e4bf0de16e3ea725daf6b04f930bd14c67c671ff9' - - ruby: '2.7' - kind_version: 'v0.11.1' - kubernetes_version: '1.19.11' - kind_image: 'kindest/node:v1.19.11@sha256:07db187ae84b4b7de440a73886f008cf903fcf5764ba8106a9fd5243d6f32729' + - kubernetes_version: "1.24.6" + kind_image: "kindest/node:v1.24.6@sha256:97e8d00bc37a7598a0b32d1fabd155a96355c49fa0d4d4790aab0f161bf31be1" + - kubernetes_version: "1.23.12" + kind_image: "kindest/node:v1.23.12@sha256:9402cf1330bbd3a0d097d2033fa489b2abe40d479cc5ef47d0b6a6960613148a" + - kubernetes_version: "1.22.15" + kind_image: "kindest/node:v1.22.15@sha256:bfd5eaae36849bfb3c1e3b9442f3da17d730718248939d9d547e86bbac5da586" steps: - uses: actions/checkout@v2 - - name: Set up Ruby ${{ matrix.ruby }} + - name: Setup Ruby ${{ matrix.ruby }} uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - - name: Setup kubectl + - name: Setup kubectl ${{ matrix.kubernetes_version }} run: | mkdir -p "${GITHUB_WORKSPACE}/bin" curl -o "${GITHUB_WORKSPACE}/bin/kubectl" -LO "https://dl.k8s.io/release/v${{ matrix.kubernetes_version }}/bin/linux/amd64/kubectl" chmod +x "${GITHUB_WORKSPACE}/bin/kubectl" echo "PATH=$GITHUB_WORKSPACE/bin:${PATH}" >> $GITHUB_ENV - - uses: engineerd/setup-kind@v0.5.0 + - name: Setup Kind v0.16.0 + uses: engineerd/setup-kind@v0.5.0 with: - version: "${{ matrix.kind_version }}" + version: v0.16.0 image: "${{ matrix.kind_image }}" - name: Run tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 99360bbff..320d16bed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ ## next +*Enhancements* + +- CI test matrix now test all Krane supported Ruby and Kubernetes versions. + Ruby: 3.1.2 , 3.0.4 , 2.7.6, Kubernetes: - 1.24.6, 1.23.12, 1.22.15. + +*Breaking Changes* +- Remove unsupported Kubernetes and Ruby versions from Krane and CI [#905](https://github.com/Shopify/krane/pull/905) + ## 2.4.9 - Fixed a Ruby 3.1 regression that caused an exception when using a `--bindings=@` argument to `krane render` [#900](https://github.com/Shopify/krane/pull/900) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5952b280d..1400ba077 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,9 +29,7 @@ behavior to [krane@shopify.com](mailto:krane@shopify.com). ## Maintainers This project is currently under the stewardship of the Production Platform group at Shopify. -The two primary maintainers are @knverey and @dturn. Approval from at least one primary maintainer is -required for all significant feature proposals and code architecture changes. In general, -two people must approve all non-trivial PRs. +In general, two people must approve all non-trivial PRs. ## What should I know before I get started? @@ -113,7 +111,7 @@ This gem uses subclasses of `KubernetesResource` to implement custom success/fai If you work for Shopify, just run `dev up`, but otherwise: -1. [Install kubectl version 1.10.0 or higher](https://kubernetes.io/docs/user-guide/prereqs/) and make sure it is in your path +1. [Install kubectl version 1.22.0 or higher](https://kubernetes.io/docs/user-guide/prereqs/) and make sure it is in your path 2. [Install minikube](https://kubernetes.io/docs/getting-started-guides/minikube/#installation) (required to run the test suite) 3. [Install any required minikube drivers](https://github.com/kubernetes/minikube/blob/master/docs/drivers.md) (on OS X, you may need the [hyperkit driver](https://github.com/kubernetes/minikube/blob/master/docs/drivers.md#hyperkit-driver) 4. Check out the repo diff --git a/README.md b/README.md index 07bfbe90a..2e987b8ef 100644 --- a/README.md +++ b/README.md @@ -74,23 +74,28 @@ If you need the ability to render dynamic values in templates before deploying, ## Prerequisites * Ruby 2.7+ -* Your cluster must be running Kubernetes v1.19.0 or higher1 +* Your cluster must be running Kubernetes v1.22.0 or higher1 -1 We run integration tests against these Kubernetes versions. You can find our -official compatibility chart below. +## Compatibility + +1 We run integration tests against these Kubernetes versions. You can find our official compatibility chart below. + +Krane provides support for official upstream supported versions [Kubernetes](https://endoflife.date/kubernetes), [Ruby](https://endoflife.date/ruby) that are part of the compatibility matrix; Nevertheless, older releases are still likely to work. | Kubernetes version | Currently Tested? | Last officially supported in gem version | |:------------------:|-------------------|:----------------------------------------:| | 1.18 | No | 2.3.7 | -| 1.19 | Yes | -- | -| 1.20 | Yes | -- | -| 1.21 | Yes | -- | +| 1.19 | No | 2.4.9 | +| 1.20 | No | 2.4.9 | +| 1.21 | No | 2.4.9 | | 1.22 | Yes | -- | | 1.23 | Yes | -- | +| 1.24 | Yes | -- | +| 1.25 | No | -- | ## Installation -1. [Install kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/#install-kubectl-binary-via-curl) (requires v1.19.0 or higher) and make sure it is available in your $PATH +1. [Install kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/#install-kubectl-binary-via-curl) (requires v1.22.0 or higher) and make sure it is available in your $PATH 2. Set up your [kubeconfig file](https://kubernetes.io/docs/tasks/access-application-cluster/authenticate-across-clusters-kubeconfig/) for access to your cluster(s). 3. `gem install krane` @@ -497,7 +502,7 @@ resource to restart. ## Prerequisites -* You've already deployed a [`PodTemplate`](https://v1-15.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.15/#podtemplate-v1-core) object with field `template` containing a `Pod` specification that does not include the `apiVersion` or `kind` parameters. An example is provided in this repo in `test/fixtures/hello-cloud/template-runner.yml`. +* You've already deployed a [`PodTemplate`](https://v1-22.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#podtemplate-v1-core) object with field `template` containing a `Pod` specification that does not include the `apiVersion` or `kind` parameters. An example is provided in this repo in `test/fixtures/hello-cloud/template-runner.yml`. * The `Pod` specification in that template has a container named `task-runner`. Based on this specification `krane run` will create a new pod with the entrypoint of the `task-runner ` container overridden with the supplied arguments. diff --git a/bin/ci b/bin/ci index 68773d77b..59dbf6a2b 100755 --- a/bin/ci +++ b/bin/ci @@ -16,5 +16,5 @@ docker run --rm \ -e VERBOSE=1 \ -e PARALLELISM=$PARALLELISM \ -w /usr/src/app \ - ruby:"${RUBY_VERSION:-2.6.6}" \ + ruby:"${RUBY_VERSION:-2.7.6}" \ bin/test diff --git a/bin/setup b/bin/setup index 680eac535..d115ea333 100755 --- a/bin/setup +++ b/bin/setup @@ -9,8 +9,8 @@ if [ ! -x "$(which minikube)" ]; then fi if [ ! -x "$(which kubectl)" ]; then - echo -e "\n\033[0;33mPlease install kubectl version 1.15.0 or higher:\nhttps://kubernetes.io/docs/user-guide/prereqs/\033[0m" + echo -e "\n\033[0;33mPlease install kubectl version 1.22.0 or higher:\nhttps://kubernetes.io/docs/user-guide/prereqs/\033[0m" else KUBECTL_VERSION=$(kubectl version --short --client | grep -oe "v[[:digit:]\.]\+") - echo -e "\n\033[0;32mKubectl version $KUBECTL_VERSION is already installed. This gem requires version v1.15.0 or greater.\033[0m" + echo -e "\n\033[0;32mKubectl version $KUBECTL_VERSION is already installed. This gem requires version v1.22.0 or greater.\033[0m" fi diff --git a/dev.yml b/dev.yml index 00bc42c27..000948fd7 100644 --- a/dev.yml +++ b/dev.yml @@ -1,12 +1,12 @@ --- name: krane up: - - ruby: '3.0.3' # Matches gemspec + - ruby: '3.1.2' # Matches gemspec - bundler - podman - kind: name: krane - image: kindest/node:v1.22.0@sha256:b8bda84bb3a190e6e028b1760d277454a72267a5454b57db34437c34a588d047 + image: kindest/node:v1.24.6@sha256:97e8d00bc37a7598a0b32d1fabd155a96355c49fa0d4d4790aab0f161bf31be1 commands: test: run: bin/test diff --git a/krane.gemspec b/krane.gemspec index 24b9c07b9..b65e2b7f6 100644 --- a/krane.gemspec +++ b/krane.gemspec @@ -25,7 +25,7 @@ Gem::Specification.new do |spec| spec.metadata['allowed_push_host'] = "https://rubygems.org" - spec.required_ruby_version = '>= 2.6.0' + spec.required_ruby_version = '>= 2.7.6' spec.add_dependency("activesupport", ">= 5.0") spec.add_dependency("kubeclient", "~> 4.9") spec.add_dependency("googleauth", "~> 1.2") diff --git a/lib/krane/cluster_resource_discovery.rb b/lib/krane/cluster_resource_discovery.rb index 00c2662d5..f23eb96ab 100644 --- a/lib/krane/cluster_resource_discovery.rb +++ b/lib/krane/cluster_resource_discovery.rb @@ -37,19 +37,6 @@ def fetch_resources(namespaced: false) end.compact.uniq { |r| "#{r['apigroup']}/#{r['kind']}" } end - def fetch_mutating_webhook_configurations - command = %w(get mutatingwebhookconfigurations) - raw_json, err, st = kubectl.run(*command, output: "json", attempts: 5, use_namespace: false) - if st.success? - JSON.parse(raw_json)["items"].map do |definition| - Krane::MutatingWebhookConfiguration.new(namespace: namespace, context: context, logger: logger, - definition: definition, statsd_tags: @namespace_tags) - end - else - raise FatalKubeAPIError, "Error retrieving mutatingwebhookconfigurations: #{err}" - end - end - private # During discovery, the api paths may not actually be at the root, so we must programatically find it. diff --git a/lib/krane/common.rb b/lib/krane/common.rb index 44901e8ff..55de017fe 100644 --- a/lib/krane/common.rb +++ b/lib/krane/common.rb @@ -20,5 +20,5 @@ require 'krane/task_config_validator' module Krane - MIN_KUBE_VERSION = '1.15.0' + MIN_KUBE_VERSION = '1.22.0' end diff --git a/lib/krane/deploy_task.rb b/lib/krane/deploy_task.rb index 3b95e476a..738c6a754 100644 --- a/lib/krane/deploy_task.rb +++ b/lib/krane/deploy_task.rb @@ -30,7 +30,6 @@ custom_resource_definition horizontal_pod_autoscaler secret - mutating_webhook_configuration ).each do |subresource| require "krane/kubernetes_resource/#{subresource}" end @@ -285,26 +284,18 @@ def validate_configuration(prune:) end measure_method(:validate_configuration) - def partition_dry_run_resources(resources) - individuals = [] - mutating_webhook_configurations = cluster_resource_discoverer.fetch_mutating_webhook_configurations - mutating_webhook_configurations.each do |mutating_webhook_configuration| - mutating_webhook_configuration.webhooks.each do |webhook| - individuals = (individuals + resources.select { |resource| webhook.matches_resource?(resource) }).uniq - resources -= individuals - end - end - [resources, individuals] - end - def validate_resources(resources) validate_globals(resources) - batchable_resources, individuals = partition_dry_run_resources(resources.dup) - batch_dry_run_success = kubectl.server_dry_run_enabled? && validate_dry_run(batchable_resources) - individuals += batchable_resources unless batch_dry_run_success + batch_dry_run_success = validate_dry_run(resources) resources.select! { |r| r.selected?(@selector) } if @selector_as_filter + Krane::Concurrency.split_across_threads(resources) do |r| - r.validate_definition(kubectl: kubectl, selector: @selector, dry_run: individuals.include?(r)) + # No need to pass in kubectl (and do per-resource dry run apply) if batch dry run succeeded + if batch_dry_run_success + r.validate_definition(kubectl: nil, selector: @selector, dry_run: false) + else + r.validate_definition(kubectl: kubectl, selector: @selector, dry_run: true) + end end failed_resources = resources.select(&:validation_failed?) if failed_resources.present? diff --git a/lib/krane/kubectl.rb b/lib/krane/kubectl.rb index 679488136..846014c1e 100644 --- a/lib/krane/kubectl.rb +++ b/lib/krane/kubectl.rb @@ -109,11 +109,7 @@ def server_dry_run_enabled? end def dry_run_flag - if client_version >= Gem::Version.new("1.18") - "--dry-run=server" - else - "--server-dry-run" - end + "--dry-run=server" end private diff --git a/lib/krane/kubernetes_resource.rb b/lib/krane/kubernetes_resource.rb index 289876454..30bc6d118 100644 --- a/lib/krane/kubernetes_resource.rb +++ b/lib/krane/kubernetes_resource.rb @@ -226,11 +226,6 @@ def group version ? grouping : "core" end - def version - prefix, version = @definition.dig("apiVersion").split("/") - version || prefix - end - def kubectl_resource_type type end @@ -569,11 +564,7 @@ def validate_spec_with_kubectl(kubectl) # Server side dry run is only supported on apply def validate_with_server_side_dry_run(kubectl) - command = if kubectl.client_version >= Gem::Version.new('1.18') - ["apply", "-f", file_path, "--dry-run=server", "--output=name"] - else - ["apply", "-f", file_path, "--server-dry-run", "--output=name"] - end + command = ["apply", "-f", file_path, "--dry-run=server", "--output=name"] kubectl.run(*command, log_failure: false, output_is_sensitive: sensitive_template_content?, retry_whitelist: [:client_timeout, :empty, :context_deadline], attempts: 3) @@ -584,11 +575,7 @@ def validate_with_server_side_dry_run(kubectl) # If the resource template uses generateName, validating with apply will fail def validate_with_local_dry_run(kubectl) verb = deploy_method == :apply ? "apply" : "create" - command = if kubectl.client_version >= Gem::Version.new('1.18') - [verb, "-f", file_path, "--dry-run=client", "--output=name"] - else - [verb, "-f", file_path, "--dry-run", "--output=name"] - end + command = [verb, "-f", file_path, "--dry-run=client", "--output=name"] kubectl.run(*command, log_failure: false, output_is_sensitive: sensitive_template_content?, retry_whitelist: [:client_timeout, :empty, :context_deadline], attempts: 3, use_namespace: !global?) diff --git a/lib/krane/kubernetes_resource/mutating_webhook_configuration.rb b/lib/krane/kubernetes_resource/mutating_webhook_configuration.rb deleted file mode 100644 index 8d830fdf3..000000000 --- a/lib/krane/kubernetes_resource/mutating_webhook_configuration.rb +++ /dev/null @@ -1,87 +0,0 @@ -# frozen_string_literal: true - -module Krane - class MutatingWebhookConfiguration < KubernetesResource - GLOBAL = true - - class Webhook - EQUIVALENT = 'Equivalent' - EXACT = 'Exact' - - class Rule - def initialize(definition) - @definition = definition - end - - def matches_resource?(resource, accept_equivalent:) - groups.each do |group| - versions.each do |version| - resources.each do |kind| - return true if (resource.group == group || group == '*' || accept_equivalent) && - (resource.version == version || version == '*' || accept_equivalent) && - (resource.type.downcase == kind.downcase.singularize || kind == "*") - end - end - end - false - end - - def groups - @definition.dig('apiGroups') - end - - def versions - @definition.dig('apiVersions') - end - - def resources - @definition.dig('resources') - end - end - - def initialize(definition) - @definition = definition - end - - def side_effects - @definition.dig('sideEffects') - end - - def has_side_effects? - # Note: After K8s 1.22, this should ALWAYS be false. - !%w(None NoneOnDryRun).include?(side_effects) - end - - def match_policy - @definition.dig('matchPolicy') - end - - def matches_resource?(resource, skip_rule_if_side_effect_none: true) - return false if skip_rule_if_side_effect_none && !has_side_effects? - rules.any? do |rule| - rule.matches_resource?(resource, accept_equivalent: match_policy == EQUIVALENT) - end - end - - def rules - @definition.fetch('rules', []).map { |rule| Rule.new(rule) } - end - end - - def initialize(namespace:, context:, definition:, logger:, statsd_tags:) - @webhooks = (definition.dig('webhooks') || []).map { |hook| Webhook.new(hook) } - super(namespace: namespace, context: context, definition: definition, - logger: logger, statsd_tags: statsd_tags) - end - - TIMEOUT = 30.seconds - - def deploy_succeeded? - exists? - end - - def webhooks - @definition.fetch('webhooks', []).map { |webhook| Webhook.new(webhook) } - end - end -end diff --git a/test/fixtures/mutating_webhook_configurations/ingress_hook.yaml b/test/fixtures/mutating_webhook_configurations/ingress_hook.yaml deleted file mode 100644 index d04616b06..000000000 --- a/test/fixtures/mutating_webhook_configurations/ingress_hook.yaml +++ /dev/null @@ -1,31 +0,0 @@ ---- -apiVersion: admissionregistration.k8s.io/v1 -kind: MutatingWebhookConfiguration -metadata: - name: ingress-webhook-configuration -webhooks: -- admissionReviewVersions: - - v1beta1 - clientConfig: - service: - name: ingress-hook - namespace: test - path: "/ingress-hook" - port: 443 - failurePolicy: Ignore - matchPolicy: Exact - name: ingress-hook.hooks.admission.krane.com - reinvocationPolicy: Never - rules: - - apiGroups: - - networking.k8s.io - apiVersions: - - v1 - operations: - - CREATE - - UPDATE - resources: - - ingresses - scope: "*" - sideEffects: NoneOnDryRun - timeoutSeconds: 1 diff --git a/test/fixtures/mutating_webhook_configurations/secret_hook.yaml b/test/fixtures/mutating_webhook_configurations/secret_hook.yaml deleted file mode 100644 index b6f80ad2a..000000000 --- a/test/fixtures/mutating_webhook_configurations/secret_hook.yaml +++ /dev/null @@ -1,31 +0,0 @@ ---- -apiVersion: admissionregistration.k8s.io/v1 -kind: MutatingWebhookConfiguration -metadata: - name: secret-hook-webhook-configuration -webhooks: -- admissionReviewVersions: - - v1beta1 - clientConfig: - service: - name: secret-hook - namespace: test - path: "/secret-hook" - port: 443 - failurePolicy: Ignore - matchPolicy: Equivalent - name: secret-hook.hooks.admission.krane.com - reinvocationPolicy: Never - rules: - - apiGroups: - - core - apiVersions: - - v1 - operations: - - CREATE - - UPDATE - resources: - - secrets - scope: "*" - sideEffects: NoneOnDryRun - timeoutSeconds: 1 diff --git a/test/integration-serial/serial_deploy_test.rb b/test/integration-serial/serial_deploy_test.rb index d00ee4aa1..576e72b0d 100644 --- a/test/integration-serial/serial_deploy_test.rb +++ b/test/integration-serial/serial_deploy_test.rb @@ -546,87 +546,9 @@ def test_batch_dry_run_apply_success_precludes_individual_resource_dry_run_valid ], in_order: true) end - # Note: After we drop support for K8s 1.21 this test can be removed, since webhooks must be dry-run safe. - def test_resources_with_side_effect_inducing_webhooks_are_not_batched_server_side_dry_run - result = deploy_global_fixtures("mutating_webhook_configurations", subset: %(ingress_hook.yaml)) - assert_deploy_success(result) - - # Note: We have to mock `has_side_effects?`, since this won't be possible with K8s 1.22+. - Krane::MutatingWebhookConfiguration::Webhook.any_instance.stubs(:has_side_effects?).returns(true) - - Krane::ResourceDeployer.any_instance.expects(:dry_run).with do |params| - # We expect the ingress to not be included in the batch run - params.length == 3 && (params.map(&:type).sort == ["ConfigMap", "Deployment", "Service"]) - end.returns(true) - - [Krane::ConfigMap, Krane::Deployment, Krane::Service].each do |r| - r.any_instance.expects(:validate_definition).with { |params| params[:dry_run] == false } - end - Krane::Ingress.any_instance.expects(:validate_definition).with { |params| params[:dry_run] } - result = deploy_fixtures('hello-cloud', subset: %w(web.yml.erb configmap-data.yml), render_erb: true) - assert_deploy_success(result) - assert_logs_match_all([ - "Result: SUCCESS", - "Successfully deployed 4 resources", - ], in_order: true) - end - - # Note: After we drop support for K8s 1.21 this test can be removed, since webhooks must be dry-run safe. - def test_resources_with_side_effect_inducing_webhooks_with_transitive_dependency_does_not_fail_batch_running - result = deploy_global_fixtures("mutating_webhook_configurations", subset: %(secret_hook.yaml)) - assert_deploy_success(result) - - # Note: We have to mock `has_side_effects?`, since this won't be possible with K8s 1.22+. - Krane::MutatingWebhookConfiguration::Webhook.any_instance.stubs(:has_side_effects?).returns(true) - - actual_dry_runs = 0 - Krane::KubernetesResource.any_instance.expects(:validate_definition).with do |params| - actual_dry_runs += 1 if params[:dry_run] - true - end.times(5) - result = deploy_fixtures('hello-cloud', subset: %w(web.yml.erb secret.yml configmap-data.yml), - render_erb: true) do |fixtures| - container = fixtures['web.yml.erb']['Deployment'][0]['spec']['template']['spec'] - container['volumes'] = [{ - 'name' => 'secret', - 'secret' => { - 'secretName' => fixtures['secret.yml']["Secret"][0]['metadata']['name'], - }, - }] - end - assert_deploy_success(result) - assert_equal(actual_dry_runs, 1) - assert_logs_match_all([ - "Result: SUCCESS", - "Successfully deployed 5 resources", - ], in_order: true) - end - - # Note: After we drop support for K8s 1.21 this test can be removed, since webhooks must be dry-run safe. - def test_multiple_resources_with_side_effect_inducing_webhooks_are_properly_partitioned - result = deploy_global_fixtures("mutating_webhook_configurations", subset: %(secret_hook.yaml ingress_hook.yaml)) - assert_deploy_success(result) - - # Note: We have to mock `has_side_effects?`, since this won't be possible with K8s 1.22+. - Krane::MutatingWebhookConfiguration::Webhook.any_instance.stubs(:has_side_effects?).returns(true) - - Krane::KubernetesResource.any_instance.expects(:validate_definition).with { |p| p[:dry_run] }.times(2) - result = deploy_fixtures('hello-cloud', subset: %w(web.yml.erb secret.yml), render_erb: true) do |fixtures| - fixtures["web.yml.erb"] = fixtures["web.yml.erb"].keep_if { |key| key == "Ingress" } - end - assert_deploy_success(result) - end - private def rollout_conditions_annotation_key Krane::Annotation.for(Krane::CustomResourceDefinition::ROLLOUT_CONDITIONS_ANNOTATION) end - - def mutating_webhook_fixture(path) - JSON.parse(File.read(path))['items'].map do |definition| - Krane::MutatingWebhookConfiguration.new(namespace: @namespace, context: @context, logger: @logger, - definition: definition, statsd_tags: @namespace_tags) - end - end end diff --git a/test/unit/krane/kubernetes_resource/mutating_webhook_configuration_test.rb b/test/unit/krane/kubernetes_resource/mutating_webhook_configuration_test.rb deleted file mode 100644 index 183536732..000000000 --- a/test/unit/krane/kubernetes_resource/mutating_webhook_configuration_test.rb +++ /dev/null @@ -1,93 +0,0 @@ -# frozen_string_literal: true -require 'test_helper' - -class MutatingWebhookConfigurationTest < Krane::TestCase - def test_load_from_json - definition = YAML.load_file(File.join(fixture_path("mutating_webhook_configurations"), "secret_hook.yaml")) - webhook_configuration = Krane::MutatingWebhookConfiguration.new( - namespace: 'test', context: 'nope', definition: definition, - logger: @logger, statsd_tags: nil - ) - assert_equal(webhook_configuration.webhooks.length, 1) - - raw_webhook = definition.dig('webhooks', 0) - webhook = webhook_configuration.webhooks.first - assert_equal(raw_webhook.dig('matchPolicy'), webhook.match_policy) - assert_equal(raw_webhook.dig('sideEffects'), webhook.side_effects) - - assert_equal(webhook.rules.length, 1) - raw_rule = definition.dig('webhooks', 0, 'rules', 0) - rule = webhook.rules.first - assert_equal(raw_rule.dig('apiGroups'), ['core']) - assert_equal(rule.groups, ['core']) - - assert_equal(raw_rule.dig('apiVersions'), ['v1']) - assert_equal(rule.versions, ['v1']) - - assert_equal(raw_rule.dig('resources'), ['secrets']) - assert_equal(rule.resources, ['secrets']) - end - - # Note: After we drop support for K8s 1.21 this test can be removed, since webhooks must be dry-run safe. - def test_webhook_configuration_matches_when_side_effects - secret_def = YAML.load_file(File.join(fixture_path('hello-cloud'), 'secret.yml')) - secret = Krane::Secret.new(namespace: 'test', context: 'nope', definition: secret_def, - logger: @logger, statsd_tags: nil) - - definition = YAML.load_file(File.join(fixture_path("mutating_webhook_configurations"), "secret_hook.yaml")) - webhook_configuration = Krane::MutatingWebhookConfiguration.new( - namespace: 'test', context: 'nope', definition: definition, - logger: @logger, statsd_tags: nil - ) - webhook = webhook_configuration.webhooks.first - # Note: We have to mock `has_side_effects?`, since this won't be possible with K8s 1.22+. - webhook.stubs(:has_side_effects?).returns(true).at_least_once - assert(webhook.has_side_effects?) - assert(webhook.matches_resource?(secret)) - assert(webhook.matches_resource?(secret, skip_rule_if_side_effect_none: true)) - assert(webhook.matches_resource?(secret, skip_rule_if_side_effect_none: false)) - end - - # Note: After we drop support for K8s 1.21 this test can be removed, since webhooks must be dry-run safe. - def test_matches_webhook_configuration_doesnt_match_when_no_side_effects_and_flag - secret_def = YAML.load_file(File.join(fixture_path('hello-cloud'), 'secret.yml')) - secret = Krane::Secret.new(namespace: 'test', context: 'nope', definition: secret_def, - logger: @logger, statsd_tags: nil) - - definition = YAML.load_file(File.join(fixture_path("mutating_webhook_configurations"), "secret_hook.yaml")) - webhook_configuration = Krane::MutatingWebhookConfiguration.new( - namespace: 'test', context: 'nope', definition: definition, - logger: @logger, statsd_tags: nil - ) - webhook = webhook_configuration.webhooks.first - webhook.stubs(:has_side_effects?).returns(false).at_least_once - refute(webhook.matches_resource?(secret)) - refute(webhook.matches_resource?(secret, skip_rule_if_side_effect_none: true)) - assert(webhook.matches_resource?(secret, skip_rule_if_side_effect_none: false)) - end - - # Note: After we drop support for K8s 1.21 this test can be removed, since webhooks must be dry-run safe. - def test_no_match_when_policy_is_exact_and_resource_doesnt_match - secret_def = YAML.load_file(File.join(fixture_path('hello-cloud'), 'secret.yml')) - secret = Krane::Secret.new(namespace: 'test', context: 'nope', definition: secret_def, - logger: @logger, statsd_tags: nil) - - definition = YAML.load_file(File.join(fixture_path("mutating_webhook_configurations"), "secret_hook.yaml")) - webhook_configuration = Krane::MutatingWebhookConfiguration.new( - namespace: 'test', context: 'nope', definition: definition, - logger: @logger, statsd_tags: nil - ) - - webhook = webhook_configuration.webhooks.first - # Note: We have to mock `has_side_effects?`, since this won't be possible with K8s 1.22+. - webhook.stubs(:has_side_effects?).returns(true).at_least_once - assert(webhook.matches_resource?(secret)) - webhook.expects(:match_policy).returns(Krane::MutatingWebhookConfiguration::Webhook::EXACT).at_least(1) - assert(webhook.matches_resource?(secret)) - secret.expects(:group).returns('fake').once - refute(webhook.matches_resource?(secret)) - secret.unstub(:group) - secret.expects(:type).returns('fake').once - refute(webhook.matches_resource?(secret)) - end -end diff --git a/test/unit/krane/kubernetes_resource_test.rb b/test/unit/krane/kubernetes_resource_test.rb index 1f005a012..75ba7cc53 100644 --- a/test/unit/krane/kubernetes_resource_test.rb +++ b/test/unit/krane/kubernetes_resource_test.rb @@ -175,7 +175,6 @@ def test_timeout_override_upper_bound_validation def test_validate_definition_doesnt_log_raw_output_for_sensitive_resources resource = DummySensitiveResource.new - kubectl.expects(:client_version).returns(Gem::Version.new('1.20')) kubectl.expects(:run) .with('apply', '-f', "/tmp/foo/bar", "--dry-run=server", '--output=name', { @@ -191,47 +190,6 @@ def test_validate_definition_doesnt_log_raw_output_for_sensitive_resources refute_includes(resource.validation_error_msg, 'S3CR3T') end - def test_validate_definition_ignores_server_dry_run_unsupported_by_webhook_response - resource = DummySensitiveResource.new - kubectl.expects(:run) - .with('apply', '-f', anything, '--dry-run=client', '--output=name', anything) - .returns(["", "", stub(success?: true)]) - - kubectl.expects(:run) - .with('apply', '-f', anything, '--dry-run=server', '--output=name', anything) - .returns([ - "Some Raw Output", - "Error from kubectl: admission webhook some-webhook does not support dry run", - stub(success?: false), - ]) - resource.validate_definition(kubectl: kubectl) - refute(resource.validation_failed?, "Failed to ignore server dry run responses matching: - #{Krane::KubernetesResource::SERVER_DRY_RUN_DISABLED_ERROR}") - end - - def test_validate_definition_ignores_server_dry_run_unsupported_by_webhook_response_k8s_1_17 - resource = DummySensitiveResource.new - - kubectl.expects(:client_version) - .returns(Gem::Version.new('1.17')) - .at_least_once - - kubectl.expects(:run) - .with('apply', '-f', anything, '--dry-run', '--output=name', anything) - .returns(["", "", stub(success?: true)]) - - kubectl.expects(:run) - .with('apply', '-f', anything, '--server-dry-run', '--output=name', anything) - .returns([ - "Some Raw Output", - "Error from kubectl: admission webhook some-webhook does not support dry run", - stub(success?: false), - ]) - resource.validate_definition(kubectl: kubectl) - refute(resource.validation_failed?, "Failed to ignore server dry run responses matching: - #{Krane::KubernetesResource::SERVER_DRY_RUN_DISABLED_ERROR}") - end - def test_annotation_and_kubectl_error_messages_are_combined customized_resource = DummyResource.new(definition_extras: build_timeout_metadata("bad")) kubectl.expects(:run).returns([