config_o_mat 0.2.1 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 35eaeccb6abdf69afb14c8cbe951c4579734f27af4844673b1761db70dfd1bf1
4
- data.tar.gz: 392a61b3ee55ba8d9351c2b7d69d39de7d591bbde87f3374fac77fc425e628b8
3
+ metadata.gz: df6fa2a90625e5927b75d670dce66e5e4823cdb9af32ce64f1bdae0280a3146d
4
+ data.tar.gz: ac2ec11e30427a04952478b8b4293eb9be53ba6af3299ac8a7e6156f6eca14ac
5
5
  SHA512:
6
- metadata.gz: 31f9277ada3b1a7fc09efe007a3b530fb3f100c480c09ab0af83a9879ab1fa3ea0b0db3252300fe8dbe9bb9877acf2244727a94554a6384bf52641a5a95c685b
7
- data.tar.gz: 4346bae4957800e67e238c0231ddf1b424e1c5afe60a86e2d29e2134f480d56979d320514e24b412557f7f31b239fd7342ffdce459c387ba2b3ce56ce3e3e054
6
+ metadata.gz: f9efe6b0f7c05308fe8acfd7e569ca95adb3c85f8952fd70a820ef82f652054c0428ce0c7e773248c1e31d8e47c01d3a70791fedf34266016eeab07fe73eb224
7
+ data.tar.gz: a43d73f798e16f4388551666da9a8d573fa3e97442288a3d8b215014fa4593dbe07253c2cde4346146b79871b4daba3856342cf41588a6d43d18f7df01c40755
data/.gitignore CHANGED
@@ -7,3 +7,6 @@ coverage/
7
7
  # YARD doc db and output
8
8
  .yardoc/
9
9
  doc/
10
+
11
+ # Gem builds
12
+ pkg/
data/CHANGELOG.md ADDED
@@ -0,0 +1,34 @@
1
+ ## 0.4.1
2
+
3
+ ENHANCEMENTS:
4
+
5
+ * Omit empty lines in generated config when using <% %> ERB templates.
6
+
7
+ ## 0.4.0
8
+
9
+ NEW FEATURES:
10
+
11
+ * Facter support. In your configomat config set a top level `facter` key to either truthy or a string. If truthy, facter data will be exposed in a profile named `facter` in all templates. If set to a string, facter data will be exposed in a profile with the given name in all templates.
12
+ * Attempting to access a configuration variable from a profile or secret that does not exist using `#[]` will now raise an exception indicating the key being incorrectly accessed. If you need to access optionally present configuration variables from profiles or secrets use `#fetch(key, nil)`.
13
+
14
+ ## 0.3.0
15
+
16
+ NEW FEATURES:
17
+
18
+ * AWS Secrets Manager support, through an AWS AppConfig configuration. Values to pull from AWS Secrets Manager can be set using an "aws:secrets" key in any loaded AWS AppConfig JSON or YAML configuration. The value must be a dictionary. Keys in this dictionary will be exposed in the #secrets hash on the profile in templates. Values in this dictionary must be a dictionary containing at a minimum a `secret_id` key, which must be the id of an AWS Secrets Manager Secret. The dictionary may also contain a `version_id` or `version_stage` key to indicate which version of the secret to load, and a `content_type` key which may be one of `text/plan`, `application/json`, or `application/x-yaml` to indicate how the secret data should be parsed.
19
+
20
+ ## 0.2.1
21
+
22
+ BUG FIXES:
23
+
24
+ * restart_all actually works.
25
+
26
+ ## 0.2.0
27
+
28
+ NEW FEATURES:
29
+
30
+ * restart_all restart_mode for services to restart all running instances of an instantiated service.
31
+
32
+ ## 0.1.0
33
+
34
+ Initial release
data/Gemfile CHANGED
@@ -16,11 +16,4 @@
16
16
 
17
17
  source 'https://rubygems.org'
18
18
 
19
- gem 'aws-sdk-appconfig', '~> 1.18', require: false
20
- gem 'logsformyfamily', '~> 0.2', require: false
21
- gem 'lifecycle_vm', '~> 0.1.1', require: false
22
- gem 'ruby-dbus', '~> 0.16.0', require: false
23
- gem 'sd_notify', '~> 0.1', require: false
24
-
25
- gem 'rspec', '~> 3.10', group: :test, require: false
26
- gem 'simplecov', '~> 0.21', group: :test, require: false
19
+ gemspec
data/Gemfile.lock CHANGED
@@ -1,36 +1,55 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ config_o_mat (0.4.0)
5
+ aws-sdk-appconfig (~> 1.18)
6
+ aws-sdk-secretsmanager (~> 1.57)
7
+ facter (~> 4.2, >= 4.2.8)
8
+ lifecycle_vm (~> 0.1.1)
9
+ logsformyfamily (~> 0.2)
10
+ ruby-dbus (~> 0.16.0)
11
+ sd_notify (~> 0.1.1)
12
+
1
13
  GEM
2
14
  remote: https://rubygems.org/
3
15
  specs:
4
16
  aws-eventstream (1.2.0)
