simplygenius-atmos 0.7.1 → 0.8.0

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.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -4
  3. data/exe/atmos +2 -2
  4. data/lib/{atmos.rb → simplygenius/atmos.rb} +9 -7
  5. data/lib/simplygenius/atmos/cli.rb +116 -0
  6. data/lib/simplygenius/atmos/commands/account.rb +69 -0
  7. data/lib/simplygenius/atmos/commands/apply.rb +24 -0
  8. data/lib/simplygenius/atmos/commands/auth_exec.rb +34 -0
  9. data/lib/simplygenius/atmos/commands/base_command.rb +16 -0
  10. data/lib/simplygenius/atmos/commands/bootstrap.rb +76 -0
  11. data/lib/simplygenius/atmos/commands/container.rb +62 -0
  12. data/lib/simplygenius/atmos/commands/destroy.rb +22 -0
  13. data/lib/simplygenius/atmos/commands/generate.rb +187 -0
  14. data/lib/simplygenius/atmos/commands/init.rb +22 -0
  15. data/lib/simplygenius/atmos/commands/new.rb +22 -0
  16. data/lib/simplygenius/atmos/commands/otp.rb +58 -0
  17. data/lib/simplygenius/atmos/commands/plan.rb +24 -0
  18. data/lib/simplygenius/atmos/commands/secret.rb +91 -0
  19. data/lib/simplygenius/atmos/commands/terraform.rb +56 -0
  20. data/lib/simplygenius/atmos/commands/user.rb +78 -0
  21. data/lib/simplygenius/atmos/config.rb +279 -0
  22. data/lib/simplygenius/atmos/exceptions.rb +13 -0
  23. data/lib/simplygenius/atmos/generator.rb +232 -0
  24. data/lib/simplygenius/atmos/ipc.rb +136 -0
  25. data/lib/simplygenius/atmos/ipc_actions/notify.rb +31 -0
  26. data/lib/simplygenius/atmos/ipc_actions/ping.rb +23 -0
  27. data/lib/simplygenius/atmos/logging.rb +164 -0
  28. data/lib/simplygenius/atmos/otp.rb +62 -0
  29. data/lib/simplygenius/atmos/plugin.rb +27 -0
  30. data/lib/simplygenius/atmos/plugin_manager.rb +120 -0
  31. data/lib/simplygenius/atmos/plugins/output_filter.rb +29 -0
  32. data/lib/simplygenius/atmos/plugins/prompt_notify.rb +21 -0
  33. data/lib/simplygenius/atmos/provider_factory.rb +23 -0
  34. data/lib/simplygenius/atmos/providers/aws/account_manager.rb +83 -0
  35. data/lib/simplygenius/atmos/providers/aws/auth_manager.rb +220 -0
  36. data/lib/simplygenius/atmos/providers/aws/container_manager.rb +118 -0
  37. data/lib/simplygenius/atmos/providers/aws/provider.rb +53 -0
  38. data/lib/simplygenius/atmos/providers/aws/s3_secret_manager.rb +51 -0
  39. data/lib/simplygenius/atmos/providers/aws/user_manager.rb +213 -0
  40. data/lib/simplygenius/atmos/settings_hash.rb +93 -0
  41. data/lib/simplygenius/atmos/source_path.rb +186 -0
  42. data/lib/simplygenius/atmos/template.rb +117 -0
  43. data/lib/simplygenius/atmos/terraform_executor.rb +297 -0
  44. data/lib/simplygenius/atmos/ui.rb +173 -0
  45. data/lib/simplygenius/atmos/utils.rb +54 -0
  46. data/lib/simplygenius/atmos/version.rb +5 -0
  47. data/templates/new/config/atmos.yml +21 -13
  48. data/templates/new/config/atmos/recipes.yml +16 -0
  49. data/templates/new/config/atmos/runtime.yml +9 -0
  50. metadata +46 -40
  51. data/lib/atmos/cli.rb +0 -105
  52. data/lib/atmos/commands/account.rb +0 -65
  53. data/lib/atmos/commands/apply.rb +0 -20
  54. data/lib/atmos/commands/auth_exec.rb +0 -29
  55. data/lib/atmos/commands/base_command.rb +0 -12
  56. data/lib/atmos/commands/bootstrap.rb +0 -72
  57. data/lib/atmos/commands/container.rb +0 -58
  58. data/lib/atmos/commands/destroy.rb +0 -18
  59. data/lib/atmos/commands/generate.rb +0 -90
  60. data/lib/atmos/commands/init.rb +0 -18
  61. data/lib/atmos/commands/new.rb +0 -18
  62. data/lib/atmos/commands/otp.rb +0 -54
  63. data/lib/atmos/commands/plan.rb +0 -20
  64. data/lib/atmos/commands/secret.rb +0 -87
  65. data/lib/atmos/commands/terraform.rb +0 -52
  66. data/lib/atmos/commands/user.rb +0 -74
  67. data/lib/atmos/config.rb +0 -208
  68. data/lib/atmos/exceptions.rb +0 -9
  69. data/lib/atmos/generator.rb +0 -199
  70. data/lib/atmos/generator_factory.rb +0 -93
  71. data/lib/atmos/ipc.rb +0 -132
  72. data/lib/atmos/ipc_actions/notify.rb +0 -27
  73. data/lib/atmos/ipc_actions/ping.rb +0 -19
  74. data/lib/atmos/logging.rb +0 -160
  75. data/lib/atmos/otp.rb +0 -61
  76. data/lib/atmos/provider_factory.rb +0 -19
  77. data/lib/atmos/providers/aws/account_manager.rb +0 -82
  78. data/lib/atmos/providers/aws/auth_manager.rb +0 -208
  79. data/lib/atmos/providers/aws/container_manager.rb +0 -116
  80. data/lib/atmos/providers/aws/provider.rb +0 -51
  81. data/lib/atmos/providers/aws/s3_secret_manager.rb +0 -49
  82. data/lib/atmos/providers/aws/user_manager.rb +0 -211
  83. data/lib/atmos/settings_hash.rb +0 -90
  84. data/lib/atmos/terraform_executor.rb +0 -267
  85. data/lib/atmos/ui.rb +0 -159
  86. data/lib/atmos/utils.rb +0 -50
  87. data/lib/atmos/version.rb +0 -3
