diff options
author | Lamont Granquist <454857+lamont-granquist@users.noreply.github.com> | 2022-03-29 13:04:03 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-03-29 13:04:03 -0700 |
commit | 2a33d348eedad45e8416a7487eb13fac337bdcfc (patch) | |
tree | e5ae17f909bf39872c161ac08f50ef34d3274490 | |
parent | 20db13b428afd1975e4049ba7c8f5ee1645217bf (diff) | |
parent | ff68fd07a9fbcd5a7de89b30d3669819c4af8ed7 (diff) | |
download | chef-2a33d348eedad45e8416a7487eb13fac337bdcfc.tar.gz |
Merge pull request #12140 from jasonwbarnett/feature/allow-setting-default-secret-service
-rw-r--r-- | lib/chef/dsl/secret.rb | 114 | ||||
-rw-r--r-- | lib/chef/run_context.rb | 16 | ||||
-rw-r--r-- | spec/unit/dsl/secret_spec.rb | 150 | ||||
-rw-r--r-- | spec/unit/run_context_spec.rb | 16 |
4 files changed, 272 insertions, 24 deletions
diff --git a/lib/chef/dsl/secret.rb b/lib/chef/dsl/secret.rb index 9d616ff35f..2377ae36fe 100644 --- a/lib/chef/dsl/secret.rb +++ b/lib/chef/dsl/secret.rb @@ -21,6 +21,118 @@ class Chef module DSL module Secret + # + # This allows you to set the default secret service that is used when + # fetching secrets. + # + # @example + # + # default_secret_service :hashi_vault + # val1 = secret(name: "test1", config: { region: "us-west-1" }) + # + # @example + # + # default_secret_service #=> nil + # default_secret_service :hashi_vault + # default_secret_service #=> :hashi_vault + # + # @param [Symbol] service default secret service to use when fetching secrets + # @return [Symbol, nil] default secret service to use when fetching secrets + # + def default_secret_service(service = nil) + return run_context.default_secret_service if service.nil? + raise Chef::Exceptions::Secret::InvalidFetcherService.new("Unsupported secret service: #{service.inspect}", Chef::SecretFetcher::SECRET_FETCHERS) unless Chef::SecretFetcher::SECRET_FETCHERS.include?(service) + + run_context.default_secret_service = service + end + + # + # This allows you to set the secret service for the scope of the block + # passed into this method. + # + # @example + # + # with_secret_service :hashi_vault do + # val1 = secret(name: "test1", config: { region: "us-west-1" }) + # val2 = secret(name: "test2", config: { region: "us-west-1" }) + # end + # + # @example Combine with #with_secret_config + # + # with_secret_service :hashi_vault do + # with_secret_config region: "us-west-1" do + # val1 = secret(name: "test1") + # val2 = secret(name: "test2") + # end + # end + # + # @param [Symbol] service The default secret service to use when fetching secrets + # + def with_secret_service(service) + raise ArgumentError, "You must pass a block to #with_secret_service" unless block_given? + + begin + old_service = default_secret_service + # Use "public" API for input validation + default_secret_service(service) + yield + ensure + # Use "private" API so we can set back to nil + run_context.default_secret_service = old_service + end + end + + # + # This allows you to set the default secret config that is used when + # fetching secrets. + # + # @example + # + # default_secret_config region: "us-west-1" + # val1 = secret(name: "test1", service: :hashi_vault) + # + # @example + # + # default_secret_config #=> {} + # default_secret_service region: "us-west-1" + # default_secret_service #=> { region: "us-west-1" } + # + # @param [Hash<Symbol,Object>] config The default configuration options to apply when fetching secrets + # @return [Hash<Symbol,Object>] + # + def default_secret_config(**config) + return run_context.default_secret_config if config.empty? + + run_context.default_secret_config = config + end + + # + # This allows you to set the secret config for the scope of the block + # passed into this method. + # + # @example + # + # with_secret_config region: "us-west-1" do + # val1 = secret(name: "test1", service: :hashi_vault) + # val2 = secret(name: "test2", service: :hashi_vault) + # end + # + # @param [Hash<Symbol,Object>] config The default configuration options to use when fetching secrets + # + def with_secret_config(**config) + raise ArgumentError, "You must pass a block to #with_secret_config" unless block_given? + + begin + old_config = default_secret_config + # Use "public" API for input validation + default_secret_config(**config) + yield + ensure + # Use "private" API so we can set back to nil + run_context.default_secret_config = old_config + end + end + # Helper method which looks up a secret using the given service and configuration, # and returns the retrieved secret value. # This DSL providers a wrapper around [Chef::SecretFetcher] @@ -49,7 +161,7 @@ class Chef # # value = secret(name: "test1", service: :aws_secrets_manager, version: "v1", config: { region: "us-west-1" }) # log "My secret is #{value}" - def secret(name: nil, version: nil, service: nil, config: {}) + def secret(name: nil, version: nil, service: default_secret_service, config: default_secret_config) Chef::Log.warn <<~EOM.gsub("\n", " ") The secrets Chef Infra language helper is currently in beta. If you have feedback or you would like to be part of the future design of this helper e-mail us at secrets_management_beta@progress.com" diff --git a/lib/chef/run_context.rb b/lib/chef/run_context.rb index 94f8a316e0..ce4d545aa4 100644 --- a/lib/chef/run_context.rb +++ b/lib/chef/run_context.rb @@ -145,6 +145,16 @@ class Chef # attr_accessor :input_collection + # + # @return [Symbol, nil] + # + attr_accessor :default_secret_service + + # + # @return [Hash<Symbol,Object>] + # + attr_accessor :default_secret_config + # Pointer back to the Chef::Runner that created this # attr_accessor :runner @@ -222,6 +232,8 @@ class Chef @input_collection = Chef::Compliance::InputCollection.new(events) @waiver_collection = Chef::Compliance::WaiverCollection.new(events) @profile_collection = Chef::Compliance::ProfileCollection.new(events) + @default_secret_service = nil + @default_secret_config = {} initialize_child_state end @@ -693,6 +705,10 @@ class Chef cookbook_collection cookbook_collection= cookbook_compiler + default_secret_config + default_secret_config= + default_secret_service + default_secret_service= definitions events events= diff --git a/spec/unit/dsl/secret_spec.rb b/spec/unit/dsl/secret_spec.rb index 96a915c43d..9952980697 100644 --- a/spec/unit/dsl/secret_spec.rb +++ b/spec/unit/dsl/secret_spec.rb @@ -17,11 +17,14 @@ # require "spec_helper" +require "chef/exceptions" require "chef/dsl/secret" require "chef/secret_fetcher/base" + class SecretDSLTester include Chef::DSL::Secret - # Because DSL is invoked in the context of a recipe, + + # Because DSL is invoked in the context of a recipe or attribute file # we expect run_context to always be available when SecretFetcher::Base # requests it - making it safe to mock here def run_context @@ -37,35 +40,136 @@ end describe Chef::DSL::Secret do let(:dsl) { SecretDSLTester.new } - it "responds to 'secret'" do - expect(dsl.respond_to?(:secret)).to eq true + let(:run_context) { Chef::RunContext.new(Chef::Node.new, {}, Chef::EventDispatch::Dispatcher.new) } + + before do + allow(dsl).to receive(:run_context).and_return(run_context) end - it "uses SecretFetcher.for_service to find the fetcher" do - substitute_fetcher = SecretFetcherImpl.new({}, nil) - expect(Chef::SecretFetcher).to receive(:for_service).with(:example, {}, nil).and_return(substitute_fetcher) - expect(substitute_fetcher).to receive(:fetch).with("key1", nil) - dsl.secret(name: "key1", service: :example, config: {}) + %w{ + secret + default_secret_service + default_secret_config + with_secret_service + with_secret_config + }.each do |m| + it "responds to ##{m}" do + expect(dsl.respond_to?(m)).to eq true + end + end + + describe "#default_secret_service" do + let(:service) { :hashi_vault } + + it "persists the service passed in as an argument" do + expect(dsl.default_secret_service).to eq(nil) + dsl.default_secret_service(service) + expect(dsl.default_secret_service).to eq(service) + end + + it "returns run_context.default_secret_service value when no argument is given" do + run_context.default_secret_service = :my_thing + expect(dsl.default_secret_service).to eq(:my_thing) + end + + it "raises exception when service given is not valid" do + stub_const("Chef::SecretFetcher::SECRET_FETCHERS", %i{service_a service_b}) + expect { dsl.default_secret_service(:unknown_service) }.to raise_error(Chef::Exceptions::Secret::InvalidFetcherService) + end end - it "resolves a secret when using the example fetcher" do - secret_value = dsl.secret(name: "test1", service: :example, config: { "test1" => "secret value" }) - expect(secret_value).to eq "secret value" + describe "#with_secret_config" do + let(:service) { :hashi_vault } + + it "sets the service for the scope of the block only" do + expect(dsl.default_secret_service).to eq(nil) + dsl.with_secret_service(service) do + expect(dsl.default_secret_service).to eq(service) + end + expect(dsl.default_secret_service).to eq(nil) + end + + it "raises exception when block is not given" do + expect { dsl.with_secret_service(service) }.to raise_error(ArgumentError) + end end - context "when used within a resource" do - let(:run_context) { - Chef::RunContext.new(Chef::Node.new, - Chef::CookbookCollection.new(Chef::CookbookLoader.new(File.join(CHEF_SPEC_DATA, "cookbooks"))), - Chef::EventDispatch::Dispatcher.new) - } - - it "marks that resource as 'sensitive'" do - recipe = Chef::Recipe.new("secrets", "test", run_context) - recipe.zen_master "secret_test" do - peace secret(name: "test1", service: :example, config: { "test1" => true }) + describe "#default_secret_config" do + let(:config) { { my_key: "value" } } + + it "persists the config passed in as argument" do + expect(dsl.default_secret_config).to eq({}) + dsl.default_secret_config(**config) + expect(dsl.default_secret_config).to eq(config) + end + + it "returns run_context.default_secret_config value when no argument is given" do + run_context.default_secret_config = { my_thing: "that" } + expect(dsl.default_secret_config).to eq({ my_thing: "that" }) + end + end + + describe "#with_secret_config" do + let(:config) { { my_key: "value" } } + + it "sets the config for the scope of the block only" do + expect(dsl.default_secret_config).to eq({}) + dsl.with_secret_config(**config) do + expect(dsl.default_secret_config).to eq(config) end - expect(run_context.resource_collection.lookup("zen_master[secret_test]").sensitive).to eql(true) + expect(dsl.default_secret_config).to eq({}) + end + + it "raises exception when block is not given" do + expect { dsl.with_secret_config(**config) }.to raise_error(ArgumentError) + end + end + + describe "#secret" do + it "uses SecretFetcher.for_service to find the fetcher" do + substitute_fetcher = SecretFetcherImpl.new({}, nil) + expect(Chef::SecretFetcher).to receive(:for_service).with(:example, {}, run_context).and_return(substitute_fetcher) + expect(substitute_fetcher).to receive(:fetch).with("key1", nil) + dsl.secret(name: "key1", service: :example, config: {}) + end + + it "resolves a secret when using the example fetcher" do + secret_value = dsl.secret(name: "test1", service: :example, config: { "test1" => "secret value" }) + expect(secret_value).to eq "secret value" + end + + context "when used within a resource" do + let(:run_context) { + Chef::RunContext.new(Chef::Node.new, + Chef::CookbookCollection.new(Chef::CookbookLoader.new(File.join(CHEF_SPEC_DATA, "cookbooks"))), + Chef::EventDispatch::Dispatcher.new) + } + + it "marks that resource as 'sensitive'" do + recipe = Chef::Recipe.new("secrets", "test", run_context) + recipe.zen_master "secret_test" do + peace secret(name: "test1", service: :example, config: { "test1" => true }) + end + expect(run_context.resource_collection.lookup("zen_master[secret_test]").sensitive).to eql(true) + end + end + + it "passes default service to SecretFetcher.for_service" do + service = :example + dsl.default_secret_service(service) + substitute_fetcher = SecretFetcherImpl.new({}, nil) + expect(Chef::SecretFetcher).to receive(:for_service).with(service, {}, run_context).and_return(substitute_fetcher) + allow(substitute_fetcher).to receive(:fetch).with("key1", nil) + dsl.secret(name: "key1") + end + + it "passes default config to SecretFetcher.for_service" do + config = { my_config: "value" } + dsl.default_secret_config(**config) + substitute_fetcher = SecretFetcherImpl.new({}, nil) + expect(Chef::SecretFetcher).to receive(:for_service).with(:example, config, run_context).and_return(substitute_fetcher) + allow(substitute_fetcher).to receive(:fetch).with("key1", nil) + dsl.secret(name: "key1", service: :example) end end end diff --git a/spec/unit/run_context_spec.rb b/spec/unit/run_context_spec.rb index 5bd7a360fb..3ec8eeed61 100644 --- a/spec/unit/run_context_spec.rb +++ b/spec/unit/run_context_spec.rb @@ -53,6 +53,22 @@ describe Chef::RunContext do expect(run_context.node).to eq(node) end + it "responds to #default_secret_service" do + expect(run_context).to respond_to(:default_secret_service) + end + + it "responds to #default_secret_config" do + expect(run_context).to respond_to(:default_secret_config) + end + + it "#default_secret_service defaults to nil" do + expect(run_context.default_secret_service).to eq(nil) + end + + it "#default_secret_config defaults to {}" do + expect(run_context.default_secret_config).to eq({}) + end + it "loads up node[:cookbooks]" do expect(run_context.node[:cookbooks]).to eql( { |