5
- aws-partitions (1.525.0)
6
- aws-sdk-appconfig (1.19.0)
7
- aws-sdk-core (~> 3, >= 3.122.0)
17
+ aws-partitions (1.574.0)
18
+ aws-sdk-appconfig (1.25.0)
19
+ aws-sdk-core (~> 3, >= 3.127.0)
8
20
  aws-sigv4 (~> 1.1)
9
- aws-sdk-core (3.122.0)
21
+ aws-sdk-core (3.130.0)
10
22
  aws-eventstream (~> 1, >= 1.0.2)
11
23
  aws-partitions (~> 1, >= 1.525.0)
12
24
  aws-sigv4 (~> 1.1)
13
25
  jmespath (~> 1.0)
26
+ aws-sdk-secretsmanager (1.59.0)
27
+ aws-sdk-core (~> 3, >= 3.127.0)
28
+ aws-sigv4 (~> 1.1)
14
29
  aws-sigv4 (1.4.0)
15
30
  aws-eventstream (~> 1, >= 1.0.2)
16
- diff-lcs (1.4.4)
31
+ diff-lcs (1.5.0)
17
32
  docile (1.4.0)
18
- jmespath (1.4.0)
19
- lifecycle_vm (0.1.2)
20
- logsformyfamily (0.2.3)
21
- rspec (3.10.0)
22
- rspec-core (~> 3.10.0)
23
- rspec-expectations (~> 3.10.0)
24
- rspec-mocks (~> 3.10.0)
25
- rspec-core (3.10.1)
26
- rspec-support (~> 3.10.0)
27
- rspec-expectations (3.10.1)
33
+ facter (4.2.9)
34
+ hocon (~> 1.3)
35
+ thor (>= 1.0.1, < 2.0)
36
+ hocon (1.3.1)
37
+ jmespath (1.6.1)
38
+ lifecycle_vm (0.1.3)
39
+ logsformyfamily (0.3.0)
40
+ rspec (3.11.0)
41
+ rspec-core (~> 3.11.0)
42
+ rspec-expectations (~> 3.11.0)
43
+ rspec-mocks (~> 3.11.0)
44
+ rspec-core (3.11.0)
45
+ rspec-support (~> 3.11.0)
46
+ rspec-expectations (3.11.0)
28
47
  diff-lcs (>= 1.2.0, < 2.0)
29
- rspec-support (~> 3.10.0)
30
- rspec-mocks (3.10.2)
48
+ rspec-support (~> 3.11.0)
49
+ rspec-mocks (3.11.0)
31
50
  diff-lcs (>= 1.2.0, < 2.0)
32
- rspec-support (~> 3.10.0)
33
- rspec-support (3.10.3)
51
+ rspec-support (~> 3.11.0)
52
+ rspec-support (3.11.0)
34
53
  ruby-dbus (0.16.0)
35
54
  sd_notify (0.1.1)
36
55
  simplecov (0.21.2)
@@ -38,19 +57,16 @@ GEM
38
57
  simplecov-html (~> 0.11)
39
58
  simplecov_json_formatter (~> 0.1)
40
59
  simplecov-html (0.12.3)
41
- simplecov_json_formatter (0.1.3)
60
+ simplecov_json_formatter (0.1.4)
61
+ thor (1.2.1)
42
62
 
43
63
  PLATFORMS
44
64
  ruby
45
65
 
46
66
  DEPENDENCIES
47
- aws-sdk-appconfig (~> 1.18)
48
- lifecycle_vm (~> 0.1.1)
49
- logsformyfamily (~> 0.2)
67
+ config_o_mat!
50
68
  rspec (~> 3.10)
51
- ruby-dbus (~> 0.16.0)
52
- sd_notify (~> 0.1)
53
- simplecov (~> 0.21)
69
+ simplecov (~> 0.21.2)
54
70
 
55
71
  BUNDLED WITH
56
72
  2.1.4
data/config_o_mat.gemspec CHANGED
@@ -39,10 +39,12 @@ Gem::Specification.new do |spec|
39
39
  spec.executables = ["config_o_mat-configurator", "config_o_mat-meta_configurator"]
40
40
 
41
41
  spec.add_dependency('aws-sdk-appconfig', '~> 1.18')
42
+ spec.add_dependency('aws-sdk-secretsmanager', '~> 1.57')
42
43
  spec.add_dependency('logsformyfamily', '~> 0.2')
43
44
  spec.add_dependency('lifecycle_vm', '~> 0.1.1')
44
45
  spec.add_dependency('ruby-dbus', '~> 0.16.0')
45
46
  spec.add_dependency('sd_notify', '~> 0.1.1')
47
+ spec.add_dependency('facter', ['~> 4.2', '>= 4.2.8'])
46
48
 
47
49
  spec.add_development_dependency('simplecov', '~> 0.21.2')
48
50
  spec.add_development_dependency('rspec', '~> 3.10')
@@ -25,7 +25,8 @@ module ConfigOMat
25
25
  :client_id, :compiled_templates, :applied_profiles, :applying_profile,
26
26
  :generated_templates, :services_to_reload, :profiles_to_apply,
27
27
  :last_refresh_time, :next_state, :retry_count, :retries_left, :retry_wait,