@@ -0,0 +1,118 @@
1
+ require_relative '../../../atmos'
2
+ require 'aws-sdk-ecs'
3
+ require 'aws-sdk-ecr'
4
+ require 'open3'
5
+
6
+ module SimplyGenius
7
+ module Atmos
8
+ module Providers
9
+ module Aws
10
+
11
+ class ContainerManager
12
+ include GemLogger::LoggerSupport
13
+
14
+ def initialize(provider)
15
+ @provider = provider
16
+ end
17
+
18
+ def push(ecs_name, local_image,
19
+ ecr_repo: ecs_name, revision: nil)
20
+
21
+ revision = Time.now.strftime('%Y%m%d%H%M%S') unless revision.present?
22
+ result = {}
23
+
24
+ ecr = ::Aws::ECR::Client.new
25
+ resp = nil
26
+
27
+ resp = ecr.get_authorization_token
28
+ auth_data = resp.authorization_data.first
29
+ token = auth_data.authorization_token
30
+ endpoint = auth_data.proxy_endpoint
31
+ user, password = Base64.decode64(token).split(':')
32
+
33
+ # docker login into the ECR repo for the current account so that we can pull/push to it
34
+ run("docker", "login", "-u", user, "-p", password, endpoint)#, stdin_data: token)
35
+
36
+ image="#{ecs_name}:latest"
37
+ ecs_image="#{endpoint.sub(/https?:\/\//, '')}/#{ecr_repo}"
38
+
39
+ tags = ['latest', revision]
40
+ logger.info "Tagging local image '#{local_image}' with #{tags}"
41
+ tags.each {|t| run("docker", "tag", local_image, "#{ecs_image}:#{t}") }
42
+
43
+ logger.info "Pushing tagged image to ECR repo"
44
+ tags.each {|t| run("docker", "push", "#{ecs_image}:#{t}") }
45
+
46
+ result[:remote_image] = "#{ecs_image}:#{revision}"
47
+ return result
48
+ end
49
+
50
+ def deploy_task(task, remote_image)
51
+ result = {}
52
+
53
+ ecs = ::Aws::ECS::Client.new
54
+ resp = nil
55
+
56
+ resp = ecs.list_task_definitions(family_prefix: task, sort: 'DESC')
57
+ latest_defn_arn = resp.task_definition_arns.first
58
+
59
+ logger.info "Latest task definition: #{latest_defn_arn}"
60
+
61
+ resp = ecs.describe_task_definition(task_definition: latest_defn_arn)
62
+ latest_defn = resp.task_definition
63
+
64
+ new_defn = latest_defn.to_h
65
+ [:revision, :status, :task_definition_arn,
66
+ :requires_attributes, :compatibilities].each do |attr|
67
+ new_defn.delete(attr)
68
+ end
69
+ new_defn[:container_definitions].each {|c| c[:image] = remote_image}
70
+
71
+ resp = ecs.register_task_definition(**new_defn)
72
+ result[:task_definition] = resp.task_definition.task_definition_arn
73
+
74
+ logger.info "Updated task=#{task} to #{result[:task_definition]} with image #{remote_image}"
75
+
76
+ return result
77
+ end
78
+
79
+ def deploy_service(cluster, service, remote_image)
80
+ result = {}
81
+
82
+ ecs = ::Aws::ECS::Client.new
83
+ resp = nil
84
+
85
+ # Get current task definition name from service
86
+ resp = ecs.describe_services(cluster: cluster, services: [service])
87
+ current_defn_arn = resp.services.first.task_definition
88
+ defn_name = current_defn_arn.split("/").last.split(":").first
89
+
90
+ logger.info "Current task definition (name=#{defn_name}): #{current_defn_arn}"
91
+ result = deploy_task(defn_name, remote_image)
92
+ new_taskdef = result[:task_definition]
93
+
94
+ logger.info "Updating service with new task definition: #{new_taskdef}"
95
+
96
+ resp = ecs.update_service(cluster: cluster, service: service, task_definition: new_taskdef)
97
+
98
+ logger.info "Updated service=#{service} on cluster=#{cluster} to #{new_taskdef} with image #{remote_image}"
99
+
100
+ return result
101
+ end
102
+
103
+ private
104
+
105
+ def run(*args, **opts)
106
+ logger.debug("Running: #{args}")
107
+ stdout, status = Open3.capture2e(ENV, *args, **opts)
108
+ logger.debug(stdout)
109
+ raise "Failed to run #{args}: #{stdout}" unless status.success?
110
+ return stdout
111
+ end
112
+
113
+ end
114
+
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,53 @@
1
+ require_relative '../../../atmos'
2
+
3
+ Dir.glob(File.join(__dir__, '*.rb')) do |f|
4
+ require_relative "#{File.basename(f).sub(/\.rb$/, "")}"
5
+ end
6
+
7
+ module SimplyGenius
8
+ module Atmos
9
+ module Providers
10
+ module Aws
11
+
12
+ class Provider
13
+ include GemLogger::LoggerSupport
14
+
15
+ def initialize(name)
16
+ @name = name
17
+ end
18
+
19
+ def auth_manager
20
+ @auth_manager ||= begin
21
+ AuthManager.new(self)
22
+ end
23
+ end
24
+
25
+ def user_manager
26
+ @user_manager ||= begin
27
+ UserManager.new(self)
28
+ end
29
+ end
30
+
31
+ def account_manager
32
+ @account_manager ||= begin
33
+ AccountManager.new(self)
34
+ end
35
+ end
36
+
37
+ def secret_manager
38
+ @secret_manager ||= begin
39
+ S3SecretManager.new(self)
40
+ end
41
+ end
42
+
43
+ def container_manager
44
+ @container_manager ||= begin
45
+ ContainerManager.new(self)
46
+ end
47
+ end
48
+ end
49
+
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,51 @@
1
+ require_relative '../../../atmos'
2
+ require 'aws-sdk-s3'
3
+
4
+ module SimplyGenius
5
+ module Atmos
6
+ module Providers
7
+ module Aws
8
+
9
+ class S3SecretManager
10
+ include GemLogger::LoggerSupport
11
+
12
+ def initialize(provider)
13
+ @provider = provider
14
+ logger.debug("Secrets config is: #{Atmos.config[:secret]}")
15
+ @bucket_name = Atmos.config[:secret][:bucket]
16
+ @encrypt = Atmos.config[:secret][:encrypt]
17
+ end
18
+
19
+ def set(key, value)
20
+ opts = {}
21
+ opts[:server_side_encryption] = "AES256" if @encrypt
22
+ bucket.object(key).put(body: value, **opts)
23
+ end
24
+
25
+ def get(key)
26
+ bucket.object(key).get.body.read
27
+ end
28
+
29
+ def delete(key)
30
+ bucket.object(key).delete
31
+ end
32
+
33
+ def to_h
34
+ Hash[bucket.objects.collect {|o|
35
+ [o.key, o.get.body.read]
36
+ }]
37
+ end
38
+
39
+ private
40
+
41
+ def bucket
42
+ raise ArgumentError.new("The s3 secret bucket is not set") unless @bucket_name
43
+ @bucket ||= ::Aws::S3::Bucket.new(@bucket_name)
44
+ end
45
+
46
+ end
47
+
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,213 @@
1
+ require_relative '../../../atmos'
2
+ require_relative '../../../atmos/otp'
3
+ require 'aws-sdk-iam'
4
+ require 'securerandom'
5
+
6
+ module SimplyGenius
7
+ module Atmos
8
+ module Providers
9
+ module Aws
10
+
11
+ class UserManager
12
+ include GemLogger::LoggerSupport
13
+ include FileUtils
14
+
15
+ def initialize(provider)
16
+ @provider = provider
17
+ end
18
+
19
+ def create_user(user_name)
20
+ result = {}
21
+ client = ::Aws::IAM::Client.new
22
+ resource = ::Aws::IAM::Resource.new
23
+
24
+ user = resource.user(user_name)
25
+
26
+ if user.exists?
27
+ logger.info "User '#{user_name}' already exists"
28
+ else
29
+ logger.info "Creating new user '#{user_name}'"
30
+ user = resource.create_user(user_name: user_name)
31
+ client.wait_until(:user_exists, user_name: user_name)
32
+ logger.debug "User created, user_name=#{user_name}"
33
+ end
34
+
35
+ result[:user_name] = user_name
36
+
37
+ return result
38
+ end
39
+
40
+ def set_groups(user_name, groups, force: false)
41
+ result = {}
42
+ resource = ::Aws::IAM::Resource.new
43
+
44
+ user = resource.user(user_name)
45
+
46
+ existing_groups = user.groups.collect(&:name)
47
+ groups_to_add = groups - existing_groups
48
+ groups_to_remove = existing_groups - groups
49
+
50
+ result[:groups] = existing_groups
51
+
52
+ groups_to_add.each do |group|
53
+ logger.debug "Adding group: #{group}"
54
+ user.add_group(group_name: group)
55
+ result[:groups] << group
56
+ end
57
+
58
+ if force
59
+ groups_to_remove.each do |group|
60
+ logger.debug "Removing group: #{group}"
61
+ user.remove_group(group_name: group)
62
+ result[:groups].delete(group)
63
+ end
64
+ end
65
+
66
+ logger.info "User associated with groups=#{result[:groups]}"
67
+
68
+ return result
69
+ end
70
+
71
+ def enable_login(user_name, force: false)
72
+ result = {}
73
+ resource = ::Aws::IAM::Resource.new
74
+
75
+ user = resource.user(user_name)
76
+
77
+ password = ""
78
+ classes = [/[a-z]/, /[A-Z]/, /[0-9]/, /[!@#$%^&*()_+\-=\[\]{}|']/]
79
+ while ! classes.all? {|c| password =~ c }
80
+ password = SecureRandom.base64(15)
81
+ end
82
+
83
+ exists = false
84
+ begin
85
+ user.login_profile.create_date
86
+ exists = true
87
+ rescue ::Aws::IAM::Errors::NoSuchEntity
88
+ exists = false
89
+ end
90
+
91
+ if exists
92
+ logger.info "User login already exists"
93
+ if force
94
+ user.login_profile.update(password: password, password_reset_required: true)
95
+ result[:password] = password
96
+ logger.info "Updated user login with password=#{password}"
97
+ end
98
+ else
99
+ user.create_login_profile(password: password, password_reset_required: true)
100
+ result[:password] = password
101
+ logger.info "User login enabled with password=#{password}"
102
+ end
103
+
104
+ return result
105
+ end
106
+
107
+ def enable_mfa(user_name, force: false)
108
+ result = {}
109
+ client = ::Aws::IAM::Client.new
110
+ resource = ::Aws::IAM::Resource.new
111
+
112
+ user = resource.user(user_name)
113
+
114
+ if user.mfa_devices.first
115
+ logger.info "User mfa devices already exist"
116
+ if force
117
+ logger.info "Deleting old mfa devices"
118
+ user.mfa_devices.each do |dev|
119
+ dev.disassociate
120
+ client.delete_virtual_mfa_device(serial_number: dev.serial_number)
121
+ Otp.instance.remove(user_name)
122
+ end
123
+ else
124
+ return result
125
+ end
126
+ end
127
+
128
+ resp = client.create_virtual_mfa_device(
129
+ virtual_mfa_device_name: user_name
130
+ )
131
+
132
+ serial = resp.virtual_mfa_device.serial_number
133
+ seed = resp.virtual_mfa_device.base_32_string_seed
134
+
135
+ Otp.instance.add(user_name, seed)
136
+ code1 = Otp.instance.generate(user_name)
137
+ interval = (30 - (Time.now.to_i % 30)) + 1
138
+ logger.info "Waiting for #{interval}s to generate second otp key for enablement"
139
+ sleep interval
140
+ code2 = Otp.instance.generate(user_name)
141
+ raise "MFA codes should not be the same" if code1 == code2
142
+
143
+ resp = client.enable_mfa_device({
144
+ user_name: user_name,
145
+ serial_number: serial,
146
+ authentication_code_1: code1,
147
+ authentication_code_2: code2,
148
+ })
149
+
150
+ result[:mfa_secret] = seed
151
+
152
+ return result
153
+ end
154
+
155
+ def enable_access_keys(user_name, force: false)
156
+ result = {}
157
+ resource = ::Aws::IAM::Resource.new
158
+
159
+ user = resource.user(user_name)
160
+
161
+ if user.access_keys.first
162
+ logger.info "User access keys already exist"
163
+ if force
164
+ logger.info "Deleting old access keys"
165
+ user.access_keys.each do |key|
166
+ key.delete
167
+ end
168
+ else
169
+ return result
170
+ end
171
+ end
172
+
173
+ # TODO: auto add to ~/.aws/credentials and config
174
+ key_pair = user.create_access_key_pair
175
+ result[:key] = key_pair.access_key_id
176
+ result[:secret] = key_pair.secret
177
+ logger.debug "User keys generated key=#{key_pair.access_key_id}, secret=#{key_pair.secret}"
178
+
179
+ return result
180
+ end
181
+
182
+ def set_public_key(user_name, public_key, force: false)
183
+ result = {}
184
+ client = ::Aws::IAM::Client.new
185
+ resource = ::Aws::IAM::Resource.new
186
+
187
+ user = resource.user(user_name)
188
+ keys = client.list_ssh_public_keys(user_name: user_name).ssh_public_keys
189
+ if keys.size > 0
190
+ logger.info "User ssh public keys already exist"
191
+ if force
192
+ logger.info "Deleting old ssh public keys"
193
+ keys.each do |key|
194
+ client.delete_ssh_public_key(user_name: user_name,
195
+ ssh_public_key_id: key.ssh_public_key_id)
196
+ end
197
+ else
198
+ return result
199
+ end
200
+ end
201
+
202
+ client.upload_ssh_public_key(user_name: user_name, ssh_public_key_body: public_key)
203
+ logger.debug "User public key assigned: #{public_key}"
204
+
205
+ return result
206
+ end
207
+
208
+ end
209
+
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,93 @@
1
+ require 'hashie'
2
+
3
+ module SimplyGenius
4
+ module Atmos
5
+
6
+ class SettingsHash < Hashie::Mash
7
+ include GemLogger::LoggerSupport
8
+ include Hashie::Extensions::DeepMerge
9
+ include Hashie::Extensions::DeepFetch
10
+ disable_warnings
11
+
12
+ PATH_PATTERN = /[\.\[\]]/
13
+
14
+ def notation_get(key)
15
+ path = key.to_s.split(PATH_PATTERN).compact
16
+ path = path.collect {|p| p =~ /^\d+$/ ? p.to_i : p }
17
+ result = nil
18
+
19
+ begin
20
+ result = deep_fetch(*path)
21
+ rescue Hashie::Extensions::DeepFetch::UndefinedPathError => e
22
+ logger.debug("Settings missing value for key='#{key}'")
23
+ end
24
+
25
+ return result
26
+ end
27
+
28
+ def notation_put(key, value, additive: true)
29
+ path = key.to_s.split(PATH_PATTERN).compact
30
+ path = path.collect {|p| p =~ /^\d+$/ ? p.to_i : p }
31
+ current_level = self
32
+ path.each_with_index do |p, i|
33
+
34
+ if i == path.size - 1
35
+ if additive && current_level[p].is_a?(Array)
36
+ current_level[p] = current_level[p] | Array(value)
37
+ else
38
+ current_level[p] = value
39
+ end
40
+ else
41
+ if current_level[p].nil?
42
+ if path[i+1].is_a?(Integer)
43
+ current_level[p] = []
44
+ else
45
+ current_level[p] = {}
46
+ end
47
+ end
48
+ end
49
+
50
+ current_level = current_level[p]
51
+ end
52
+ end
53
+
54
+ def self.add_config(yml_file, key, value, additive: true)
55
+ orig_config_with_comments = File.read(yml_file)
56
+
57
+ comment_places = {}
58
+ comment_lines = []
59
+ orig_config_with_comments.each_line do |line|
60
+ line.gsub!(/\s+$/, "\n")
61
+ if line =~ /^\s*(#.*)?$/
62
+ comment_lines << line
63
+ else
64
+ if comment_lines.present?
65
+ comment_places[line.chomp] = comment_lines
66
+ comment_lines = []
67
+ end
68
+ end
69
+ end
70
+ comment_places["<EOF>"] = comment_lines
71
+
72
+ orig_config = SettingsHash.new((YAML.load_file(yml_file) rescue {}))
73
+ orig_config.notation_put(key, value, additive: additive)
74
+ new_config_no_comments = YAML.dump(orig_config.to_hash)
75
+ new_config_no_comments.sub!(/\A---\n/, "")
76
+
77
+ new_yml = ""
78
+ new_config_no_comments.each_line do |line|
79
+ line.gsub!(/\s+$/, "\n")
80
+ cline = comment_places.keys.find {|k| line =~ /^#{k}/ }
81
+ comments = comment_places[cline]
82
+ comments.each {|comment| new_yml << comment } if comments
83
+ new_yml << line
84
+ end
85
+ comment_places["<EOF>"].each {|comment| new_yml << comment }
86
+
87
+ return new_yml
88
+ end
89
+
90
+ end
91
+
92
+ end
93
+ end