Skip to content

Commit

Permalink
Merge pull request #831 from Shopify/select-any
Browse files Browse the repository at this point in the history
Add option select-any for deployTask and globalDeployTask
  • Loading branch information
peiranliushop committed Jun 9, 2021
2 parents 49e362d + af83245 commit 41bc7e5
Show file tree
Hide file tree
Showing 12 changed files with 165 additions and 3 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
## next

## 2.1.11

*Enhancements*

- Add a new option `--selector-as-filter` to command `krane deploy` and `krane global-deploy` [#831](https://github.com/Shopify/krane/pull/831)

## 2.1.10

*Bug Fixes*
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ Refer to `krane help` for the authoritative set of options.
- `--global-timeout=duration`: Raise a timeout error if it takes longer than _duration_ for any
resource to deploy.
- `--selector`: Instructs krane to only prune resources which match the specified label selector, such as `environment=staging`. If you use this option, all resource templates must specify matching labels. See [Sharing a namespace](#sharing-a-namespace) below.
- `--selector-as-filter`: Instructs krane to only deploy resources that are filtered by the specified labels in `--selector`. The deploy will not fail if not all resources match the labels. This is useful if you only want to deploy a subset of resources within a given YAML file. See [Sharing a namespace](#sharing-a-namespace) below.
- `--no-verify-result`: Skip verification that workloads correctly deployed.
- `--protected-namespaces=default kube-system kube-public`: Fail validation if a deploy is targeted at a protected namespace.
- `--verbose-log-prefix`: Add [context][namespace] to the log prefix
Expand All @@ -132,6 +133,8 @@ If you need to, you may specify `--no-prune` to disable all pruning behaviour, b

If you need to share a namespace with resources which are managed by other tools or indeed other krane deployments, you can supply the `--selector` option, such that only resources with labels matching the selector are considered for pruning.

If you need to share a namespace with different set of resources using the same YAML file, you can supply the `--selector` and `--selector-as-filter` options, such that only the resources that match with the labels will be deployed. In each run of deploy, you can use different labels in `--selector` to deploy a different set of resources. Only the deployed resources in each run are considered for pruning.

### Using templates

All templates must be YAML formatted.
Expand Down Expand Up @@ -441,6 +444,7 @@ Refer to `krane global-deploy help` for the authoritative set of options.
- `--filenames / -f [PATHS]`: Accepts a list of directories and/or filenames to specify the set of directories/files that will be deployed. Use `-` to specify STDIN.
- `--no-prune`: Skips pruning of resources that are no longer in your Kubernetes template set. Not recommended, as it allows your namespace to accumulate cruft that is not reflected in your deploy directory.
- `--selector`: Instructs krane to only prune resources which match the specified label selector, such as `environment=staging`. By using this option, all resource templates must specify matching labels. See [Sharing a namespace](#sharing-a-namespace) below.
- `--selector-as-filter`: Instructs krane to only deploy resources that are filtered by the specified labels in `--selector`. The deploy will not fail if not all resources match the labels. This is useful if you only want to deploy a subset of resources within a given YAML file. See [Sharing a namespace](#sharing-a-namespace) below.
- `--global-timeout=duration`: Raise a timeout error if it takes longer than _duration_ for any
resource to deploy.
- `--no-verify-result`: Skip verification that resources correctly deployed.
Expand Down
10 changes: 10 additions & 0 deletions lib/krane/cli/deploy_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ class DeployCommand
default: true },
"selector" => { type: :string, banner: "'label=value'",
desc: "Select workloads by selector(s)" },
"selector-as-filter" => { type: :boolean,
desc: "Use --selector as a label filter to deploy only a subset "\
"of the provided resources",
default: false },
"verbose-log-prefix" => { type: :boolean, desc: "Add [context][namespace] to the log prefix",
default: false },
"verify-result" => { type: :boolean, default: true,
Expand All @@ -37,6 +41,11 @@ def self.from_options(namespace, context, options)
require 'krane/label_selector'

selector = ::Krane::LabelSelector.parse(options[:selector]) if options[:selector]
selector_as_filter = options['selector-as-filter']

if selector_as_filter && !selector
raise(Thor::RequiredArgumentMissingError, '--selector must be set when --selector-as-filter is set')
end

logger = ::Krane::FormattedLogger.build(namespace, context,
verbose_prefix: options['verbose-log-prefix'])
Expand All @@ -60,6 +69,7 @@ def self.from_options(namespace, context, options)
logger: logger,
global_timeout: ::Krane::DurationParser.new(options["global-timeout"]).parse!.to_i,
selector: selector,
selector_as_filter: selector_as_filter,
protected_namespaces: protected_namespaces,
)

Expand Down
10 changes: 10 additions & 0 deletions lib/krane/cli/global_deploy_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ class GlobalDeployCommand
desc: "Verify workloads correctly deployed" },
"selector" => { type: :string, banner: "'label=value'", required: true,
desc: "Select workloads owned by selector(s)" },
"selector-as-filter" => { type: :boolean,
desc: "Use --selector as a label filter to deploy only a subset "\
"of the provided resources",
default: false },
"prune" => { type: :boolean, desc: "Enable deletion of resources that match"\
" the provided selector and do not appear in the provided templates",
default: true },
Expand All @@ -28,6 +32,11 @@ def self.from_options(context, options)
require 'krane/duration_parser'

selector = ::Krane::LabelSelector.parse(options[:selector])
selector_as_filter = options['selector-as-filter']

if selector_as_filter && !selector
raise(Thor::RequiredArgumentMissingError, '--selector must be set when --selector-as-filter is set')
end

filenames = options[:filenames].dup
filenames << "-" if options[:stdin]
Expand All @@ -41,6 +50,7 @@ def self.from_options(context, options)
filenames: paths,
global_timeout: ::Krane::DurationParser.new(options["global-timeout"]).parse!.to_i,
selector: selector,
selector_as_filter: selector_as_filter,
)

deploy.run!(
Expand Down
6 changes: 5 additions & 1 deletion lib/krane/deploy_task.rb
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,13 @@ def server_version
# @param bindings [Hash] Bindings parsed by Krane::BindingsParser
# @param global_timeout [Integer] Timeout in seconds
# @param selector [Hash] Selector(s) parsed by Krane::LabelSelector
# @param selector_as_filter [Boolean] Allow selecting a subset of Kubernetes resource templates to deploy
# @param filenames [Array<String>] An array of filenames and/or directories containing templates (*required*)
# @param protected_namespaces [Array<String>] Array of protected Kubernetes namespaces (defaults
# to Krane::DeployTask::PROTECTED_NAMESPACES)
# @param render_erb [Boolean] Enable ERB rendering
def initialize(namespace:, context:, current_sha: nil, logger: nil, kubectl_instance: nil, bindings: {},
global_timeout: nil, selector: nil, filenames: [], protected_namespaces: nil,
global_timeout: nil, selector: nil, selector_as_filter: false, filenames: [], protected_namespaces: nil,
render_erb: false, kubeconfig: nil)
@logger = logger || Krane::FormattedLogger.build(namespace, context)
@template_sets = TemplateSets.from_dirs_and_files(paths: filenames, logger: @logger, render_erb: render_erb)
Expand All @@ -118,6 +119,7 @@ def initialize(namespace:, context:, current_sha: nil, logger: nil, kubectl_inst
@kubectl = kubectl_instance
@global_timeout = global_timeout
@selector = selector
@selector_as_filter = selector_as_filter
@protected_namespaces = protected_namespaces || PROTECTED_NAMESPACES
@render_erb = render_erb
end
Expand Down Expand Up @@ -273,6 +275,7 @@ def validate_configuration(prune:)

confirm_ejson_keys_not_prunable if prune
@logger.info("Using resource selector #{@selector}") if @selector
@logger.info("Only deploying resources filtered by labels in selector") if @selector && @selector_as_filter
@namespace_tags |= tags_from_namespace_labels
@logger.info("All required parameters and files are present")
end
Expand All @@ -295,6 +298,7 @@ def validate_resources(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
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))
end
Expand Down
6 changes: 5 additions & 1 deletion lib/krane/global_deploy_task.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,18 @@ class GlobalDeployTask
# @param context [String] Kubernetes context (*required*)
# @param global_timeout [Integer] Timeout in seconds
# @param selector [Hash] Selector(s) parsed by Krane::LabelSelector (*required*)
# @param selector_as_filter [Boolean] Allow selecting a subset of Kubernetes resource templates to deploy
# @param filenames [Array<String>] An array of filenames and/or directories containing templates (*required*)
def initialize(context:, global_timeout: nil, selector: nil, filenames: [], logger: nil, kubeconfig: nil)
def initialize(context:, global_timeout: nil, selector: nil, selector_as_filter: false,
filenames: [], logger: nil, kubeconfig: nil)
template_paths = filenames.map { |path| File.expand_path(path) }

@task_config = TaskConfig.new(context, nil, logger, kubeconfig)
@template_sets = TemplateSets.from_dirs_and_files(paths: template_paths,
logger: @task_config.logger, render_erb: false)
@global_timeout = global_timeout
@selector = selector
@selector_as_filter = selector_as_filter
end

# Runs the task, returning a boolean representing success or failure
Expand Down Expand Up @@ -130,6 +133,7 @@ def validate_configuration
def validate_resources(resources)
validate_globals(resources)

resources.select! { |r| r.selected?(@selector) } if @selector_as_filter
Concurrency.split_across_threads(resources) do |r|
r.validate_definition(kubectl: @kubectl, selector: @selector)
end
Expand Down
4 changes: 4 additions & 0 deletions lib/krane/kubernetes_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,10 @@ def global?
@global || self.class::GLOBAL
end

def selected?(selector)
selector.nil? || selector.to_h <= labels
end

private

def validate_timeout_annotation
Expand Down
23 changes: 23 additions & 0 deletions test/exe/deploy_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,28 @@ def test_deploy_fails_without_filename
end
end

def test_deploy_fails_with_selector_as_filter_but_without_selector
selector = Krane::LabelSelector.new('key' => 'value')
Krane::LabelSelector.expects(:parse).returns(selector)
set_krane_deploy_expectations(new_args: {
filenames: ['/my/file/path'],
selector: selector,
selector_as_filter: true,
})
flags = '-f /my/file/path --selector key:value --selector-as-filter'
krane_deploy!(flags: flags)

flags = '-f /my/file/path --selector-as-filter'
krane = Krane::CLI::Krane.new(
[deploy_task_config.namespace, deploy_task_config.context],
flags.split
)
assert_raises_message(Thor::RequiredArgumentMissingError,
"--selector must be set when --selector-as-filter is set") do
krane.invoke("deploy")
end
end

def test_stdin_flag_deduped_if_specified_multiple_times
Dir.mktmpdir do |tmp_path|
$stdin.expects("read").returns("").times(2)
Expand Down Expand Up @@ -140,6 +162,7 @@ def default_options(new_args = {}, run_args = {})
logger: logger,
global_timeout: 300,
selector: nil,
selector_as_filter: false,
protected_namespaces: ["default", "kube-system", "kube-public"],
}.merge(new_args),
run_args: {
Expand Down
23 changes: 23 additions & 0 deletions test/exe/global_deploy_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,28 @@ def test_deploy_fails_without_filename
end
end

def test_deploy_fails_selector_required
selector = Krane::LabelSelector.new('key' => 'value')
Krane::LabelSelector.expects(:parse).returns(selector)
set_krane_global_deploy_expectations!(new_args: {
filenames: ['/my/file/path'],
selector: "key=value",
selector_as_filter: true,
})
flags = '-f /my/file/path --selector key:value --selector-as-filter'
krane_global_deploy!(flags: flags)

flags = '-f /my/file/path --selector-as-filter'
krane = Krane::CLI::Krane.new(
[task_config.context],
flags.split
)
assert_raises_message(Thor::RequiredArgumentMissingError,
"No value provided for required options '--selector'") do
krane.invoke("global_deploy")
end
end

def test_deploy_parses_selector
selector = 'name=web'
set_krane_global_deploy_expectations!(new_args: { selector: selector })
Expand Down Expand Up @@ -120,6 +142,7 @@ def default_options(new_args = {}, run_args = {})
filenames: ['/tmp'],
global_timeout: 300,
selector: 'name=web',
selector_as_filter: false,
}.merge(new_args),
run_args: {
verify_result: true,
Expand Down
38 changes: 38 additions & 0 deletions test/fixtures/slow-cloud/web-deploy-3.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: web3
labels:
name: web3
branch: staging
app: slow-cloud
annotations:
shipit.shopify.io/restart: "true"
krane.shopify.io/required-rollout: maxUnavailable
spec:
replicas: 2
selector:
matchLabels:
name: web3
app: slow-cloud
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 0
maxUnavailable: 1
template:
metadata:
labels:
name: web3
app: slow-cloud
sha: deploy3
spec:
terminationGracePeriodSeconds: 0
containers:
- name: app
image: busybox
imagePullPolicy: IfNotPresent
command: ["tail", "-f", "/dev/null"]
ports:
- containerPort: 80
name: http
4 changes: 3 additions & 1 deletion test/helpers/fixture_deploy_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ def deploy_raw_fixtures(set, wait: true, bindings: {}, subset: nil, render_erb:
end

def deploy_dirs_without_profiling(dirs, wait: true, prune: true, bindings: {},
sha: "k#{SecureRandom.hex(6)}", kubectl_instance: nil, global_timeout: nil, selector: nil,
sha: "k#{SecureRandom.hex(6)}", kubectl_instance: nil, global_timeout: nil,
selector: nil, selector_as_filter: false,
protected_namespaces: nil, render_erb: false)
kubectl_instance = build_kubectl

Expand All @@ -103,6 +104,7 @@ def deploy_dirs_without_profiling(dirs, wait: true, prune: true, bindings: {},
bindings: bindings,
global_timeout: global_timeout,
selector: selector,
selector_as_filter: selector_as_filter,
protected_namespaces: protected_namespaces,
render_erb: render_erb
)
Expand Down
34 changes: 34 additions & 0 deletions test/integration/krane_deploy_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,40 @@ def test_selector
assert_equal("master", deployments.first.metadata.labels.branch)
end

def test_selector_as_filter
# Deploy only the resource matching the selector without validation error
assert_deploy_success(deploy_fixtures("slow-cloud", subset: ['web-deploy-1.yml', 'web-deploy-3.yml'],
selector: Krane::LabelSelector.parse("branch=master"),
selector_as_filter: true))
assert_logs_match_all([
"Using resource selector branch=master",
"Only deploying resources filtered by labels in selector",
], in_order: true)
# Ensure only the selected resource is deployed
deployments = apps_v1_kubeclient.get_deployments(namespace: @namespace)
assert_equal(1, deployments.size)
assert_equal("master", deployments.first.metadata.labels.branch)

# Deploy another resource with a different selector
assert_deploy_success(deploy_fixtures("slow-cloud", subset: ['web-deploy-1.yml', 'web-deploy-3.yml'],
selector: Krane::LabelSelector.parse("branch=staging"),
selector_as_filter: true))
assert_logs_match_all([
"Using resource selector branch=staging",
"Only deploying resources filtered by labels in selector",
], in_order: true)
# Ensure the not selected resource is not pruned
deployments = apps_v1_kubeclient.get_deployments(namespace: @namespace)
assert_equal(2, deployments.size)
deployments = apps_v1_kubeclient.get_deployments(namespace: @namespace, label_selector: "branch=master")
assert_equal(1, deployments.size)
assert_equal("master", deployments.first.metadata.labels.branch)
# Ensure the selected resource is deployed
deployments = apps_v1_kubeclient.get_deployments(namespace: @namespace, label_selector: "branch=staging")
assert_equal(1, deployments.size)
assert_equal("staging", deployments.first.metadata.labels.branch)
end

def test_mismatched_selector
assert_deploy_failure(deploy_fixtures("slow-cloud", subset: %w(web-deploy-1.yml),
selector: Krane::LabelSelector.parse("branch=staging")))
Expand Down

0 comments on commit 41bc7e5

Please sign in to comment.