28
- :region, :appconfig_client, :systemd_interface
28
+ :region, :appconfig_client, :secretsmanager_client, :systemd_interface,
29
+ :secrets_loader_memory
29
30
 
30
31
  def initialize(
31
32
  argv: [],
@@ -55,7 +56,9 @@ module ConfigOMat
55
56
  retry_wait: 2,
56
57
  region: nil,
57
58
  appconfig_client: nil,
58
- systemd_interface: nil
59
+ secretsmanager_client: nil,
60
+ systemd_interface: nil,
61
+ secrets_loader_memory: nil
59
62
  )
60
63
  super()
61
64
 
@@ -86,7 +89,9 @@ module ConfigOMat
86
89
  @retry_wait = retry_wait
87
90
  @region = region
88
91
  @appconfig_client = appconfig_client
92
+ @secretsmanager_client = secretsmanager_client
89
93
  @systemd_interface = systemd_interface
94
+ @secrets_loader_memory = secrets_loader_memory
90
95
  end
91
96
  end
92
97
  end
@@ -28,7 +28,7 @@ module ConfigOMat
28
28
  self.compiled_templates = template_defs.each_with_object({}) do |(key, templ_def), hash|
29
29
  filename = File.join(configuration_directory, 'templates', templ_def.src)
30
30
  begin
31
- templ = ERB.new(File.read(filename))
31
+ templ = ERB.new(File.read(filename), trim_mode: '<>')
32
32
  templ.filename = filename
33
33
  hash[key] = templ.def_class(Object, 'render(profiles)').new
34
34
  rescue SyntaxError, StandardError => e
@@ -17,18 +17,20 @@
17
17
  require 'lifecycle_vm/op_base'
18
18
 
19
19
  require 'aws-sdk-appconfig'
20
+ require 'aws-sdk-secretsmanager'
20
21
 
21
22
  module ConfigOMat
22
23
  module Op
23
- class ConnectToAppconfig < LifecycleVM::OpBase
24
+ class ConnectToAws < LifecycleVM::OpBase
24
25
  reads :region
25
- writes :appconfig_client
26
+ writes :appconfig_client, :secretsmanager_client
26
27
 
27
28
  def call
28
29
  client_opts = { logger: logger }
29
30
  client_opts[:region] = region if region
30
31
 
31
32
  self.appconfig_client = Aws::AppConfig::Client.new(client_opts)
33
+ self.secretsmanager_client = Aws::SecretsManager::Client.new(client_opts)
32
34
  end
33
35
  end
34
36
  end
@@ -16,47 +16,95 @@
16
16
 
17
17
  require 'lifecycle_vm/op_base'
18
18
 
19
+ require 'config_o_mat/secrets_loader'
20
+
19
21
  module ConfigOMat
20
22
  module Op
21
23
  class RefreshAllProfiles < LifecycleVM::OpBase
22
- reads :profile_defs, :applied_profiles, :client_id, :appconfig_client
23
- writes :profiles_to_apply, :last_refresh_time
24
+ reads :profile_defs, :applied_profiles, :client_id, :appconfig_client, :secrets_loader_memory, :secretsmanager_client
25
+ writes :profiles_to_apply, :last_refresh_time, :secrets_loader_memory
24
26
 
25
27
  def call
26
28
  self.profiles_to_apply = []
27
29
  self.last_refresh_time = Time.now.to_i
28
30
 
29
31
  profile_defs.each do |(profile_name, definition)|
30
- request = {
31
- application: definition.application, environment: definition.environment,
32
- configuration: definition.profile, client_id: client_id
33
- }
34
-
35
- current_version = applied_profiles&.fetch(profile_name, nil)&.version
36
- request[:client_configuration_version] = current_version if current_version
37
-
38
- response =
39
- begin
40
- appconfig_client.get_configuration(request)
41
- rescue StandardError => e
42
- error profile_name, e
43
- nil
44
- end
45
-
46
- next if response.nil?
47
-
48
- loaded_version = response.configuration_version
49
- next if current_version && loaded_version == current_version
50
-
51
- logger&.notice(
52
- :updated_profile,
53
- name: profile_name, previous_version: current_version, new_version: loaded_version
54
- )
55
-
56
- profiles_to_apply << LoadedProfile.new(
57
- profile_name, loaded_version, response.content.read, response.content_type
58
- )
32
+ if definition.kind_of?(ConfigOMat::Profile)
33
+ refresh_appconfig_profile(profile_name, definition)
34
+ elsif definition.kind_of?(ConfigOMat::FacterProfile)
35
+ refresh_facter_profile(profile_name)
36
+ else
37
+ error profile_name, "unknown profile type #{definition.class.name}"
38
+ end
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def refresh_facter_profile(profile_name)
45
+ current_version = applied_profiles&.fetch(profile_name, nil)&.version
46
+ new_profile = ConfigOMat::LoadedFacterProfile.new(profile_name)
47
+
48
+ return if new_profile.version == current_version
49
+
50
+ logger&.notice(
51
+ :updated_profile,
52
+ name: profile_name, previous_version: current_version, new_version: new_profile.version
53
+ )
54
+
55
+ profiles_to_apply << LoadedProfile.new(new_profile, nil)
56
+ end
57
+
58
+ def refresh_appconfig_profile(profile_name, definition)
59
+ request = {
60
+ application: definition.application, environment: definition.environment,
61
+ configuration: definition.profile, client_id: client_id
62
+ }
63
+
64
+ current_version = applied_profiles&.fetch(profile_name, nil)&.version
65
+ request[:client_configuration_version] = current_version if current_version
66
+
67
+ response =
68
+ begin
69
+ appconfig_client.get_configuration(request)
70
+ rescue StandardError => e
71
+ error profile_name, e
72
+ nil
73
+ end
74
+
75
+ return if response.nil?
76
+
77
+ loaded_version = response.configuration_version
78
+ return if current_version && loaded_version == current_version
79
+
80
+ logger&.notice(
81
+ :updated_profile,
82
+ name: profile_name, previous_version: current_version, new_version: loaded_version
83
+ )
84
+
85
+ profile = LoadedAppconfigProfile.new(
86
+ profile_name, loaded_version, response.content.read, response.content_type
87
+ )
88
+
89
+ loaded_secrets = nil
90
+
91
+ if !profile.secret_defs.empty?
92
+ self.secrets_loader_memory ||= ConfigOMat::SecretsLoader::Memory.new(secretsmanager_client: secretsmanager_client)
93
+ secrets_loader_memory.update_secret_defs_to_load(profile.secret_defs.values)
94
+
95
+ vm = ConfigOMat::SecretsLoader::VM.new(secrets_loader_memory).call
96
+
97
+ if vm.errors?
98
+ error :"#{profile_name}_secrets", vm.errors
99
+ return
100
+ end
101
+
102
+ loaded_secrets = secrets_loader_memory.loaded_secrets.each_with_object({}) do |(key, value), hash|
103
+ hash[value.name] = value
104
+ end
59
105
  end
