summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJason Barnett <jason.w.barnett@gmail.com>2021-10-05 11:29:13 -0600
committerJason Barnett <jason.w.barnett@gmail.com>2022-05-06 08:55:46 -0600
commit2179c04f7ca6a72a16b106fdc146971ed0e08ad9 (patch)
treeca61e34d5c9f68ad42dc998aec5c63da2775818a
parent9d518ac61d6003f675f1860e3861c42895818632 (diff)
downloadchef-2179c04f7ca6a72a16b106fdc146971ed0e08ad9.tar.gz
Add support for default secret service
Signed-off-by: Jason Barnett <jason.w.barnett@gmail.com>
-rw-r--r--lib/chef/dsl/secret.rb114
-rw-r--r--lib/chef/run_context.rb16
-rw-r--r--spec/unit/dsl/secret_spec.rb150
-rw-r--r--spec/unit/run_context_spec.rb16
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(
{