aws2-ssm-env 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,67 @@
1
+ require 'aws-ssm-env/fetchers/factory'
2
+ require 'aws-ssm-env/naming_strategies/factory'
3
+
4
+ module AwsSsmEnv
5
+ # このgemのエントリポイントとなるクラス。メイン処理を行う。
6
+ # AWS EC2 Parameters Storeからパラメータを取得してENVに書き込む。
7
+ #
8
+ # @author Ryohei Sonoda
9
+ # @since 0.1.0
10
+ class Loader
11
+ # メイン処理。引数の詳細は AwsSsmEnv#load を参照。
12
+ def self.load(**args)
13
+ new(args).load
14
+ end
15
+
16
+ def initialize(**args)
17
+ parse_options(args)
18
+ if @logger
19
+ @logger.debug("#{self.class.name} overwrite: #{@overwrite}")
20
+ @logger.debug("#{self.class.name} fetcher: #{@fetcher}")
21
+ @logger.debug("#{self.class.name} naming_strategy: #{@naming_strategy}")
22
+ end
23
+ end
24
+
25
+ def load
26
+ @fetcher.each do |parameter|
27
+ var_name = @naming_strategy.parse_name(parameter)
28
+ @logger.debug("#{self.class.name} #{parameter.name} parameter value into ENV['#{var_name}']") if @logger
29
+ send(@applier, var_name, parameter.value)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def parse_options(**options)
36
+ @logger = options[:logger]
37
+ @fetcher = AwsSsmEnv::FetcherFactory.create_fetcher(options)
38
+ @naming_strategy = AwsSsmEnv::NamingStrategyFactory.create_naming_strategy(options)
39
+ @overwrite = overwrite?(options)
40
+ if @overwrite
41
+ @applier = :apply!
42
+ else
43
+ @applier = :apply
44
+ end
45
+ end
46
+
47
+ # overwriteフラグが指定されているかどうかを返す。
48
+ def overwrite?(overwrite: nil, **)
49
+ if overwrite.nil?
50
+ false
51
+ else
52
+ overwrite.to_s.downcase == 'true'
53
+ end
54
+ end
55
+
56
+ def apply(name, value)
57
+ if ENV[name]
58
+ return
59
+ end
60
+ apply!(name, value)
61
+ end
62
+
63
+ def apply!(name, value)
64
+ ENV[name] = value
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,19 @@
1
+ require 'aws-ssm-env/naming_strategy'
2
+
3
+ module AwsSsmEnv
4
+ # 環境変数名にパラメータ名の階層表現のbasenameを利用するようにするNamingStrategy実装クラス。
5
+ # AwsSsmEnv#load で`naming`を指定しなかった場合にはこのクラスのインスタンスが利用される。
6
+ # 例えば、`/path/to/ENV_NAME`というパラメータ名であればENV['ENV_NAME']にパラメータ値がインジェクションされる。
7
+ #
8
+ # @author Ryohei Sonoda
9
+ # @since 0.1.0
10
+ class BasenameNamingStrategy < NamingStrategy
11
+ # @see AwsSsmEnv::NamingStrategy#parse_name
12
+ #
13
+ # パラメータ名の最後の階層を変数名として返す。
14
+ # @return [String] 環境変数名
15
+ def parse_name(parameter)
16
+ File.basename(parameter.name)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,68 @@
1
+ require 'aws-ssm-env/naming_strategy'
2
+
3
+ module AwsSsmEnv
4
+ # 環境変数名を導出するためのNamingStrategyクラスを取得もしくは生成するファクトリクラス。
5
+ #
6
+ # @author Ryohei Sonoda
7
+ # @since 0.1.0
8
+ class NamingStrategyFactory
9
+ BASENAME_STRATEGY = :basename
10
+ SNAKE_CASE_STRATEGY = :snakecase
11
+
12
+ class << self
13
+ # 環境変数名を導出するためのNamingStrategyクラスを取得もしくは生成する。
14
+ #
15
+ # @param [Hash] args AwsSsmEnv#load に渡された引数がそのまま渡される。
16
+ # @option args [Symbol, AwsSsmEnv::NamingStrategy, Object] naming
17
+ # 引数の詳細は AwsSsmEnv#load の説明を参照。
18
+ def create_naming_strategy(**args)
19
+ naming_strategy = args[:naming]
20
+ if naming_strategy.nil?
21
+ return default_strategy(args)
22
+ end
23
+ case naming_strategy
24
+ when BASENAME_STRATEGY
25
+ create_basename_strategy(args)
26
+ when SNAKE_CASE_STRATEGY
27
+ create_snakecase_strategy(args)
28
+ else
29
+ unknown_naming_strategy(naming_strategy)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def default_strategy(**args)
36
+ create_basename_strategy(args)
37
+ end
38
+
39
+ def create_basename_strategy(**args)
40
+ require 'aws-ssm-env/naming_strategies/basename'
41
+ AwsSsmEnv::BasenameNamingStrategy.new(args)
42
+ end
43
+
44
+ def create_snakecase_strategy(**args)
45
+ require 'aws-ssm-env/naming_strategies/snakecase'
46
+ AwsSsmEnv::SnakeCaseNamingStrategy.new(args)
47
+ end
48
+
49
+ def unknown_naming_strategy(naming_strategy)
50
+ unless naming_strategy_instance?(naming_strategy)
51
+ raise ArgumentError, 'Possible values for :naming are either :basename, :snakecase, ' \
52
+ + '"AwsSsmEnv::NamingStrategy" implementation class, an object with "parse_name" method.'
53
+ end
54
+ naming_strategy
55
+ end
56
+
57
+ def naming_strategy_instance?(object)
58
+ if object.is_a?(AwsSsmEnv::NamingStrategy)
59
+ true
60
+ elsif object.respond_to?(:parse_name)
61
+ true
62
+ else
63
+ false
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,58 @@
1
+ require 'aws-ssm-env/naming_strategy'
2
+
3
+ module AwsSsmEnv
4
+ # パラメータ名の階層表現をスネークケースに変換した値を環境変数名とする。
5
+ # 例えば、`removed_prefix`が`/path`で`/path/to/environment_name`というパラメータ名なら
6
+ # ENV['TO_ENVIRONMENT_NAME']にパラメータ値がインジェクションされる。
7
+ #
8
+ # @author Ryohei Sonoda
9
+ # @since 0.1.0
10
+ class SnakeCaseNamingStrategy < NamingStrategy
11
+ # ここの引数は AwsSsmEnv#load の呼び出し時に渡された引数がそのまま渡される。
12
+ #
13
+ # @param [Hash] args AwsSsmEnv#load の呼び出し時に渡された引数。
14
+ # @option args [String] :removed_prefix
15
+ # パラメータ名から除去するプレフィクス。この文字列は導出される環境変数名に含まない。
16
+ # :removed_prefixが指定されておらず、:begins_with または :path が指定されていた場合はそれを利用する。 TODO: AwsSsmEnv#loadとREADMEに反映
17
+ # @option args [String, Regexp] :delimiter
18
+ # アンダースコアに変換する区切り文字。デフォルトはスラッシュ('/')。 TODO: AwsSsmEnv#loadとREADMEに反映
19
+ def initialize(**args)
20
+ @logger = args[:logger]
21
+ @delimiter = detect_delimiter(args)
22
+ removed_prefix = detect_prefix(args).sub(%r{/\z}, '')
23
+ @removed_prefix = /\A#{Regexp.escape(removed_prefix)}/
24
+ @logger.debug("#{self.class.name} removed_prefix is #{@removed_prefix}") if @logger
25
+ end
26
+
27
+ # @see AwsSsmEnv::NamingStrategy#parse_name
28
+ #
29
+ # パラメータ名からプレフィクスを除去してパス区切りをアンダースコアに変換後、大文字にして返す。
30
+ # @return [String] 環境変数名
31
+ def parse_name(parameter)
32
+ name_without_prefix = parameter.name.gsub(@removed_prefix, '')
33
+ name_without_prefix.gsub(@delimiter, '_').upcase
34
+ end
35
+
36
+ private
37
+
38
+ def detect_delimiter(**args)
39
+ if args[:delimiter].nil?
40
+ '/'
41
+ else
42
+ args[:delimiter]
43
+ end
44
+ end
45
+
46
+ def detect_prefix(**args)
47
+ if args[:removed_prefix]
48
+ args[:removed_prefix]
49
+ elsif args[:begins_with]
50
+ args[:begins_with]
51
+ elsif args[:path]
52
+ args[:path]
53
+ else
54
+ ''
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,23 @@
1
+ module AwsSsmEnv
2
+ # パラメータの値を設定する環境変数名を決定するためのStrategyクラス。
3
+ # 実装クラスを AwsSsmEnv#load の引数で`naming`パラメータとして渡すことにより
4
+ # インジェクションされる環境変数名の命名ルールを切り替えられるようにする。
5
+ #
6
+ # @abstract
7
+ # @author Ryohei Sonoda
8
+ # @since 0.1.0
9
+ class NamingStrategy
10
+ # ここの引数は AwsSsmEnv#load の呼び出し時に渡された引数がそのまま渡される。
11
+ # サブクラスでは必要に応じて使う引数をインスタンス変数に保持しておく。
12
+ #
13
+ # @param [Hash] ** AwsSsmEnv#load の呼び出し時に渡された引数。
14
+ def initialize(**); end
15
+
16
+ # パラメータから環境変数名を導出するメソッド。
17
+ # @abstract
18
+ # @return [String] 環境変数名
19
+ def parse_name(_parameter)
20
+ raise NotImplementedError, 'parse_name'
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ module AwsSsmEnv
2
+ VERSION = '0.1.1'.freeze
3
+ end
@@ -0,0 +1,158 @@
1
+ require 'spec_helper'
2
+
3
+ describe AwsSsmEnv::Fetcher do
4
+ let(:fetcher) { described_class.new(args) }
5
+ let(:args) { {} }
6
+ let(:client) { fetcher.send(:client) }
7
+ let(:ssm_client_args) {
8
+ {
9
+ access_key_id: 'access_key_id',
10
+ secret_access_key: 'secret_access_key'
11
+ }
12
+ }
13
+
14
+ describe '#initialize' do
15
+ context 'when decryption was not set' do
16
+ it 'with_decryption is true' do
17
+ expect(fetcher.send(:with_decryption)).to be_truthy
18
+ end
19
+ end
20
+
21
+ context 'when decryption is nil' do
22
+ let(:args) { { decryption: nil } }
23
+
24
+ it 'with_decryption is true' do
25
+ expect(fetcher.send(:with_decryption)).to be_truthy
26
+ end
27
+ end
28
+
29
+ context 'when decryption is truthy string' do
30
+ let(:args) { { decryption: 'TrUe' } }
31
+
32
+ it 'with_decryption is true' do
33
+ expect(fetcher.send(:with_decryption)).to be_truthy
34
+ end
35
+ end
36
+
37
+ context 'when decryption is not truthy string' do
38
+ let(:args) { { decryption: 'foo' } }
39
+
40
+ it 'with_decryption is false' do
41
+ expect(fetcher.send(:with_decryption)).to be_falsey
42
+ end
43
+ end
44
+
45
+ context 'when client was not set' do
46
+ context 'when ssm_client_args was set' do
47
+ let(:args) { { ssm_client_args: ssm_client_args } }
48
+
49
+ it 'client is initialized by ssm_client_args' do
50
+ expect(client.config[:access_key_id]).to eq('access_key_id')
51
+ expect(client.config[:secret_access_key]).to eq('secret_access_key')
52
+ end
53
+ end
54
+
55
+ context 'when ssm_client_args was not set' do
56
+ it 'client is default construction' do
57
+ expect(client.config[:access_key_id]).to be_nil
58
+ end
59
+ end
60
+ end
61
+
62
+ context 'when client was set' do
63
+ context 'when client is instance of Aws::SSM::Client' do
64
+ let(:ssm_client) { Aws::SSM::Client.new }
65
+ let(:args) { { client: ssm_client } }
66
+
67
+ it 'client is equals to args[:client]' do
68
+ expect(client).to eq(ssm_client)
69
+ end
70
+ end
71
+
72
+ context 'when client is not instance of Aws::SSM::Client' do
73
+ let(:ssm_client) { 'foo' }
74
+ let(:args) { { client: ssm_client, ssm_client_args: ssm_client_args } }
75
+
76
+ it 'client is not equals to args[:client] and client is initialized by ssm_client_args' do
77
+ expect(client).not_to eq(ssm_client)
78
+ expect(client.config[:access_key_id]).to eq('access_key_id')
79
+ expect(client.config[:secret_access_key]).to eq('secret_access_key')
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ describe '#each' do
86
+ let(:parameters) { [ Parameter.new('foo', 'foo'), Parameter.new('bar', 'bar') ] }
87
+ let(:fetcher) {
88
+ mock_class = Class.new(described_class) do
89
+ def initialize(response); @response = response; end
90
+ protected def fetch(_); @response; end
91
+ end
92
+ mock_class.new(dummy_response)
93
+ }
94
+
95
+ context 'when fetch returns empty parameters at first' do
96
+ let(:dummy_response) { AwsSsmEnv::FetchResult::EMPTY }
97
+
98
+ it 'consumer is not called' do
99
+ called = false
100
+ fetcher.each do |_|
101
+ called = true
102
+ end
103
+ expect(called).to be_falsey
104
+ end
105
+ end
106
+
107
+ context 'when fetch returns two parameters at first and empty next_token' do
108
+ let(:dummy_response) { AwsSsmEnv::FetchResult.new(parameters, nil) }
109
+
110
+ it 'consumer is called twice' do
111
+ called = 0
112
+ fetcher.each do |_|
113
+ called += 1
114
+ end
115
+ expect(called).to eq(2)
116
+ end
117
+ end
118
+
119
+ context 'when fetch returns two parameters and next_token at first, fetch returns two parameters and empty next_token at second' do
120
+ let(:fetcher) {
121
+ mock_class = Class.new(described_class) do
122
+ def initialize(parameters); @parameters = parameters; @count = 0; end
123
+ protected def fetch(_)
124
+ if @count == 0
125
+ @count = 1
126
+ AwsSsmEnv::FetchResult.new(@parameters, 'next_token')
127
+ else
128
+ AwsSsmEnv::FetchResult.new(@parameters, nil)
129
+ end
130
+ end
131
+ end
132
+ mock_class.new(parameters)
133
+ }
134
+
135
+ it 'consumer is called four times' do
136
+ called = 0
137
+ fetcher.each do |_|
138
+ called += 1
139
+ end
140
+ expect(called).to eq(4)
141
+ end
142
+ end
143
+ end
144
+
145
+ describe '#fetch' do
146
+ it 'raise error' do
147
+ expect { fetcher.send(:fetch, 'next_token') }.to raise_error(NotImplementedError)
148
+ end
149
+ end
150
+ end
151
+
152
+ describe AwsSsmEnv::FetchResult do
153
+ describe '#initialize' do
154
+ subject { described_class.new([], 'next_token') }
155
+
156
+ it { is_expected.to have_attributes(parameters: [], next_token: 'next_token') }
157
+ end
158
+ end
@@ -0,0 +1,106 @@
1
+ require 'spec_helper'
2
+ require 'aws-ssm-env/fetchers/begins_with'
3
+
4
+ describe AwsSsmEnv::BeginsWithFetcher do
5
+ let(:fetcher) { described_class.new(args) }
6
+ let(:args) { { begins_with: [ '/foo', '/bar' ] } }
7
+ let(:base_params) { fetcher.instance_variable_get(:'@base_params') }
8
+ let(:parameter_filter) { base_params[:parameter_filters][0] }
9
+
10
+ describe '#initialize' do
11
+ context 'when :begins_with was not set' do
12
+ it 'raise error' do
13
+ expect { described_class.new(begins_with: nil) }.to raise_error(ArgumentError)
14
+ end
15
+ end
16
+
17
+ context 'when :begins_with is Array' do
18
+ it 'parameter_filter[:values] is begins_with value' do
19
+ expect(parameter_filter[:values]).to eq(args[:begins_with])
20
+ end
21
+ end
22
+
23
+ context 'when :begins_with is not Array' do
24
+ let(:args) { { begins_with: '/foo' } }
25
+
26
+ it 'parameter_filter[:values] is [ begins_with value ]' do
27
+ expect(parameter_filter[:values]).to eq([ args[:begins_with] ])
28
+ end
29
+ end
30
+
31
+ context 'when :fetch_size was set and less than 50' do
32
+ let(:args) { { begins_with: '/foo', fetch_size: 49 } }
33
+
34
+ it '@base_params[:max_results] is fetch_size value' do
35
+ expect(base_params[:max_results]).to eq(49)
36
+ end
37
+ end
38
+
39
+ context 'when :fetch_size was not set' do
40
+ let(:args) { { begins_with: '/foo', fetch_size: nil } }
41
+
42
+ it '@base_params[:max_results] is 50' do
43
+ expect(base_params[:max_results]).to eq(50)
44
+ end
45
+ end
46
+
47
+ context 'when :fetch_size > 50' do
48
+ let(:args) { { begins_with: '/foo', fetch_size: 51 } }
49
+
50
+ it '@base_params[:max_results] is 50' do
51
+ expect(base_params[:max_results]).to eq(50)
52
+ end
53
+ end
54
+ end
55
+
56
+ describe '#fetch' do
57
+ let(:client) { fetcher.send(:client) }
58
+
59
+ context 'when describe_parameters return empty parameters' do
60
+ before do
61
+ allow_any_instance_of(Aws::SSM::Client).to \
62
+ receive(:describe_parameters).and_return(AwsSsmEnv::FetchResult::EMPTY)
63
+ end
64
+
65
+ it 'return AwsSsmEnv::FetchResult::EMPTY' do
66
+ expect(fetcher.send(:fetch, nil)).to eq(AwsSsmEnv::FetchResult::EMPTY)
67
+ end
68
+
69
+ context 'when next_token is nil' do
70
+ it 'called describe_parameters without next_token' do
71
+ expect(fetcher.send(:fetch, nil)).to eq(AwsSsmEnv::FetchResult::EMPTY)
72
+ expect(client).to \
73
+ have_received(:describe_parameters).with(base_params).once
74
+ end
75
+ end
76
+
77
+ context 'when next_token is not nil' do
78
+ it 'called get_parameters_by_path with next_token' do
79
+ expect(fetcher.send(:fetch, 'next_token')).to eq(AwsSsmEnv::FetchResult::EMPTY)
80
+ expect(client).to have_received(:describe_parameters)
81
+ .with(base_params.merge(next_token: 'next_token')).once
82
+ end
83
+ end
84
+ end
85
+
86
+ context 'when describe_parameters return not empty parameters' do
87
+ let!(:dummy_parameters) { [ Parameter.new('foo'), Parameter.new('bar') ] }
88
+ let!(:dummy_response) { AwsSsmEnv::FetchResult.new(dummy_parameters, 'next_token') }
89
+
90
+ before do
91
+ allow_any_instance_of(Aws::SSM::Client).to \
92
+ receive(:describe_parameters).and_return(dummy_response)
93
+ allow_any_instance_of(Aws::SSM::Client).to \
94
+ receive(:get_parameters).and_return(dummy_response)
95
+ end
96
+
97
+ it 'return parameters' do
98
+ response = fetcher.send(:fetch, 'next_token')
99
+ expect(response.parameters).to eq(dummy_response.parameters)
100
+ expect(response.next_token).to eq(dummy_response.next_token)
101
+ expect(client).to have_received(:get_parameters)
102
+ .with(names: dummy_parameters.map(&:name), with_decryption: true).once
103
+ end
104
+ end
105
+ end
106
+ end