106
+
107
+ profiles_to_apply << LoadedProfile.new(profile, loaded_secrets)
60
108
  end
61
109
  end
62
110
  end
@@ -19,10 +19,42 @@ require 'lifecycle_vm/op_base'
19
19
  module ConfigOMat
20
20
  module Op
21
21
  class RefreshProfile < LifecycleVM::OpBase
22
- reads :profile_defs, :client_id, :applying_profile, :appconfig_client
23
- writes :applying_profile
22
+ reads :profile_defs, :client_id, :applying_profile, :appconfig_client, :secretsmanager_client, :secrets_loader_memory
23
+ writes :applying_profile, :secrets_loader_memory
24
24
 
25
25
  def call
26
+ if applying_profile.loaded_profile_data.kind_of?(ConfigOMat::LoadedAppconfigProfile)
27
+ refresh_appconfig_profile
28
+ elsif applying_profile.loaded_profile_data.kind_of?(ConfigOMat::LoadedFacterProfile)
29
+ refresh_facter_profile
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def refresh_facter_profile
36
+ profile_name = applying_profile.name
37
+ profile_version = applying_profile.version
38
+ definition = profile_defs[profile_name]
39
+ new_profile = ConfigOMat::LoadedFacterProfile.new(profile_name)
40
+
41
+ if new_profile.version == profile_version
42
+ logger&.warning(
43
+ :no_update,
44
+ name: profile_name, version: profile_version
45
+ )
46
+ return
47
+ end
48
+
49
+ logger&.notice(
50
+ :updated_profile,
51
+ name: profile_name, previous_version: profile_version, new_version: new_profile.version
52
+ )
53
+
54
+ self.applying_profile = LoadedProfile.new(new_profile, nil)
55
+ end
56
+
57
+ def refresh_appconfig_profile
26
58
  profile_name = applying_profile.name
27
59
  profile_version = applying_profile.version
28
60
  definition = profile_defs[profile_name]
@@ -56,9 +88,29 @@ module ConfigOMat
56
88
  name: profile_name, previous_version: profile_version, new_version: loaded_version
57
89
  )
58
90
 
59
- self.applying_profile = LoadedProfile.new(
91
+ profile = LoadedAppconfigProfile.new(
60
92
  profile_name, loaded_version, response.content.read, response.content_type
61
93
  )
94
+
95
+ loaded_secrets = nil
96
+
97
+ if !profile.secret_defs.empty?
98
+ self.secrets_loader_memory ||= ConfigOMat::SecretsLoader::Memory.new(secretsmanager_client: secretsmanager_client)
99
+ secrets_loader_memory.update_secret_defs_to_load(profile.secret_defs.values)
100
+
101
+ vm = ConfigOMat::SecretsLoader::VM.new(secrets_loader_memory).call
102
+
103
+ if vm.errors?
104
+ error :"#{profile_name}_secrets", vm.errors
105
+ return
106
+ end
107
+
108
+ loaded_secrets = secrets_loader_memory.loaded_secrets.each_with_object({}) do |(key, value), hash|
109
+ hash[value.name] = value
110
+ end
111
+ end
112
+
113
+ self.applying_profile = LoadedProfile.new(profile, loaded_secrets)
62
114
  end
63
115
  end
64
116
  end
@@ -41,9 +41,9 @@ module ConfigOMat
41
41
 
42
42
  on :reading_meta_config, do: Op::LoadMetaConfig, then: :compiling_templates
43
43
 
44
- on :compiling_templates, do: Op::CompileTemplates, then: :connecting_to_appconfig
44
+ on :compiling_templates, do: Op::CompileTemplates, then: :connecting_to_aws
45
45
 
46
- on :connecting_to_appconfig, do: Op::ConnectToAppconfig, then: :refreshing_profiles
46
+ on :connecting_to_aws, do: Op::ConnectToAws, then: :refreshing_profiles
47
47
 
48
48
  on :refreshing_profiles,
49
49
  do: Op::RefreshAllProfiles,
@@ -116,7 +116,7 @@ module ConfigOMat
116
116
  }
