simplygenius-atmos 0.7.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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