117
117
  }
118
118
 
119
- on :refreshing_profile, do: Op::RefreshProfile, then: :applying_profile
119
+ on :refreshing_profile, do: Op::RefreshProfile, then: :generating_templates
120
120
  end
121
121
  end
122
122
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2021 Teak.io, Inc.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'lifecycle_vm/cond_base'
18
+
19
+ module ConfigOMat
20
+ module Cond
21
+ class RequiresLoad < LifecycleVM::CondBase
22
+ reads :loading_secret
23
+
24
+ def call
25
+ !loading_secret.nil?
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2021 Teak.io, Inc.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'lifecycle_vm/cond_base'
18
+
19
+ module ConfigOMat
20
+ module Cond
21
+ class SecretsToLoad < LifecycleVM::CondBase
22
+ reads :secret_defs_to_load
23
+
24
+ def call
25
+ !secret_defs_to_load.empty?
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2021 Teak.io, Inc.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ Dir[File.join(__dir__, 'cond', '*')].sort.each { |file| require file }
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2021 Teak.io, Inc.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'lifecycle_vm'
18
+
19
+ module ConfigOMat
20
+ module SecretsLoader
21
+ class Memory < LifecycleVM::Memory
22
+ attr_reader :secretsmanager_client
23
+
24
+ attr_accessor :secrets_cache, :secret_defs_to_load, :loaded_secrets, :loading_secret
25
+
26
+ def initialize(
27
+ secretsmanager_client: nil,
28
+ secret_defs_to_load: [],
29
+ secrets_cache: {},
30
+ loading_secret: nil,
31
+ logger: nil
32
+ )
33
+ @secretsmanager_client = secretsmanager_client
34
+ @secret_defs_to_load = secret_defs_to_load
35
+ @secrets_cache = secrets_cache
36
+ @loading_secret = loading_secret
37
+ @loaded_secrets = {}
38
+ @logger = logger
39
+ end
40
+
41
+ def update_secret_defs_to_load(defs)
42
+ @loaded_secrets = {}
43
+ @secret_defs_to_load = defs
44
+ self
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2021 Teak.io, Inc.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'lifecycle_vm/op_base'
18
+
19
+ module ConfigOMat
20
+ module Op
21
+ class CheckCache < LifecycleVM::OpBase
22
+ reads :loading_secret, :secrets_cache, :loaded_secrets
23
+ writes :loading_secret, :loaded_secrets
24
+
25
+ def call
26
+ # If we're referencing a secret by its version stage, never cache it, since these can be
27
+ # updated out from under us.
28
+ return unless loading_secret.version_stage.nil? || loading_secret.version_stage.empty?
29
+
30
+ cached_secret = secrets_cache[loading_secret.secret_id]
31
+
32
+ if cached_secret && cached_secret.version_id == loading_secret.version_id
33
+ logger&.info(:cached_secret, name: loading_secret.name, version_id: loading_secret.version_id)
34
+ loaded_secrets[loading_secret] = cached_secret
35
+ self.loading_secret = nil
36
+ elsif cached_secret
37
+ logger&.info(
38
+ :invalid_cached_secret,
39
+ name: loading_secret.name,
40
+ expected_version_id: loading_secret.version_id,
41
+ cached_version_id: cached_secret.version_id
42
+ )
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2021 Teak.io, Inc.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'lifecycle_vm/op_base'
18
+
19
+ require 'config_o_mat/shared/types'
20
+
21
+ module ConfigOMat
22
+ module Op
23
+ class LoadSecret < LifecycleVM::OpBase
24
+ reads :loading_secret, :secrets_cache, :loaded_secrets, :secretsmanager_client
25
+ writes :loading_secret, :loaded_secrets, :secrets_cache
26
+
27
+ def call
28
+ opts = {
29
+ secret_id: loading_secret.secret_id
30
+ }
31
+
32
+ if loading_secret.version_id
33
+ opts[:version_id] = loading_secret.version_id
34
+ else
35
+ opts[:version_stage] = loading_secret.version_stage
36
+ end
37
+
38
+ response =
39
+ begin
40
+ secretsmanager_client.get_secret_value(opts)
41
+ rescue StandardError => e
42
+ error loading_secret.name, e
43
+ nil
44
+ end
45
+
46
+ return if response.nil? || errors?
47
+
48
+ loaded_secret = LoadedSecret.new(
49
+ loading_secret.name, loading_secret.secret_id, response.version_id,
50
+ response.secret_string, loading_secret.content_type
51
+ )
52
+
53
+ logger&.info(
54
+ :loaded_secret, name: loading_secret.name, arn: response.arn,
55
+ version_id: response.version_id
56
+ )
57
+
58
+ begin
59
+ loaded_secret.validate!
60
+ rescue StandardError => e
61
+ logger&.error(
62
+ :invalid_secret, name: loading_secret.name, arn: response.arn,
63
+ version_id: response.version_id, errors: e
64
+ )
65
+ error loaded_secret.name, e
66
+ end
67
+
68
+ return if errors?
69
+
70
+ secrets_cache[loading_secret.secret_id] = loaded_secret
71
+ loaded_secrets[loading_secret] = loaded_secret
72
+ self.loading_secret = nil
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2021 Teak.io, Inc.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'lifecycle_vm/op_base'
18
+
19
+ module ConfigOMat
20
+ module Op
21
+ class StageOneSecret < LifecycleVM::OpBase
22
+ reads :secret_defs_to_load
23
+ writes :loading_secret, :secret_defs_to_load
24
+
25
+ def call
26
+ self.loading_secret = secret_defs_to_load.pop
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2021 Teak.io, Inc.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ Dir[File.join(__dir__, 'op', '*')].sort.each { |file| require file }
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2021 Teak.io, Inc.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'lifecycle_vm'
18
+
19
+ require 'config_o_mat/secrets_loader/op'
20
+ require 'config_o_mat/secrets_loader/cond'
21
+ require 'config_o_mat/secrets_loader/memory'
22
+
23
+ module ConfigOMat
24
+ module SecretsLoader
25
+ class VM < LifecycleVM::VM
26
+ memory_class ConfigOMat::SecretsLoader::Memory
27
+
28
+ on :start, then: {
29
+ case: Cond::SecretsToLoad,
30
+ when: {
31
+ true => {
32
+ do: Op::StageOneSecret,
33
+ then: :check_cache
34
+ },
35
+ false => {
36
+ then: :exit
37
+ }
38
+ }
39
+ }
40
+
41
+ on :check_cache, do: Op::CheckCache, then: {
42
+ case: Cond::RequiresLoad,
43
+ when: {
44
+ true => :load_secret,
45
+ false => :start
46
+ }
47
+ }
48
+
49
+ on :load_secret, do: Op::LoadSecret, then: :start
50
+ end
51
+ end
52
+ end
@@ -126,6 +126,16 @@ module ConfigOMat
126
126
  self.template_defs = instantiate.call(:templates, Template)
127
127
  self.profile_defs = instantiate.call(:profiles, Profile)
128
128
 
129
+ facter = merged_config[:facter]
130
+ if facter
131
+ facter_key = facter.kind_of?(String) ? facter.to_sym : :facter
132
+ if profile_defs.key?(facter_key)
133
+ error :facter, "conflicts with profile #{facter_key}"
134
+ else
135
+ profile_defs[facter_key] = ConfigOMat::FacterProfile.new
136
+ end
137
+ end
138
+
129
139
  self.logger = LogsForMyFamily::Logger.new if !logger
130
140
 
131
141
  log_type = merged_config[:log_type]&.to_sym || :stdout
@@ -14,6 +14,8 @@
14
14
  # See the License for the specific language governing permissions and
15
15
  # limitations under the License.
16
16
 
17
+ require 'facter'
18
+
17
19
  require 'json'
18
20
  require 'yaml'
19
21
  require 'digest'
@@ -187,9 +189,86 @@ module ConfigOMat
187
189
  end
188
190
  end
189
191
 
190
- class LoadedProfile < ConfigItem
192
+ class FacterProfile < ConfigItem
193
+ end
194
+
195
+ class LoadedFacterProfile < ConfigItem
196
+ CLEAR_FROM_FACTER = [
197
+ "memoryfree", "memoryfree_mb", "load_averages", "uptime", "system_uptime", "uptime_seconds", "uptime_hours", "uptime_days",
198
+ {"ec2_metadata" => ["identity-credentials"]},
199
+ {"memory" => [{"system" => ["capacity", "available_bytes", "used", "used_bytes", "available"] }] }
200
+ ].freeze
201
+
191
202
  attr_reader :name, :version, :contents
192
203
 
204
+ def initialize(name)
205
+ @name = name
206
+ load_from_facter
207
+ @version = contents.hash
208
+ end
209
+
210
+ def validate
211
+ error :name, 'must be present' if @name.nil? || @name.empty?
212
+ error :contents, 'must be present' if @contents.nil? || @contents.empty?
213
+ error :contents, 'must be a hash' if !@contents.kind_of?(Hash)
214
+ end
215
+
216
+ def hash
217
+ @name.hash ^ @version
218
+ end
219
+
220
+ def to_h
221
+ @contents
222
+ end
223
+
224
+ def eql?(other)
225
+ return false if !super(other)
226
+ return false if other.version != version || other.name != name
227
+ true
228
+ end
229
+
230
+ private
231
+
232
+ def load_from_facter
233
+ Facter.clear
234
+ # This is to work around a bug in Facter wherein it fails to invalidate a second cache of the
235
+ # IMDSv2 token.
236
+ Facter::Resolvers::Ec2.instance_variable_set(:@v2_token, nil)
237
+ new_facts = Facter.to_hash
238
+ clear(new_facts, CLEAR_FROM_FACTER)
239
+ transform(new_facts)
240
+ @contents = new_facts
241
+ end
242
+
243
+ def clear(hash, diffs)
244
+ diffs.each do |diff|
245
+ if diff.kind_of?(Hash)
246
+ diff.each do |(key, values)|
247
+ clear(hash[key], values) if hash.key?(key)
248
+ end
249
+ elsif hash
250
+ hash.delete(diff)
251
+ end
252
+ end
253
+ end
254
+
255
+ def transform(hash)
256
+ return unless hash.kind_of?(Hash)
257
+ hash.transform_keys!(&:to_sym)
258
+ hash.default_proc = proc { |hash, key| raise KeyError.new("No key #{key.inspect} in profile #{name}", key: key, receiver: hash) }
259
+ hash.each_value do |value|
260
+ if value.kind_of?(Hash)
261
+ transform(value)
262
+ elsif value.kind_of?(Array)
263
+ value.each { |v| transform(v) }
264
+ end
265
+ end
266
+ end
267
+ end
268
+
269
+ class LoadedAppconfigProfile < ConfigItem
270
+ attr_reader :name, :version, :contents, :secret_defs
271
+
193
272
  PARSERS = {
194
273
  'text/plain' => proc { |str| str },
195
274
  'application/json' => proc { |str| JSON.parse(str, symbolize_names: true) },
@@ -199,12 +278,19 @@ module ConfigOMat
199
278
  def initialize(name, version, contents, content_type)
200
279
  @name = name
201
280
  @version = version
281
+ @secret_defs = {}
202
282
 
203
283
  parser = PARSERS[content_type]
204
284
 
205
285
  if parser
206
286
  begin
207
287
  @contents = parser.call(contents)
288
+ if @contents.kind_of?(Hash)
289
+ parse_secrets
290
+ @contents.default_proc = proc do |hash, key|
291
+ raise KeyError.new("No key #{key.inspect} in profile #{name}", key: key, receiver: hash)
292
+ end
293
+ end
208
294
  rescue StandardError => e
209
295
  error :contents, e
210
296
  end
@@ -233,6 +319,138 @@ module ConfigOMat
233
319
  return false if other.version != version || other.contents != contents || other.name != name
234
320
  true
235
321
  end
322
+
323
+ private
324
+
325
+ def parse_secrets
326
+ secret_entries = @contents.fetch(:"aws:secrets", nil)
327
+ return if secret_entries.nil?
328
+
329
+ error :contents_secrets, 'must be a dictionary' if !secret_entries.kind_of?(Hash)
330
+
331
+ secret_entries.each do |(secret_name, secret_conf)|
332
+ secret_def = Secret.new(secret_name, secret_conf)
333
+ secret_def.validate
334
+ error :"contents_secrets_#{secret_name}", secret_def.errors if secret_def.errors?
335
+
336
+ @secret_defs[secret_name] = secret_def
337
+ end
338
+ end
339
+ end
340
+
341
+ class Secret < ConfigItem
342
+ VALID_CONTENT_TYPES = LoadedAppconfigProfile::PARSERS.keys.freeze
343
+
344
+ attr_reader :name, :secret_id, :version_id, :version_stage, :content_type
345
+
346
+ def initialize(name, opts)
347
+ @name = name
348
+ @secret_id = opts[:secret_id]
349
+ @version_id = opts[:version_id]
350
+ @version_stage = opts[:version_stage]
351
+ @content_type = opts[:content_type]&.downcase
352
+
353
+ if (@version_id.nil? || @version_id.empty?) && (@version_stage.nil? || @version_stage.empty?)
354
+ @version_stage = 'AWSCURRENT'
355
+ end
356
+
357
+ @content_type ||= 'application/json'
358
+ end
359
+
360
+ def validate
361
+ error :secret_id, 'must be present' if @secret_id.nil? || @secret_id.empty?
362
+ error :content_type, "must be one of #{VALID_CONTENT_TYPES}" unless VALID_CONTENT_TYPES.include?(@content_type)
363
+ end
364
+
365
+ def hash
366
+ secret_id.hash ^ version_id.hash ^ version_stage.hash & content_type.hash
367
+ end
368
+
369
+ def eql?(other)
370
+ return false if !super(other)
371
+ if other.name != name ||
372
+ other.secret_id != secret_id ||
373
+ other.version_id != version_id ||
374
+ other.version_stage != version_stage ||
375
+ other.content_type != content_type
376
+ return false
377
+ end
378
+ true
379
+ end
380
+ end
381
+
382
+ class LoadedSecret < ConfigItem
383
+ attr_reader :name, :secret_id, :version_id, :contents
384
+
385
+ def initialize(name, secret_id, version_id, secret_string, content_type)
386
+ @name = name
387
+ @secret_id = secret_id
388
+ @version_id = version_id
389
+
390
+ begin
391
+ @contents = LoadedAppconfigProfile::PARSERS[content_type].call(secret_string)
392
+ if @contents.kind_of?(Hash)
393
+ @contents.default_proc = proc do |hash, key|
394
+ raise KeyError.new("No key #{key.inspect} in secret #{name}", key: key, receiver: hash)
395
+ end
396
+ end
397
+ rescue StandardError => e
398
+ error :contents, e
399
+ end
400
+ end
401
+
402
+ def validate
403
+ # Since name and version_id are coming from AWS and must be present, I'm not going to check
404
+ # them here.
405
+ end
406
+
407
+ def hash
408
+ @name.hash ^ @secret_id.hash ^ @version_id.hash ^ @contents.hash
409
+ end
410
+
411
+ def eql?(other)
412
+ return false if !super(other)
413
+ if other.name != name ||
414
+ other.secret_id != secret_id ||
415
+ other.version_id != version_id ||
416
+ other.contents != contents
417
+ return false
418
+ end
419
+ true
420
+ end
421
+ end
422
+
423
+
424
+ class LoadedProfile < ConfigItem
425
+ extend Forwardable
426
+
427
+ attr_reader :secrets, :loaded_profile_data
428
+
429
+ def_delegators :@loaded_profile_data, :name, :version, :contents
430
+
431
+ def initialize(loaded_profile_data, secrets)
432
+ @loaded_profile_data = loaded_profile_data
433
+ @secrets = secrets || {}
434
+
435
+ @errors = @loaded_profile_data.errors if @loaded_profile_data.errors?
436
+ end
437
+
438
+ def validate
439
+ end
440
+
441
+ def hash
442
+ @loaded_profile_data.hash ^ @secrets.hash
443
+ end
444
+
445
+ def to_h
446
+ contents
447
+ end
448
+
449
+ def eql?(other)
450
+ return false if !super(other)
451
+ return false if other.loaded_profile_data != @loaded_profile_data || other.secrets != @secrets
452
+ true
453
+ end
236
454
  end
237
455
 
238
456
  class GeneratedTemplate < ConfigItem
@@ -15,5 +15,5 @@
15
15
  # limitations under the License.
16
16
 
17
17
  module ConfigOMat
18
- VERSION = "0.2.1"
18
+ VERSION = "0.4.1"
19
19
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: config_o_mat
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Scarborough
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-02-15 00:00:00.000000000 Z
11
+ date: 2022-04-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-appconfig
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.18'
27
+ - !ruby/object:Gem::Dependency
28
+ name: aws-sdk-secretsmanager
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.57'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.57'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: logsformyfamily
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +94,26 @@ dependencies:
80
94
  - - "~>"
81
95
  - !ruby/object:Gem::Version
82
96
  version: 0.1.1
97
+ - !ruby/object:Gem::Dependency
98
+ name: facter
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '4.2'
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: 4.2.8
107
+ type: :runtime
108
+ prerelease: false
109
+ version_requirements: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - "~>"
112
+ - !ruby/object:Gem::Version
113
+ version: '4.2'
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: 4.2.8
83
117
  - !ruby/object:Gem::Dependency
84
118
  name: simplecov
85
119
  requirement: !ruby/object:Gem::Requirement
@@ -121,6 +155,7 @@ files:
121
155
  - ".rspec"
122
156
  - ".ruby-gemset"
123
157
  - ".ruby-version"
158
+ - CHANGELOG.md
124
159
  - Gemfile
125
160
  - Gemfile.lock
126
161
  - LICENSE
@@ -144,7 +179,7 @@ files:
144
179
  - lib/config_o_mat/configurator/op/apply_all_profiles.rb
145
180
  - lib/config_o_mat/configurator/op/commit_staged_profile.rb
146
181
  - lib/config_o_mat/configurator/op/compile_templates.rb
147
- - lib/config_o_mat/configurator/op/connect_to_appconfig.rb
182
+ - lib/config_o_mat/configurator/op/connect_to_aws.rb
148
183
  - lib/config_o_mat/configurator/op/generate_all_templates.rb
149
184
  - lib/config_o_mat/configurator/op/next_tick.rb
150
185
  - lib/config_o_mat/configurator/op/notify_systemd_start.rb
@@ -170,6 +205,15 @@ files:
170
205
  - lib/config_o_mat/meta_configurator/op.rb
171
206
  - lib/config_o_mat/meta_configurator/op/generate_systemd_config.rb
172
207
  - lib/config_o_mat/meta_configurator/op/parse_meta_cli.rb
208
+ - lib/config_o_mat/secrets_loader.rb
209
+ - lib/config_o_mat/secrets_loader/cond.rb
210
+ - lib/config_o_mat/secrets_loader/cond/requires_load.rb
211
+ - lib/config_o_mat/secrets_loader/cond/secrets_to_load.rb
212
+ - lib/config_o_mat/secrets_loader/memory.rb
213
+ - lib/config_o_mat/secrets_loader/op.rb
214
+ - lib/config_o_mat/secrets_loader/op/check_cache.rb
215
+ - lib/config_o_mat/secrets_loader/op/load_secret.rb
216
+ - lib/config_o_mat/secrets_loader/op/stage_one_secret.rb
173
217
  - lib/config_o_mat/shared/cond.rb
174
218
  - lib/config_o_mat/shared/cond/early_exit.rb
175
219
  - lib/config_o_mat/shared/op.rb