cloud-mu 2.1.0beta → 3.0.0beta

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 (291) hide show
  1. checksums.yaml +5 -5
  2. data/Berksfile +4 -5
  3. data/Berksfile.lock +179 -0
  4. data/README.md +1 -6
  5. data/ansible/roles/geerlingguy.firewall/templates/firewall.bash.j2 +0 -0
  6. data/ansible/roles/mu-installer/README.md +33 -0
  7. data/ansible/roles/mu-installer/defaults/main.yml +2 -0
  8. data/ansible/roles/mu-installer/handlers/main.yml +2 -0
  9. data/ansible/roles/mu-installer/meta/main.yml +60 -0
  10. data/ansible/roles/mu-installer/tasks/main.yml +13 -0
  11. data/ansible/roles/mu-installer/tests/inventory +2 -0
  12. data/ansible/roles/mu-installer/tests/test.yml +5 -0
  13. data/ansible/roles/mu-installer/vars/main.yml +2 -0
  14. data/bin/mu-adopt +125 -0
  15. data/bin/mu-aws-setup +4 -4
  16. data/bin/mu-azure-setup +265 -0
  17. data/bin/mu-azure-tests +43 -0
  18. data/bin/mu-cleanup +20 -8
  19. data/bin/mu-configure +224 -98
  20. data/bin/mu-deploy +8 -3
  21. data/bin/mu-gcp-setup +16 -8
  22. data/bin/mu-gen-docs +92 -8
  23. data/bin/mu-load-config.rb +52 -12
  24. data/bin/mu-momma-cat +36 -0
  25. data/bin/mu-node-manage +34 -27
  26. data/bin/mu-self-update +2 -2
  27. data/bin/mu-ssh +12 -8
  28. data/bin/mu-upload-chef-artifacts +11 -4
  29. data/bin/mu-user-manage +3 -0
  30. data/cloud-mu.gemspec +8 -11
  31. data/cookbooks/firewall/libraries/helpers_iptables.rb +2 -2
  32. data/cookbooks/firewall/metadata.json +1 -1
  33. data/cookbooks/firewall/recipes/default.rb +5 -9
  34. data/cookbooks/mu-firewall/attributes/default.rb +2 -0
  35. data/cookbooks/mu-firewall/metadata.rb +1 -1
  36. data/cookbooks/mu-glusterfs/templates/default/mu-gluster-client.erb +0 -0
  37. data/cookbooks/mu-master/Berksfile +2 -2
  38. data/cookbooks/mu-master/files/default/check_mem.pl +0 -0
  39. data/cookbooks/mu-master/files/default/cloudamatic.png +0 -0
  40. data/cookbooks/mu-master/metadata.rb +5 -4
  41. data/cookbooks/mu-master/recipes/389ds.rb +1 -1
  42. data/cookbooks/mu-master/recipes/basepackages.rb +30 -10
  43. data/cookbooks/mu-master/recipes/default.rb +59 -7
  44. data/cookbooks/mu-master/recipes/firewall-holes.rb +1 -1
  45. data/cookbooks/mu-master/recipes/init.rb +65 -47
  46. data/cookbooks/mu-master/recipes/{eks-kubectl.rb → kubectl.rb} +4 -10
  47. data/cookbooks/mu-master/recipes/sssd.rb +2 -1
  48. data/cookbooks/mu-master/recipes/update_nagios_only.rb +6 -6
  49. data/cookbooks/mu-master/templates/default/web_app.conf.erb +2 -2
  50. data/cookbooks/mu-master/templates/mods/ldap.conf.erb +4 -0
  51. data/cookbooks/mu-php54/Berksfile +1 -2
  52. data/cookbooks/mu-php54/metadata.rb +4 -5
  53. data/cookbooks/mu-php54/recipes/default.rb +1 -1
  54. data/cookbooks/mu-splunk/templates/default/splunk-init.erb +0 -0
  55. data/cookbooks/mu-tools/Berksfile +3 -2
  56. data/cookbooks/mu-tools/files/default/Mu_CA.pem +33 -0
  57. data/cookbooks/mu-tools/libraries/helper.rb +20 -8
  58. data/cookbooks/mu-tools/metadata.rb +5 -2
  59. data/cookbooks/mu-tools/recipes/apply_security.rb +2 -3
  60. data/cookbooks/mu-tools/recipes/eks.rb +1 -1
  61. data/cookbooks/mu-tools/recipes/gcloud.rb +5 -30
  62. data/cookbooks/mu-tools/recipes/nagios.rb +1 -1
  63. data/cookbooks/mu-tools/recipes/rsyslog.rb +1 -0
  64. data/cookbooks/mu-tools/recipes/selinux.rb +19 -0
  65. data/cookbooks/mu-tools/recipes/split_var_partitions.rb +0 -1
  66. data/cookbooks/mu-tools/recipes/windows-client.rb +256 -122
  67. data/cookbooks/mu-tools/resources/disk.rb +3 -1
  68. data/cookbooks/mu-tools/templates/amazon/sshd_config.erb +1 -1
  69. data/cookbooks/mu-tools/templates/default/etc_hosts.erb +1 -1
  70. data/cookbooks/mu-tools/templates/default/{kubeconfig.erb → kubeconfig-eks.erb} +0 -0
  71. data/cookbooks/mu-tools/templates/default/kubeconfig-gke.erb +27 -0
  72. data/cookbooks/mu-tools/templates/windows-10/sshd_config.erb +137 -0
  73. data/cookbooks/mu-utility/recipes/nat.rb +4 -0
  74. data/extras/alpha.png +0 -0
  75. data/extras/beta.png +0 -0
  76. data/extras/clean-stock-amis +2 -2
  77. data/extras/generate-stock-images +131 -0
  78. data/extras/git-fix-permissions-hook +0 -0
  79. data/extras/image-generators/AWS/centos6.yaml +17 -0
  80. data/extras/image-generators/{aws → AWS}/centos7-govcloud.yaml +0 -0
  81. data/extras/image-generators/{aws → AWS}/centos7.yaml +0 -0
  82. data/extras/image-generators/{aws → AWS}/rhel7.yaml +0 -0
  83. data/extras/image-generators/{aws → AWS}/win2k12.yaml +0 -0
  84. data/extras/image-generators/{aws → AWS}/win2k16.yaml +0 -0
  85. data/extras/image-generators/{aws → AWS}/windows.yaml +0 -0
  86. data/extras/image-generators/{gcp → Google}/centos6.yaml +1 -0
  87. data/extras/image-generators/Google/centos7.yaml +18 -0
  88. data/extras/python_rpm/build.sh +0 -0
  89. data/extras/release.png +0 -0
  90. data/extras/ruby_rpm/build.sh +0 -0
  91. data/extras/ruby_rpm/muby.spec +1 -1
  92. data/install/README.md +43 -5
  93. data/install/deprecated-bash-library.sh +0 -0
  94. data/install/installer +1 -1
  95. data/install/jenkinskeys.rb +0 -0
  96. data/install/mu-master.yaml +55 -0
  97. data/modules/mommacat.ru +41 -7
  98. data/modules/mu.rb +444 -149
  99. data/modules/mu/adoption.rb +500 -0
  100. data/modules/mu/cleanup.rb +235 -158
  101. data/modules/mu/cloud.rb +675 -138
  102. data/modules/mu/clouds/aws.rb +156 -24
  103. data/modules/mu/clouds/aws/alarm.rb +4 -14
  104. data/modules/mu/clouds/aws/bucket.rb +60 -18
  105. data/modules/mu/clouds/aws/cache_cluster.rb +8 -20
  106. data/modules/mu/clouds/aws/collection.rb +12 -22
  107. data/modules/mu/clouds/aws/container_cluster.rb +209 -118
  108. data/modules/mu/clouds/aws/database.rb +120 -45
  109. data/modules/mu/clouds/aws/dnszone.rb +7 -18
  110. data/modules/mu/clouds/aws/endpoint.rb +5 -15
  111. data/modules/mu/clouds/aws/firewall_rule.rb +144 -72
  112. data/modules/mu/clouds/aws/folder.rb +4 -11
  113. data/modules/mu/clouds/aws/function.rb +6 -16
  114. data/modules/mu/clouds/aws/group.rb +4 -12
  115. data/modules/mu/clouds/aws/habitat.rb +11 -13
  116. data/modules/mu/clouds/aws/loadbalancer.rb +40 -28
  117. data/modules/mu/clouds/aws/log.rb +5 -13
  118. data/modules/mu/clouds/aws/msg_queue.rb +9 -24
  119. data/modules/mu/clouds/aws/nosqldb.rb +4 -12
  120. data/modules/mu/clouds/aws/notifier.rb +6 -13
  121. data/modules/mu/clouds/aws/role.rb +69 -40
  122. data/modules/mu/clouds/aws/search_domain.rb +17 -20
  123. data/modules/mu/clouds/aws/server.rb +184 -94
  124. data/modules/mu/clouds/aws/server_pool.rb +33 -38
  125. data/modules/mu/clouds/aws/storage_pool.rb +5 -12
  126. data/modules/mu/clouds/aws/user.rb +59 -33
  127. data/modules/mu/clouds/aws/userdata/linux.erb +18 -30
  128. data/modules/mu/clouds/aws/userdata/windows.erb +9 -9
  129. data/modules/mu/clouds/aws/vpc.rb +214 -145
  130. data/modules/mu/clouds/azure.rb +978 -44
  131. data/modules/mu/clouds/azure/container_cluster.rb +413 -0
  132. data/modules/mu/clouds/azure/firewall_rule.rb +500 -0
  133. data/modules/mu/clouds/azure/habitat.rb +167 -0
  134. data/modules/mu/clouds/azure/loadbalancer.rb +205 -0
  135. data/modules/mu/clouds/azure/role.rb +211 -0
  136. data/modules/mu/clouds/azure/server.rb +810 -0
  137. data/modules/mu/clouds/azure/user.rb +257 -0
  138. data/modules/mu/clouds/azure/userdata/README.md +4 -0
  139. data/modules/mu/clouds/azure/userdata/linux.erb +137 -0
  140. data/modules/mu/clouds/azure/userdata/windows.erb +275 -0
  141. data/modules/mu/clouds/azure/vpc.rb +782 -0
  142. data/modules/mu/clouds/cloudformation.rb +12 -9
  143. data/modules/mu/clouds/cloudformation/firewall_rule.rb +5 -13
  144. data/modules/mu/clouds/cloudformation/server.rb +10 -1
  145. data/modules/mu/clouds/cloudformation/server_pool.rb +1 -0
  146. data/modules/mu/clouds/cloudformation/vpc.rb +0 -2
  147. data/modules/mu/clouds/google.rb +554 -117
  148. data/modules/mu/clouds/google/bucket.rb +173 -32
  149. data/modules/mu/clouds/google/container_cluster.rb +1112 -157
  150. data/modules/mu/clouds/google/database.rb +24 -47
  151. data/modules/mu/clouds/google/firewall_rule.rb +344 -89
  152. data/modules/mu/clouds/google/folder.rb +156 -79
  153. data/modules/mu/clouds/google/group.rb +272 -82
  154. data/modules/mu/clouds/google/habitat.rb +177 -52
  155. data/modules/mu/clouds/google/loadbalancer.rb +9 -34
  156. data/modules/mu/clouds/google/role.rb +1211 -0
  157. data/modules/mu/clouds/google/server.rb +491 -227
  158. data/modules/mu/clouds/google/server_pool.rb +233 -48
  159. data/modules/mu/clouds/google/user.rb +479 -125
  160. data/modules/mu/clouds/google/userdata/linux.erb +3 -3
  161. data/modules/mu/clouds/google/userdata/windows.erb +9 -9
  162. data/modules/mu/clouds/google/vpc.rb +381 -223
  163. data/modules/mu/config.rb +689 -214
  164. data/modules/mu/config/bucket.rb +1 -1
  165. data/modules/mu/config/cache_cluster.rb +1 -1
  166. data/modules/mu/config/cache_cluster.yml +0 -4
  167. data/modules/mu/config/container_cluster.rb +18 -9
  168. data/modules/mu/config/database.rb +6 -23
  169. data/modules/mu/config/firewall_rule.rb +9 -15
  170. data/modules/mu/config/folder.rb +22 -21
  171. data/modules/mu/config/habitat.rb +22 -21
  172. data/modules/mu/config/loadbalancer.rb +2 -2
  173. data/modules/mu/config/role.rb +9 -40
  174. data/modules/mu/config/server.rb +26 -5
  175. data/modules/mu/config/server_pool.rb +1 -1
  176. data/modules/mu/config/storage_pool.rb +2 -2
  177. data/modules/mu/config/user.rb +4 -0
  178. data/modules/mu/config/vpc.rb +350 -110
  179. data/modules/mu/defaults/{amazon_images.yaml → AWS.yaml} +37 -39
  180. data/modules/mu/defaults/Azure.yaml +17 -0
  181. data/modules/mu/defaults/Google.yaml +24 -0
  182. data/modules/mu/defaults/README.md +1 -1
  183. data/modules/mu/deploy.rb +168 -125
  184. data/modules/mu/groomer.rb +2 -1
  185. data/modules/mu/groomers/ansible.rb +104 -32
  186. data/modules/mu/groomers/chef.rb +96 -44
  187. data/modules/mu/kittens.rb +20602 -0
  188. data/modules/mu/logger.rb +38 -11
  189. data/modules/mu/master.rb +90 -8
  190. data/modules/mu/master/chef.rb +2 -3
  191. data/modules/mu/master/ldap.rb +0 -1
  192. data/modules/mu/master/ssl.rb +250 -0
  193. data/modules/mu/mommacat.rb +917 -513
  194. data/modules/scratchpad.erb +1 -1
  195. data/modules/tests/super_complex_bok.yml +0 -0
  196. data/modules/tests/super_simple_bok.yml +0 -0
  197. data/roles/mu-master.json +2 -1
  198. data/spec/azure_creds +5 -0
  199. data/spec/mu.yaml +56 -0
  200. data/spec/mu/clouds/azure_spec.rb +164 -27
  201. data/spec/spec_helper.rb +5 -0
  202. data/test/clean_up.py +0 -0
  203. data/test/exec_inspec.py +0 -0
  204. data/test/exec_mu_install.py +0 -0
  205. data/test/exec_retry.py +0 -0
  206. data/test/smoke_test.rb +0 -0
  207. metadata +90 -118
  208. data/cookbooks/mu-jenkins/Berksfile +0 -14
  209. data/cookbooks/mu-jenkins/CHANGELOG.md +0 -13
  210. data/cookbooks/mu-jenkins/LICENSE +0 -37
  211. data/cookbooks/mu-jenkins/README.md +0 -105
  212. data/cookbooks/mu-jenkins/attributes/default.rb +0 -42
  213. data/cookbooks/mu-jenkins/files/default/cleanup_deploy_config.xml +0 -73
  214. data/cookbooks/mu-jenkins/files/default/deploy_config.xml +0 -44
  215. data/cookbooks/mu-jenkins/metadata.rb +0 -21
  216. data/cookbooks/mu-jenkins/recipes/default.rb +0 -195
  217. data/cookbooks/mu-jenkins/recipes/node-ssh-config.rb +0 -54
  218. data/cookbooks/mu-jenkins/recipes/public_key.rb +0 -24
  219. data/cookbooks/mu-jenkins/templates/default/example_job.config.xml.erb +0 -24
  220. data/cookbooks/mu-jenkins/templates/default/org.jvnet.hudson.plugins.SSHBuildWrapper.xml.erb +0 -14
  221. data/cookbooks/mu-jenkins/templates/default/ssh_config.erb +0 -6
  222. data/cookbooks/nagios/Berksfile +0 -11
  223. data/cookbooks/nagios/CHANGELOG.md +0 -589
  224. data/cookbooks/nagios/CONTRIBUTING.md +0 -11
  225. data/cookbooks/nagios/LICENSE +0 -37
  226. data/cookbooks/nagios/README.md +0 -328
  227. data/cookbooks/nagios/TESTING.md +0 -2
  228. data/cookbooks/nagios/attributes/config.rb +0 -171
  229. data/cookbooks/nagios/attributes/default.rb +0 -228
  230. data/cookbooks/nagios/chefignore +0 -102
  231. data/cookbooks/nagios/definitions/command.rb +0 -33
  232. data/cookbooks/nagios/definitions/contact.rb +0 -33
  233. data/cookbooks/nagios/definitions/contactgroup.rb +0 -33
  234. data/cookbooks/nagios/definitions/host.rb +0 -33
  235. data/cookbooks/nagios/definitions/hostdependency.rb +0 -33
  236. data/cookbooks/nagios/definitions/hostescalation.rb +0 -34
  237. data/cookbooks/nagios/definitions/hostgroup.rb +0 -33
  238. data/cookbooks/nagios/definitions/nagios_conf.rb +0 -38
  239. data/cookbooks/nagios/definitions/resource.rb +0 -33
  240. data/cookbooks/nagios/definitions/service.rb +0 -33
  241. data/cookbooks/nagios/definitions/servicedependency.rb +0 -33
  242. data/cookbooks/nagios/definitions/serviceescalation.rb +0 -34
  243. data/cookbooks/nagios/definitions/servicegroup.rb +0 -33
  244. data/cookbooks/nagios/definitions/timeperiod.rb +0 -33
  245. data/cookbooks/nagios/libraries/base.rb +0 -314
  246. data/cookbooks/nagios/libraries/command.rb +0 -91
  247. data/cookbooks/nagios/libraries/contact.rb +0 -230
  248. data/cookbooks/nagios/libraries/contactgroup.rb +0 -112
  249. data/cookbooks/nagios/libraries/custom_option.rb +0 -36
  250. data/cookbooks/nagios/libraries/data_bag_helper.rb +0 -23
  251. data/cookbooks/nagios/libraries/default.rb +0 -90
  252. data/cookbooks/nagios/libraries/host.rb +0 -412
  253. data/cookbooks/nagios/libraries/hostdependency.rb +0 -181
  254. data/cookbooks/nagios/libraries/hostescalation.rb +0 -173
  255. data/cookbooks/nagios/libraries/hostgroup.rb +0 -119
  256. data/cookbooks/nagios/libraries/nagios.rb +0 -282
  257. data/cookbooks/nagios/libraries/resource.rb +0 -59
  258. data/cookbooks/nagios/libraries/service.rb +0 -455
  259. data/cookbooks/nagios/libraries/servicedependency.rb +0 -215
  260. data/cookbooks/nagios/libraries/serviceescalation.rb +0 -195
  261. data/cookbooks/nagios/libraries/servicegroup.rb +0 -144
  262. data/cookbooks/nagios/libraries/timeperiod.rb +0 -160
  263. data/cookbooks/nagios/libraries/users_helper.rb +0 -54
  264. data/cookbooks/nagios/metadata.rb +0 -25
  265. data/cookbooks/nagios/recipes/_load_databag_config.rb +0 -153
  266. data/cookbooks/nagios/recipes/_load_default_config.rb +0 -241
  267. data/cookbooks/nagios/recipes/apache.rb +0 -48
  268. data/cookbooks/nagios/recipes/default.rb +0 -204
  269. data/cookbooks/nagios/recipes/nginx.rb +0 -82
  270. data/cookbooks/nagios/recipes/pagerduty.rb +0 -143
  271. data/cookbooks/nagios/recipes/server_package.rb +0 -40
  272. data/cookbooks/nagios/recipes/server_source.rb +0 -164
  273. data/cookbooks/nagios/templates/default/apache2.conf.erb +0 -96
  274. data/cookbooks/nagios/templates/default/cgi.cfg.erb +0 -266
  275. data/cookbooks/nagios/templates/default/commands.cfg.erb +0 -13
  276. data/cookbooks/nagios/templates/default/contacts.cfg.erb +0 -37
  277. data/cookbooks/nagios/templates/default/hostgroups.cfg.erb +0 -25
  278. data/cookbooks/nagios/templates/default/hosts.cfg.erb +0 -15
  279. data/cookbooks/nagios/templates/default/htpasswd.users.erb +0 -6
  280. data/cookbooks/nagios/templates/default/nagios.cfg.erb +0 -22
  281. data/cookbooks/nagios/templates/default/nginx.conf.erb +0 -62
  282. data/cookbooks/nagios/templates/default/pagerduty.cgi.erb +0 -185
  283. data/cookbooks/nagios/templates/default/resource.cfg.erb +0 -27
  284. data/cookbooks/nagios/templates/default/servicedependencies.cfg.erb +0 -15
  285. data/cookbooks/nagios/templates/default/servicegroups.cfg.erb +0 -14
  286. data/cookbooks/nagios/templates/default/services.cfg.erb +0 -14
  287. data/cookbooks/nagios/templates/default/templates.cfg.erb +0 -31
  288. data/cookbooks/nagios/templates/default/timeperiods.cfg.erb +0 -13
  289. data/extras/image-generators/aws/centos6.yaml +0 -18
  290. data/modules/mu/defaults/google_images.yaml +0 -16
  291. data/roles/mu-master-jenkins.json +0 -24
@@ -17,27 +17,19 @@ module MU
17
17
  class Google
18
18
  # Creates an Google project as configured in {MU::Config::BasketofKittens::habitats}
19
19
  class Habitat < MU::Cloud::Habitat
20
- @deploy = nil
21
- @config = nil
22
-
23
- attr_reader :mu_name
24
- attr_reader :config
25
- attr_reader :cloud_id
26
-
27
- # @param mommacat [MU::MommaCat]: A {MU::Mommacat} object containing the deploy of which this resource is/will be a member.
28
- # @param kitten_cfg [Hash]: The fully parsed and resolved {MU::Config} resource descriptor as defined in {MU::Config::BasketofKittens::habitats}
29
- def initialize(mommacat: nil, kitten_cfg: nil, mu_name: nil, cloud_id: nil)
30
- @deploy = mommacat
31
- @config = MU::Config.manxify(kitten_cfg)
32
- @cloud_id ||= cloud_id
33
-
34
- if !mu_name.nil?
35
- @mu_name = mu_name
36
- elsif @config['scrub_mu_isms']
37
- @mu_name = @config['name']
38
- else
39
- @mu_name = @deploy.getResourceName(@config['name'])
20
+
21
+ # Initialize this cloud resource object. Calling +super+ will invoke the initializer defined under {MU::Cloud}, which should set the attribtues listed in {MU::Cloud::PUBLIC_ATTRS} as well as applicable dependency shortcuts, like <tt>@vpc</tt>, for us.
22
+ # @param args [Hash]: Hash of named arguments passed via Ruby's double-splat
23
+ def initialize(**args)
24
+ super
25
+ cloud_desc if @cloud_id # XXX maybe this isn't my job
26
+
27
+ # XXX this definitely isn't my job
28
+ if !@cloud_id and cloud_desc and cloud_desc.project_id
29
+ @cloud_id = cloud_desc.project_id
40
30
  end
31
+
32
+ @mu_name ||= @deploy.getResourceName(@config['name'])
41
33
  end
42
34
 
43
35
  # Called automatically by {MU::Deploy#createResources}
@@ -47,12 +39,13 @@ module MU
47
39
  name_string = if @config['scrub_mu_isms']
48
40
  @config["name"]
49
41
  else
50
- @deploy.getResourceName(@config["name"], max_length: 30).downcase
42
+ @deploy.getResourceName(@config["name"], max_length: 30)
51
43
  end
44
+ display_name = @config['display_name'] || name_string.gsub(/[^a-z0-9\-'"\s!]/i, "-")
52
45
 
53
46
  params = {
54
- name: name_string,
55
- project_id: name_string,
47
+ name: display_name,
48
+ project_id: name_string.downcase.gsub(/[^0-9a-z\-]/, "-")
56
49
  }
57
50
 
58
51
  MU::MommaCat.listStandardTags.each_pair { |name, value|
@@ -65,6 +58,9 @@ module MU
65
58
  params[:labels] = labels
66
59
  end
67
60
 
61
+ if @config['parent']['name'] and !@config['parent']['id']
62
+ @config['parent']['deploy_id'] = @deploy.deploy_id
63
+ end
68
64
  parent = MU::Cloud::Google::Folder.resolveParent(@config['parent'], credentials: @config['credentials'])
69
65
  if !parent
70
66
  MU.log "Unable to resolve parent resource of Google Project #{@config['name']}", MU::ERR, details: @config['parent']
@@ -79,22 +75,31 @@ module MU
79
75
 
80
76
  project_obj = MU::Cloud::Google.resource_manager(:Project).new(params)
81
77
 
82
- MU.log "Creating project #{name_string} under #{parent}", details: project_obj
83
- MU::Cloud::Google.resource_manager(credentials: @config['credentials']).create_project(project_obj)
78
+ MU.log "Creating project #{params[:project_id]} (#{params[:name]}) under #{parent} (#{@config['credentials']})", details: project_obj
79
+
80
+ begin
81
+ pp MU::Cloud::Google.resource_manager(credentials: @config['credentials']).create_project(project_obj)
82
+ rescue ::Google::Apis::ClientError => e
83
+ MU.log "Got #{e.message} attempting to create #{params[:project_id]}", MU::ERR, details: project_obj
84
+ end
84
85
 
85
86
 
86
87
  found = false
87
88
  retries = 0
88
89
  begin
89
- resp = MU::Cloud::Google.resource_manager(credentials: credentials).list_projects
90
+ # can... can we filter this?
91
+ resp = MU::Cloud::Google.resource_manager(credentials: credentials).list_projects(filter: "id:#{name_string.downcase.gsub(/[^0-9a-z\-]/, "-")}")
90
92
  if resp and resp.projects
91
93
  resp.projects.each { |p|
92
- if p.name == name_string.downcase
94
+ if p.project_id == name_string.downcase.gsub(/[^0-9a-z\-]/, "-")
93
95
  found = true
94
96
  end
95
97
  }
96
98
  end
97
99
  if !found
100
+ if retries > 30
101
+ raise MuError, "Project #{name_string} never showed up in list_projects after I created it!"
102
+ end
98
103
  if retries > 0 and (retries % 3) == 0
99
104
  MU.log "Waiting for Google Cloud project #{name_string} to appear in list_projects results...", MU::NOTICE
100
105
  end
@@ -104,8 +109,16 @@ module MU
104
109
  end while !found
105
110
 
106
111
 
107
- @cloud_id = name_string.downcase
108
- setProjectBilling
112
+ @cloud_id = params[:project_id]
113
+ @habitat_id = parent_id
114
+ begin
115
+ setProjectBilling
116
+ rescue Exception => e
117
+ MU.log "Failed to set billing account #{@config['billing_acct']} on project #{@cloud_id}: #{e.message}", MU::ERR
118
+ MU::Cloud::Google.resource_manager(credentials: @config['credentials']).delete_project(@cloud_id)
119
+ raise e
120
+ end
121
+ MU.log "Project #{params[:project_id]} (#{params[:name]}) created"
109
122
  end
110
123
 
111
124
  # Called automatically by {MU::Deploy#createResources}
@@ -113,6 +126,18 @@ module MU
113
126
  setProjectBilling
114
127
  end
115
128
 
129
+ # Retrieve the IAM bindings for this project (associates between IAM roles and groups/users)
130
+ def bindings
131
+ MU::Cloud::Google::Habitat.bindings(@cloud_id, credentials: @config['credentials'])
132
+ end
133
+
134
+ # Retrieve the IAM bindings for this project (associates between IAM roles and groups/users)
135
+ # @param project [String]:
136
+ # @param credentials [String]:
137
+ def self.bindings(project, credentials: nil)
138
+ MU::Cloud::Google.resource_manager(credentials: credentials).get_project_iam_policy(project).bindings
139
+ end
140
+
116
141
  # Associate a billing account with this project. If none is specified in
117
142
  # our configuration, use the billing account tied the the default
118
143
  # project of our credential set.
@@ -130,17 +155,26 @@ module MU
130
155
  project_id: @cloud_id
131
156
  )
132
157
  MU.log "Associating project #{@cloud_id} with billing account #{@config['billing_acct']}"
133
- MU::Cloud::Google.billing(credentials: credentials).update_project_billing_info(
134
- "projects/"+@cloud_id,
135
- billing_obj
136
- )
158
+ begin
159
+ MU::Cloud::Google.billing(credentials: credentials).update_project_billing_info(
160
+ "projects/"+@cloud_id,
161
+ billing_obj
162
+ )
163
+ rescue ::Google::Apis::ClientError => e
164
+ MU.log "Error setting billing for #{@cloud_id}: "+e.message, MU::ERR, details: billing_obj
165
+ end
137
166
 
138
167
  end
139
168
  end
140
169
 
141
170
  # Return the cloud descriptor for the Habitat
171
+ # @return [Google::Apis::Core::Hashable]
142
172
  def cloud_desc
143
- MU::Cloud::Google::Habitat.find(cloud_id: @cloud_id).values.first
173
+ @cached_cloud_desc ||= MU::Cloud::Google::Habitat.find(cloud_id: @cloud_id).values.first
174
+ if @cached_cloud_desc and @cached_cloud_desc.parent
175
+ @habitat_id ||= @cached_cloud_desc.parent.id
176
+ end
177
+ @cached_cloud_desc
144
178
  end
145
179
 
146
180
  # Return the metadata for this project's configuration
@@ -159,7 +193,27 @@ module MU
159
193
  # Denote whether this resource implementation is experiment, ready for
160
194
  # testing, or ready for production use.
161
195
  def self.quality
162
- MU::Cloud::BETA
196
+ MU::Cloud::RELEASE
197
+ end
198
+
199
+ # Check whether is in the +ACTIVE+ state and has billing enabled.
200
+ # @param project_id [String]
201
+ # @return [Boolean]
202
+ def self.isLive?(project_id, credentials = nil)
203
+ project = MU::Cloud::Google::Habitat.find(cloud_id: project_id, credentials: credentials).values.first
204
+ return false if project.nil? or project.lifecycle_state != "ACTIVE"
205
+
206
+ begin
207
+ billing = MU::Cloud::Google.billing(credentials: credentials).get_project_billing_info("projects/"+project_id)
208
+ if !billing or !billing.billing_account_name or
209
+ billing.billing_account_name.empty?
210
+ return false
211
+ end
212
+ rescue ::Google::Apis::ClientError => e
213
+ return false
214
+ end
215
+
216
+ true
163
217
  end
164
218
 
165
219
  # Remove all Google projects associated with the currently loaded deployment. Try to, anyway.
@@ -173,15 +227,16 @@ module MU
173
227
  resp.projects.each { |p|
174
228
  if p.labels and p.labels["mu-id"] == MU.deploy_id.downcase and
175
229
  p.lifecycle_state == "ACTIVE"
176
- MU.log "Deleting project #{p.name}", details: p
230
+ MU.log "Deleting project #{p.project_id} (#{p.name})", details: p
177
231
  if !noop
178
232
  begin
179
- MU::Cloud::Google.resource_manager(credentials: credentials).delete_project(p.name)
233
+ MU::Cloud::Google.resource_manager(credentials: credentials).delete_project(p.project_id)
180
234
  rescue ::Google::Apis::ClientError => e
181
235
  if e.message.match(/Cannot delete an inactive project/)
182
236
  # this is fine
183
237
  else
184
- raise e
238
+ MU.log "Got #{e.message} trying to delete project #{p.project_id} (#{p.name})", MU::ERR
239
+ next
185
240
  end
186
241
  end
187
242
  end
@@ -190,28 +245,94 @@ module MU
190
245
  end
191
246
  end
192
247
 
248
+ @@list_projects_cache = nil
249
+
193
250
  # Locate an existing project
194
- # @param cloud_id [String]: The cloud provider's identifier for this resource.
195
- # @param region [String]: The cloud provider region.
196
- # @param flags [Hash]: Optional flags
197
- # @return [OpenStruct]: The cloud provider's complete descriptions of matching project
198
- def self.find(cloud_id: nil, region: MU.curRegion, credentials: nil, flags: {}, tag_key: nil, tag_value: nil)
251
+ # @return [Hash<OpenStruct>]: The cloud provider's complete descriptions of matching project
252
+ def self.find(**args)
253
+ args[:project] ||= args[:habitat]
254
+ args[:cloud_id] ||= args[:project]
255
+ #MU.log "habitat.find called by #{caller[0]}", MU::WARN, details: args
199
256
  found = {}
200
- if cloud_id
201
- resp = MU::Cloud::Google.resource_manager(credentials: credentials).list_projects(
202
- filter: "name:#{cloud_id}"
257
+
258
+ # XXX we probably want to cache this
259
+ # XXX but why are we being called over and over?
260
+
261
+ if args[:cloud_id]
262
+ resp = MU::Cloud::Google.resource_manager(credentials: args[:credentials]).list_projects(
263
+ filter: "id:#{args[:cloud_id]}"
203
264
  )
204
- found[resp.name] = resp.projects.first if resp and resp.projects
265
+
266
+ if resp and resp.projects and resp.projects.size == 1
267
+ found[args[:cloud_id]] = resp.projects.first if resp and resp.projects
268
+ else
269
+ # it's loony that there's no filter for project_number
270
+ resp = MU::Cloud::Google.resource_manager(credentials: args[:credentials]).list_projects
271
+ resp.projects.each { |p|
272
+ if p.project_number.to_s == args[:cloud_id].to_s
273
+ found[args[:cloud_id]] = p
274
+ break
275
+ end
276
+ }
277
+ end
205
278
  else
206
- resp = MU::Cloud::Google.resource_manager(credentials: credentials).list_projects().projects
207
- resp.each { |p|
208
- found[p.name] = p
279
+ return @@list_projects_cache if @@list_projects_cache # XXX decide on stale-ness after time or something
280
+ resp = MU::Cloud::Google.resource_manager(credentials: args[:credentials]).list_projects#(page_token: page_token)
281
+ resp.projects.each { |p|
282
+ next if p.lifecycle_state == "DELETE_REQUESTED"
283
+ found[p.project_id] = p
209
284
  }
285
+ @@list_projects_cache = found
210
286
  end
211
-
287
+
212
288
  found
213
289
  end
214
290
 
291
+ # Reverse-map our cloud description into a runnable config hash.
292
+ # We assume that any values we have in +@config+ are placeholders, and
293
+ # calculate our own accordingly based on what's live in the cloud.
294
+ def toKitten(rootparent: nil, billing: nil, habitats: nil)
295
+ bok = {
296
+ "cloud" => "Google",
297
+ "credentials" => @config['credentials']
298
+ }
299
+
300
+ bok['name'] = cloud_desc.project_id
301
+ bok['cloud_id'] = cloud_desc.project_id
302
+ # if cloud_desc.name != cloud_desc.project_id
303
+ bok['display_name'] = cloud_desc.name
304
+ # end
305
+
306
+ if cloud_desc.parent and cloud_desc.parent.id
307
+ if cloud_desc.parent.type == "folder"
308
+ bok['parent'] = MU::Config::Ref.get(
309
+ id: "folders/"+cloud_desc.parent.id, # honestly, Google, make up your mind about your identifiers
310
+ cloud: "Google",
311
+ credentials: @config['credentials'],
312
+ type: "folders"
313
+ )
314
+ elsif rootparent
315
+ bok['parent'] = {
316
+ 'id' => rootparent.is_a?(String) ? rootparent : rootparent.cloud_desc.name
317
+ }
318
+ else
319
+ # org parent is *probably* safe to infer from credentials
320
+ end
321
+ end
322
+
323
+ if billing
324
+ bok['billing_acct'] = billing
325
+ else
326
+ cur_billing = MU::Cloud::Google.billing(credentials: @config['credentials']).get_project_billing_info("projects/"+@cloud_id)
327
+ if cur_billing and cur_billing.billing_account_name
328
+ bok['billing_acct'] = cur_billing.billing_account_name.sub(/^billingAccounts\//, '')
329
+ end
330
+ end
331
+
332
+ bok
333
+ end
334
+
335
+
215
336
  # Cloud-specific configuration properties.
216
337
  # @param config [MU::Config]: The calling MU::Config object
217
338
  # @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource
@@ -221,6 +342,10 @@ module MU
221
342
  "billing_acct" => {
222
343
  "type" => "string",
223
344
  "description" => "Billing account ID to associate with a newly-created Google Project. If not specified, will attempt to locate a billing account associated with the default project for our credentials."
345
+ },
346
+ "display_name" => {
347
+ "type" => "string",
348
+ "description" => "A human readable name for this project. If not specified, will default to our long-form deploy-generated name."
224
349
  }
225
350
  }
226
351
  [toplevel_required, schema]
@@ -234,7 +359,7 @@ module MU
234
359
  ok = true
235
360
 
236
361
  if !MU::Cloud::Google.getOrg(habitat['credentials'])
237
- MU.log "Cannot manage Google Cloud projects in environments without an organization. See also: https://cloud.google.com/resource-manager/docs/creating-managing-organization", MU::ERR
362
+ MU.log "Cannot manage Google Cloud folders in environments without an organization", MU::ERR
238
363
  ok = false
239
364
  end
240
365
 
@@ -18,41 +18,18 @@ module MU
18
18
  # A load balancer as configured in {MU::Config::BasketofKittens::loadbalancers}
19
19
  class LoadBalancer < MU::Cloud::LoadBalancer
20
20
 
21
- @project_id = nil
22
- @deploy = nil
23
21
  @lb = nil
24
- attr_reader :mu_name
25
- attr_reader :config
26
- attr_reader :cloud_id
27
22
  attr_reader :targetgroups
28
23
 
29
- @cloudformation_data = {}
30
- attr_reader :cloudformation_data
31
-
32
- # @param mommacat [MU::MommaCat]: A {MU::Mommacat} object containing the deploy of which this resource is/will be a member.
33
- # @param kitten_cfg [Hash]: The fully parsed and resolved {MU::Config} resource descriptor as defined in {MU::Config::BasketofKittens::loadbalancers}
34
- def initialize(mommacat: nil, kitten_cfg: nil, mu_name: nil, cloud_id: nil)
35
- @deploy = mommacat
36
- @config = MU::Config.manxify(kitten_cfg)
37
- @cloud_id ||= cloud_id
38
- if !mu_name.nil?
39
- @mu_name = mu_name
40
- @config['project'] ||= MU::Cloud::Google.defaultProject(@config['credentials'])
41
- if !@project_id
42
- project = MU::Cloud::Google.projectLookup(@config['project'], @deploy, sibling_only: true, raise_on_fail: false)
43
- @project_id = project.nil? ? @config['project'] : project.cloudobj.cloud_id
44
- end
45
- elsif @config['scrub_mu_isms']
46
- @mu_name = @config['name']
47
- else
48
- @mu_name = @deploy.getResourceName(@config["name"])
49
- end
24
+ # Initialize this cloud resource object. Calling +super+ will invoke the initializer defined under {MU::Cloud}, which should set the attribtues listed in {MU::Cloud::PUBLIC_ATTRS} as well as applicable dependency shortcuts, like <tt>@vpc</tt>, for us.
25
+ # @param args [Hash]: Hash of named arguments passed via Ruby's double-splat
26
+ def initialize(**args)
27
+ super
28
+ @mu_name ||= @deploy.getResourceName(@config["name"])
50
29
  end
51
30
 
52
31
  # Called automatically by {MU::Deploy#createResources}
53
32
  def create
54
- @project_id = MU::Cloud::Google.projectLookup(@config['project'], @deploy).cloudobj.cloud_id
55
-
56
33
  parent_thread_id = Thread.current.object_id
57
34
 
58
35
  backends = {}
@@ -81,7 +58,7 @@ module MU
81
58
  if !@config["private"]
82
59
  #TODO ip_address, port_range, target
83
60
  realproto = ["HTTP", "HTTPS"].include?(l['lb_protocol']) ? l['lb_protocol'] : "TCP"
84
- ruleobj = ::Google::Apis::ComputeBeta::ForwardingRule.new(
61
+ ruleobj = ::Google::Apis::ComputeV1::ForwardingRule.new(
85
62
  name: MU::Cloud::Google.nameStr(@mu_name+"-"+l['targetgroup']),
86
63
  description: @deploy.deploy_id,
87
64
  load_balancing_scheme: "EXTERNAL",
@@ -91,7 +68,7 @@ module MU
91
68
  )
92
69
  else
93
70
  # TODO network, subnetwork, port_range, target
94
- ruleobj = ::Google::Apis::ComputeBeta::ForwardingRule.new(
71
+ ruleobj = ::Google::Apis::ComputeV1::ForwardingRule.new(
95
72
  name: MU::Cloud::Google.nameStr(@mu_name+"-"+l['targetgroup']),
96
73
  description: @deploy.deploy_id,
97
74
  load_balancing_scheme: "INTERNAL",
@@ -118,10 +95,6 @@ module MU
118
95
 
119
96
  end
120
97
 
121
- # Wrapper that fetches the API's description of one of these things
122
- def cloud_desc
123
- end
124
-
125
98
  # Return the metadata for this LoadBalancer
126
99
  # @return [Hash]
127
100
  def notify
@@ -175,6 +148,7 @@ module MU
175
148
  # @return [void]
176
149
  def self.cleanup(noop: false, ignoremaster: false, region: nil, credentials: nil, flags: {})
177
150
  flags["project"] ||= MU::Cloud::Google.defaultProject(credentials)
151
+ return if !MU::Cloud::Google::Habitat.isLive?(flags["project"], credentials)
178
152
 
179
153
  if region
180
154
  ["forwarding_rule", "region_backend_service"].each { |type|
@@ -192,6 +166,7 @@ module MU
192
166
  MU::Cloud::Google.compute(credentials: credentials).delete(
193
167
  type,
194
168
  flags["project"],
169
+ nil,
195
170
  noop
196
171
  )
197
172
  }
@@ -0,0 +1,1211 @@
1
+ # Copyright:: Copyright (c) 2019 eGlobalTech, Inc., all rights reserved
2
+ #
3
+ # Licensed under the BSD-3 license (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License in the root of the project or at
6
+ #
7
+ # http://egt-labs.com/mu/LICENSE.html
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module MU
16
+ class Cloud
17
+ class Google
18
+ # A role as configured in {MU::Config::BasketofKittens::roles}
19
+ class Role < MU::Cloud::Role
20
+
21
+ # Initialize this cloud resource object. Calling +super+ will invoke the initializer defined under {MU::Cloud}, which should set the attribtues listed in {MU::Cloud::PUBLIC_ATTRS} as well as applicable dependency shortcuts, like <tt>@vpc</tt>, for us.
22
+ # @param args [Hash]: Hash of named arguments passed via Ruby's double-splat
23
+ def initialize(**args)
24
+ super
25
+
26
+ @mu_name ||= if !@config['scrub_mu_isms']
27
+ @deploy.getResourceName(@config["name"])
28
+ else
29
+ @config['name']
30
+ end
31
+
32
+ # If we're being reverse-engineered from a cloud descriptor, use that
33
+ # to determine what sort of account we are.
34
+ if args[:from_cloud_desc]
35
+ @cloud_desc_cache = args[:from_cloud_desc]
36
+ if args[:from_cloud_desc].class == ::Google::Apis::AdminDirectoryV1::Role
37
+ @config['role_source'] = "directory"
38
+ elsif args[:from_cloud_desc].name.match(/^roles\/(.*)/) or
39
+ (@cloud_id and @cloud_id.match(/^roles\/(.*)/))
40
+ @config['role_source'] = "canned"
41
+ @config['name'] = Regexp.last_match[1]
42
+ elsif args[:from_cloud_desc].name.match(/^organizations\/\d+\/roles\/(.*)/) or
43
+ (@cloud_id and @cloud_id.match(/^organizations\/\d+\/roles\/(.*)/))
44
+ @config['role_source'] = "org"
45
+ @config['name'] = Regexp.last_match[1]
46
+ elsif args[:from_cloud_desc].name.match(/^projects\/([^\/]+?)\/roles\/(.*)/) or
47
+ (@cloud_id and @cloud_id.match(/^projects\/\d+\/roles\/(.*)/))
48
+ @config['project'] = Regexp.last_match[1]
49
+ @config['name'] = Regexp.last_match[2]
50
+ @project_id = @config['project']
51
+ @config['role_source'] = "project"
52
+ else
53
+ MU.log "I don't know what to do with this #{args[:from_cloud_desc].class.name}", MU::ERR, details: args[:from_cloud_desc]
54
+ raise MuError, "I don't know what to do with this #{args[:from_cloud_desc].class.name}"
55
+ end
56
+ end
57
+ end
58
+
59
+ # Called automatically by {MU::Deploy#createResources}
60
+ def create
61
+ @config['display_name'] ||= @mu_name
62
+ if @config['role_source'] == "directory"
63
+ role_obj = MU::Cloud::Google.admin_directory(:Role).new(
64
+ role_name: @mu_name,
65
+ role_description: @config['display_name'],
66
+ role_privileges: MU::Cloud::Google::Role.map_directory_privileges(@config['import'], credentials: @credentials).first
67
+ )
68
+ MU.log "Creating directory role #{@mu_name}", details: role_obj
69
+
70
+ resp = MU::Cloud::Google.admin_directory(credentials: @credentials).insert_role(@customer, role_obj)
71
+ @cloud_id = resp.role_id.to_s
72
+
73
+ elsif @config['role_source'] == "canned"
74
+ @cloud_id = @config['name']
75
+ if !@cloud_id.match(/^roles\//)
76
+ @cloud_id = "roles/"+@cloud_id
77
+ end
78
+ else
79
+ create_role_obj = MU::Cloud::Google.iam(:CreateRoleRequest).new(
80
+ role: MU::Cloud::Google.iam(:Role).new(
81
+ title: @config['display_name'],
82
+ description: @config['description']
83
+ ),
84
+ role_id: MU::Cloud::Google.nameStr(@deploy.getResourceName(@config["name"], max_length: 64)).gsub(/[^a-zA-Z0-9_\.]/, "_")
85
+ )
86
+
87
+ resp = if @config['role_source'] == "org"
88
+ my_org = MU::Cloud::Google.getOrg(@config['credentials'])
89
+ MU.log "Creating IAM organization role #{@mu_name} in #{my_org.display_name}", details: create_role_obj
90
+ resp = MU::Cloud::Google.iam(credentials: @credentials).create_organization_role(my_org.name, create_role_obj)
91
+ elsif @config['role_source'] == "project"
92
+ if !@project_id
93
+ raise MuError, "Role #{@mu_name} is supposed to be in project #{@config['project']}, but no such project was found"
94
+ end
95
+ MU.log "Creating IAM project role #{@mu_name} in #{@project_id}", details: create_role_obj
96
+ MU::Cloud::Google.iam(credentials: @credentials).create_project_role("projects/"+@project_id, create_role_obj)
97
+ end
98
+
99
+ @cloud_id = resp.name
100
+
101
+ end
102
+ end
103
+
104
+ # Called automatically by {MU::Deploy#createResources}
105
+ def groom
106
+ if @config['role_source'] == "directory"
107
+ # MU.log "Updating directory role #{@mu_name}", MU::NOTICE, details: role_obj
108
+ # MU::Cloud::Google.admin_directory(credentials: @credentials).patch_role(@customer, @cloud_id, role_obj)
109
+ elsif @config['role_source'] == "org"
110
+ elsif @config['role_source'] == "project"
111
+ elsif @config['role_source'] == "canned"
112
+ end
113
+
114
+ @config['bindings'].each { |binding|
115
+ binding.keys.each { |scopetype|
116
+ next if scopetype == "entity"
117
+ binding[scopetype].each { |scope|
118
+ # XXX handle entity being a MU::Config::Ref
119
+ entity_id = if binding["entity"]["name"]
120
+ sib = @deploy.findLitterMate(name: binding["entity"]["name"], type: binding["entity"]["type"])
121
+ raise MuError, "Failed to look up sibling #{binding["entity"]["type"]}:#{binding["entity"]["name"]}" if !sib
122
+ if binding["entity"]["type"] == "users" and sib.config["type"] == "service"
123
+ binding["entity"]["type"] = "serviceAccount"
124
+ end
125
+ sib.cloud_id
126
+ else
127
+ binding["entity"]["id"]
128
+ end
129
+ # XXX resolve scope as well, if it's named or a MU::Config::Ref
130
+ bindToIAM(binding["entity"]["type"], entity_id.sub(/.*?\/([^\/]+)$/, '\1'), scopetype, scope["id"])
131
+ }
132
+ }
133
+ }
134
+ end
135
+
136
+ # Return the cloud descriptor for the Role
137
+ # @return [Google::Apis::Core::Hashable]
138
+ def cloud_desc
139
+ return @cloud_desc_cache if @cloud_desc_cache
140
+
141
+ my_org = MU::Cloud::Google.getOrg(@config['credentials'])
142
+
143
+ @cloud_desc_cache = if @config['role_source'] == "directory"
144
+ MU::Cloud::Google.admin_directory(credentials: @config['credentials']).get_role(@customer, @cloud_id)
145
+ elsif @config['role_source'] == "canned"
146
+ MU::Cloud::Google.iam(credentials: @config['credentials']).get_role(@cloud_id)
147
+ elsif @config['role_source'] == "project"
148
+ MU::Cloud::Google.iam(credentials: @config['credentials']).get_project_role(@cloud_id)
149
+ elsif @config['role_source'] == "org"
150
+ MU::Cloud::Google.iam(credentials: @config['credentials']).get_organization_role(@cloud_id)
151
+ end
152
+
153
+ @cloud_desc_cache
154
+ end
155
+
156
+ # Return the metadata for this group configuration
157
+ # @return [Hash]
158
+ def notify
159
+ base = MU.structToHash(cloud_desc)
160
+ base.delete(:etag)
161
+ base["cloud_id"] = @cloud_id
162
+
163
+ base
164
+ end
165
+
166
+ # Wrapper for #{MU::Cloud::Google::Role.bindToIAM}
167
+ def bindToIAM(entity_type, entity_id, scope_type, scope_id)
168
+ MU::Cloud::Google::Role.bindToIAM(@cloud_id, entity_type, entity_id, scope_type, scope_id, credentials: @config['credentials'])
169
+ end
170
+
171
+ @@role_bind_semaphore = Mutex.new
172
+ @@role_bind_scope_semaphores = {}
173
+
174
+ # Attach a role to an entity
175
+ # @param role_id [String]: The cloud identifier of the role to which we're binding
176
+ # @param entity_type [String]: The kind of entity to bind; typically user, group, or domain
177
+ # @param entity_id [String]: The cloud identifier of the entity
178
+ # @param scope_type [String]: The kind of scope in which this binding will be valid; typically project, folder, or organization
179
+ # @param scope_id [String]: The cloud identifier of the scope in which this binding will be valid
180
+ # @param credentials [String]:
181
+ def self.bindToIAM(role_id, entity_type, entity_id, scope_type, scope_id, credentials: nil, debug: false)
182
+ loglevel = debug ? MU::NOTICE : MU::DEBUG
183
+
184
+ MU.log "Google::Role.bindToIAM(role_id: #{role_id}, entity_type: #{entity_type}, entity_id: #{entity_id}, scope_type: #{scope_type}, scope_id: #{scope_id}, credentials: #{credentials})", loglevel
185
+
186
+ # scope_id might actually be the name of a credential set; if so, we
187
+ # map it back to an actual organization on the fly
188
+ if scope_type == "organizations"
189
+ my_org = MU::Cloud::Google.getOrg(scope_id)
190
+ if my_org
191
+ scope_id = my_org.name
192
+ end
193
+ end
194
+
195
+ @@role_bind_semaphore.synchronize {
196
+ @@role_bind_scope_semaphores[scope_id] ||= Mutex.new
197
+ }
198
+
199
+ @@role_bind_scope_semaphores[scope_id].synchronize {
200
+ entity = entity_type.sub(/s$/, "")+":"+entity_id
201
+ policy = if scope_type == "organizations"
202
+ MU::Cloud::Google.resource_manager(credentials: credentials).get_organization_iam_policy(scope_id)
203
+ elsif scope_type == "folders"
204
+ MU::Cloud::Google.resource_manager(credentials: credentials).get_folder_iam_policy(scope_id)
205
+ elsif scope_type == "projects"
206
+ if !scope_id
207
+ raise MuError, "Google::Role.bindToIAM was called without a scope_id"
208
+ elsif scope_id.is_a?(Hash)
209
+ if scope_id["id"]
210
+ scope_id = scope_id["id"]
211
+ else
212
+ raise MuError, "Google::Role.bindToIAM was called with a scope_id Ref hash that has no id field"
213
+ end
214
+ end
215
+ MU::Cloud::Google.resource_manager(credentials: credentials).get_project_iam_policy(scope_id.sub(/^projects\//, ""))
216
+ else
217
+ puts "WTF DO WIT #{scope_type}"
218
+ end
219
+
220
+ saw_role = false
221
+ policy.bindings.each { |binding|
222
+ if binding.role == role_id
223
+ saw_role = true
224
+ if binding.members.include?(entity)
225
+ return # it's already bound, nothing needs doing
226
+ else
227
+ binding.members << entity
228
+ end
229
+ end
230
+ }
231
+ if !saw_role
232
+ policy.bindings << MU::Cloud::Google.resource_manager(:Binding).new(
233
+ role: role_id,
234
+ members: [entity]
235
+ )
236
+ end
237
+ MU.log "Granting #{role_id} to #{entity} in #{scope_id}", MU::NOTICE
238
+ req_obj = MU::Cloud::Google.resource_manager(:SetIamPolicyRequest).new(
239
+ policy: policy
240
+ )
241
+ policy = if scope_type == "organizations"
242
+ MU::Cloud::Google.resource_manager(credentials: credentials).set_organization_iam_policy(
243
+ scope_id,
244
+ req_obj
245
+ )
246
+ elsif scope_type == "folders"
247
+ MU::Cloud::Google.resource_manager(credentials: credentials).set_folder_iam_policy(
248
+ scope_id,
249
+ req_obj
250
+ )
251
+ elsif scope_type == "projects"
252
+ MU::Cloud::Google.resource_manager(credentials: credentials).set_project_iam_policy(
253
+ scope_id,
254
+ req_obj
255
+ )
256
+ end
257
+ }
258
+ end
259
+
260
+ # Remove all bindings for the specified entity
261
+ # @param entity_type [String]: The kind of entity to bind; typically user, group, or domain
262
+ # @param entity_id [String]: The cloud identifier of the entity
263
+ # @param credentials [String]:
264
+ # @param noop [Boolean]: Just say what we'd do without doing it
265
+ def self.removeBindings(entity_type, entity_id, credentials: nil, noop: false)
266
+
267
+ scopes = {}
268
+
269
+ my_org = MU::Cloud::Google.getOrg(credentials)
270
+ if my_org
271
+ scopes["organizations"] = [my_org.name]
272
+ folders = MU::Cloud::Google::Folder.find(credentials: credentials)
273
+ if folders and folders.size > 0
274
+ scopes["folders"] = folders.keys
275
+ end
276
+ end
277
+
278
+ projects = MU::Cloud::Google::Habitat.find(credentials: credentials)
279
+ if projects and projects.size > 0
280
+ scopes["projects"] = projects.keys
281
+ end
282
+
283
+ scopes.each_pair { |scope_type, scope_ids|
284
+ scope_ids.each { |scope_id|
285
+ @@role_bind_semaphore.synchronize {
286
+ @@role_bind_scope_semaphores[scope_id] ||= Mutex.new
287
+ }
288
+
289
+ @@role_bind_scope_semaphores[scope_id].synchronize {
290
+ entity = entity_type.sub(/s$/, "")+":"+entity_id
291
+ policy = if scope_type == "organizations"
292
+ MU::Cloud::Google.resource_manager(credentials: credentials).get_organization_iam_policy(my_org.name)
293
+ elsif scope_type == "folders"
294
+ MU::Cloud::Google.resource_manager(credentials: credentials).get_folder_iam_policy(scope_id)
295
+ elsif scope_type == "projects"
296
+ MU::Cloud::Google.resource_manager(credentials: credentials).get_project_iam_policy(scope_id)
297
+ end
298
+
299
+ need_update = false
300
+ policy.bindings.each { |binding|
301
+ if binding.members.include?(entity)
302
+ MU.log "Unbinding #{binding.role} from #{entity} in #{scope_id}"
303
+ need_update = true
304
+ binding.members.delete(entity)
305
+ end
306
+ }
307
+ # XXX maybe drop bindings with 0 members?
308
+ next if !need_update or noop
309
+ req_obj = MU::Cloud::Google.resource_manager(:SetIamPolicyRequest).new(
310
+ policy: policy
311
+ )
312
+
313
+ policy = if scope_type == "organizations"
314
+ MU::Cloud::Google.resource_manager(credentials: credentials).set_organization_iam_policy(
315
+ scope_id,
316
+ req_obj
317
+ )
318
+ elsif scope_type == "folders"
319
+ MU::Cloud::Google.resource_manager(credentials: credentials).set_folder_iam_policy(
320
+ scope_id,
321
+ req_obj
322
+ )
323
+ elsif scope_type == "projects"
324
+ MU::Cloud::Google.resource_manager(credentials: credentials).set_project_iam_policy(
325
+ scope_id,
326
+ req_obj
327
+ )
328
+ end
329
+ }
330
+
331
+ }
332
+ }
333
+ end
334
+
335
+ # Add role bindings for a given entity from its BoK config
336
+ # @param entity_type [String]: The kind of entity to bind; typically user, group, or domain
337
+ # @param entity_id [String]: The cloud identifier of the entity
338
+ # @param cfg [Hash]: A configuration block confirming to our own {MU::Cloud::Google::Role.ref_schema}
339
+ # @param credentials [String]:
340
+ def self.bindFromConfig(entity_type, entity_id, cfg, credentials: nil, deploy: nil, debug: false)
341
+ loglevel = debug ? MU::NOTICE : MU::DEBUG
342
+
343
+ bindings = []
344
+
345
+ return if !cfg
346
+ MU.log "Google::Role::bindFromConfig binding called for #{entity_type} #{entity_id}", loglevel, details: cfg
347
+
348
+ cfg.each { |binding|
349
+ if deploy and binding["role"]["name"] and !binding["role"]["id"]
350
+ role_obj = deploy.findLitterMate(name: binding["role"]["name"], type: "roles")
351
+ binding["role"]["id"] = role_obj.cloud_id if role_obj
352
+ end
353
+ ["organizations", "projects", "folders"].each { |scopetype|
354
+ next if !binding[scopetype]
355
+
356
+ binding[scopetype].each { |scope|
357
+ # XXX resolution of Ref bits (roles, projects, and folders anyway; organizations and domains are direct)
358
+ MU::Cloud::Google::Role.bindToIAM(
359
+ binding["role"]["id"],
360
+ entity_type,
361
+ entity_id,
362
+ scopetype,
363
+ scope,
364
+ credentials: credentials
365
+ )
366
+ }
367
+ }
368
+ if binding["directories"]
369
+ binding["directories"].each { |dir|
370
+ # this is either an organization cloud_id, or the name of one
371
+ # of our credential sets, which we must map to an organization
372
+ # cloud id
373
+ creds = MU::Cloud::Google.credConfig(dir)
374
+
375
+ customer = if creds
376
+ my_org = MU::Cloud::Google.getOrg(dir)
377
+ if !my_org
378
+ raise MuError, "Google directory role binding specified directory #{dir}, which looks like one of our credential sets, but does not appear to map to an organization!"
379
+ end
380
+ my_org.owner.directory_customer_id
381
+ elsif dir.match(/^organizations\//)
382
+ # Not sure if there's ever a case where we can do this with
383
+ # an org that's different from the one our credentials go with
384
+ my_org = MU::Cloud::Google.getOrg(credentials, with_id: dir)
385
+ if !my_org
386
+ raise MuError, "Failed to retrieve #{dir} with credentials #{credentials} in Google directory role binding for role #{binding["role"].to_s}"
387
+ end
388
+ my_org.owner.directory_customer_id
389
+ else
390
+ # assume it's a raw customer id and hope for the best
391
+ dir
392
+ end
393
+
394
+ if !binding["role"]["id"].match(/^\d+$/)
395
+ resp = MU::Cloud::Google.admin_directory(credentials: credentials).list_roles(customer)
396
+ if resp and resp.items
397
+ resp.items.each { |role|
398
+ if role.role_name == binding["role"]["id"]
399
+ binding["role"]["id"] = role.role_id
400
+ break
401
+ end
402
+ }
403
+ end
404
+ end
405
+
406
+ # Ensure we're using the stupid internal id, instead of the
407
+ # email field (which is the "real" id most of the time)
408
+ real_id = nil
409
+ if entity_type == "group"
410
+ found = MU::Cloud::Google::Group.find(cloud_id: entity_id, credentials: credentials)
411
+ if found[entity_id]
412
+ real_id = found[entity_id].id
413
+ end
414
+ elsif entity_type == "user"
415
+ found = MU::Cloud::Google::User.find(cloud_id: entity_id, credentials: credentials)
416
+ if found[entity_id]
417
+ real_id = found[entity_id].id
418
+ end
419
+ else
420
+ raise MuError, "I don't know how to identify entity type #{entity_type} with id #{entity_id} in directory role binding"
421
+ end
422
+ real_id ||= entity_id # fingers crossed
423
+
424
+ assign_obj = MU::Cloud::Google.admin_directory(:RoleAssignment).new(
425
+ assigned_to: real_id,
426
+ role_id: binding["role"]["id"],
427
+ scope_type: "CUSTOMER"
428
+ )
429
+ # XXX guard this mess
430
+ MU.log "Binding directory role #{(binding["role"]["name"] || binding["role"]["id"])} to #{entity_type} #{entity_id} in #{dir}", details: assign_obj
431
+ MU::Cloud::Google.admin_directory(credentials: credentials).insert_role_assignment(
432
+ customer,
433
+ assign_obj
434
+ )
435
+
436
+ }
437
+ end
438
+ }
439
+
440
+ # XXX whattabout GSuite-tier roles?
441
+ end
442
+
443
+ # Does this resource type exist as a global (cloud-wide) artifact, or
444
+ # is it localized to a region/zone?
445
+ # @return [Boolean]
446
+ def self.isGlobal?
447
+ true
448
+ end
449
+
450
+ # Return the list of "container" resource types in which this resource
451
+ # can reside. The list will include an explicit nil if this resource
452
+ # can exist outside of any container.
453
+ # @return [Array<Symbol,nil>]
454
+ def self.canLiveIn
455
+ [nil, :Habitat, :Folder]
456
+ end
457
+
458
+ # Denote whether this resource implementation is experiment, ready for
459
+ # testing, or ready for production use.
460
+ def self.quality
461
+ MU::Cloud::RELEASE
462
+ end
463
+
464
+ # Remove all roles associated with the currently loaded deployment.
465
+ # @param noop [Boolean]: If true, will only print what would be done
466
+ # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
467
+ # @param region [String]: The cloud provider region
468
+ # @return [void]
469
+ def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
470
+ customer = MU::Cloud::Google.customerID(credentials)
471
+ my_org = MU::Cloud::Google.getOrg(credentials)
472
+
473
+ if flags['known']
474
+ flags['known'].each { |id|
475
+ next if id.nil?
476
+ # GCP roles don't have a useful field for packing in our deploy
477
+ # id, so if we have metadata to leverage for this, use it. For
478
+ # directory roles, we try to make it into the name field, so
479
+ # we'll check that later, but for org and project roles this is
480
+ # our only option.
481
+ if my_org and id.is_a?(Integer) or id.match(/^\d+/)
482
+ begin
483
+ resp = MU::Cloud::Google.admin_directory(credentials: credentials).get_role(customer, id)
484
+ rescue ::Google::Apis::ClientError => e
485
+ next if e.message.match(/notFound/)
486
+ raise e
487
+ end
488
+ if resp
489
+ MU.log "Deleting directory role #{resp.role_name}"
490
+ if !noop
491
+ MU::Cloud::Google.admin_directory(credentials: credentials).delete_role(customer, id)
492
+ end
493
+ end
494
+ elsif id.match(/^projects\//)
495
+ begin
496
+ resp = MU::Cloud::Google.iam(credentials: credentials).get_project_role(id)
497
+ rescue ::Google::Apis::ClientError => e
498
+ #MU.log e.message, MU::ERR, details: id
499
+ #next
500
+ next if e.message.match(/notFound/)
501
+ raise e
502
+ end
503
+ if resp
504
+ MU.log "Deleting project role #{resp.name}"
505
+ if !noop
506
+ MU::Cloud::Google.iam(credentials: credentials).delete_project_role(id)
507
+ end
508
+ end
509
+ elsif id.match(/^organizations\//)
510
+ begin
511
+ resp = MU::Cloud::Google.iam(credentials: credentials).get_organization_role(id)
512
+ rescue ::Google::Apis::ClientError => e
513
+ #MU.log e.message, MU::ERR, details: id
514
+ #next
515
+ next if e.message.match(/notFound/)
516
+ raise e
517
+ end
518
+ if resp
519
+ MU.log "Deleting organization role #{resp.name}"
520
+ if !noop
521
+ MU::Cloud::Google.iam(credentials: credentials).delete_organization_role(id)
522
+ end
523
+ end
524
+ end
525
+ }
526
+ end
527
+
528
+ if my_org and MU.deploy_id and !MU.deploy_id.empty?
529
+ resp = MU::Cloud::Google.admin_directory(credentials: credentials).list_roles(customer)
530
+ if resp and resp.items
531
+ resp.items.each { |role|
532
+ if role.role_name.match(/^#{Regexp.quote(MU.deploy_id)}/)
533
+ MU.log "Deleting directory role #{role.role_name}"
534
+ if !noop
535
+ MU::Cloud::Google.admin_directory(credentials: credentials).delete_role(customer, role.role_id)
536
+ end
537
+ end
538
+ }
539
+ end
540
+ end
541
+
542
+ end
543
+
544
+ # Locate and return cloud provider descriptors of this resource type
545
+ # which match the provided parameters, or all visible resources if no
546
+ # filters are specified. At minimum, implementations of +find+ must
547
+ # honor +credentials+ and +cloud_id+ arguments. We may optionally
548
+ # support other search methods, such as +tag_key+ and +tag_value+, or
549
+ # cloud-specific arguments like +project+. See also {MU::MommaCat.findStray}.
550
+ # @param args [Hash]: Hash of named arguments passed via Ruby's double-splat
551
+ # @return [Hash<String,OpenStruct>]: The cloud provider's complete descriptions of matching resources
552
+ def self.find(**args)
553
+ credcfg = MU::Cloud::Google.credConfig(args[:credentials])
554
+ customer = MU::Cloud::Google.customerID(args[:credentials])
555
+ my_org = MU::Cloud::Google.getOrg(args[:credentials])
556
+
557
+ found = {}
558
+ args[:project] ||= args[:habitat]
559
+
560
+ if args[:project]
561
+ canned = Hash[MU::Cloud::Google.iam(credentials: args[:credentials]).list_roles.roles.map { |r| [r.name, r] }]
562
+ begin
563
+ MU::Cloud::Google::Habitat.bindings(args[:project], credentials: args[:credentials]).each { |binding|
564
+ found[binding.role] = canned[binding.role]
565
+ }
566
+ rescue ::Google::Apis::ClientError => e
567
+ raise e if !e.message.match(/forbidden: /)
568
+ end
569
+
570
+ resp = begin
571
+ MU::Cloud::Google.iam(credentials: args[:credentials]).list_project_roles("projects/"+args[:project])
572
+ rescue ::Google::Apis::ClientError => e
573
+ raise e if !e.message.match(/forbidden: /)
574
+ end
575
+ if resp and resp.roles
576
+ resp.roles.each { |role|
577
+ found[role.name] = role
578
+ }
579
+ end
580
+ if args[:cloud_id]
581
+ found.reject! { |k, v| k != role.name }
582
+ end
583
+
584
+ # Now go get everything that's bound here
585
+ bindings = MU::Cloud::Google::Role.getAllBindings(args[:credentials])
586
+ if bindings and bindings['by_scope'] and
587
+ bindings['by_scope']['projects'] and
588
+ bindings['by_scope']['projects'][args[:project]]
589
+ bindings['by_scope']['projects'][args[:project]].keys.each { |r|
590
+ if r.match(/^roles\//)
591
+ role = MU::Cloud::Google.iam(credentials: args[:credentials]).get_role(r)
592
+ found[role.name] = role
593
+ elsif !found[r]
594
+ MU.log "NEED TO GET #{r}", MU::WARN
595
+ end
596
+ }
597
+ end
598
+ else
599
+ if credcfg['masquerade_as']
600
+ if args[:cloud_id]
601
+ begin
602
+ resp = MU::Cloud::Google.admin_directory(credentials: args[:credentials]).get_role(customer, args[:cloud_id].to_i)
603
+ if resp
604
+ found[args[:cloud_id].to_s] = resp
605
+ end
606
+ rescue ::Google::Apis::ClientError => e
607
+ raise e if !e.message.match(/(?:forbidden|notFound): /)
608
+ end
609
+ else
610
+ resp = MU::Cloud::Google.admin_directory(credentials: args[:credentials]).list_roles(customer)
611
+ if resp and resp.items
612
+ resp.items.each { |role|
613
+ found[role.role_id.to_s] = role
614
+ }
615
+ end
616
+ end
617
+
618
+ end
619
+ # These are the canned roles
620
+ resp = begin
621
+ MU::Cloud::Google.iam(credentials: args[:credentials]).list_roles
622
+ rescue ::Google::Apis::ClientError => e
623
+ raise e if !e.message.match(/forbidden: /)
624
+ end
625
+ if resp
626
+ resp.roles.each { |role|
627
+ found[role.name] = role
628
+ }
629
+ end
630
+
631
+ resp = begin
632
+ MU::Cloud::Google.iam(credentials: args[:credentials]).list_organization_roles(my_org.name)
633
+ rescue ::Google::Apis::ClientError => e
634
+ raise e if !e.message.match(/forbidden: /)
635
+ end
636
+ if resp and resp.roles
637
+ resp.roles.each { |role|
638
+ found[role.name] = role
639
+ }
640
+ end
641
+ end
642
+
643
+ found
644
+ end
645
+
646
+ # Reverse-map our cloud description into a runnable config hash.
647
+ # We assume that any values we have in +@config+ are placeholders, and
648
+ # calculate our own accordingly based on what's live in the cloud.
649
+ def toKitten(rootparent: nil, billing: nil, habitats: nil)
650
+ bok = {
651
+ "cloud" => "Google",
652
+ "credentials" => @config['credentials'],
653
+ "cloud_id" => @cloud_id
654
+ }
655
+
656
+ my_org = MU::Cloud::Google.getOrg(@config['credentials'])
657
+
658
+ # This can happen if the role_source isn't set correctly. This logic
659
+ # maybe belongs inside cloud_desc. XXX
660
+ if cloud_desc.nil?
661
+ if @cloud_id and @cloud_id.match(/^roles\/(.*)/)
662
+ @config['role_source'] = "canned"
663
+ elsif @cloud_id and @cloud_id.match(/^organizations\/\d+\/roles\/(.*)/)
664
+ @config['role_source'] = "org"
665
+ elsif @cloud_id and @cloud_id.match(/^projects\/\d+\/roles\/(.*)/)
666
+ @config['role_source'] = "project"
667
+ end
668
+ end
669
+
670
+ # GSuite or Cloud Identity role
671
+ if cloud_desc.class == ::Google::Apis::AdminDirectoryV1::Role
672
+ return nil if cloud_desc.is_system_role
673
+
674
+ bok["name"] = @config['name'].gsub(/[^a-z0-9]/i, '-').downcase
675
+ bok['role_source'] = "directory"
676
+ bok["display_name"] = @config['name']
677
+ if !cloud_desc.role_description.empty?
678
+ bok["description"] = cloud_desc.role_description
679
+ end
680
+ if !cloud_desc.role_privileges.nil? and !cloud_desc.role_privileges.empty?
681
+ bok['import'] = []
682
+ ids, names, privs = MU::Cloud::Google::Role.privilege_service_to_name(@config['credentials'])
683
+ cloud_desc.role_privileges.each { |priv|
684
+ if !ids[priv.service_id]
685
+ MU.log "Role privilege defined for a service id with no name I can find, writing with raw id", MU::WARN, details: priv
686
+ bok["import"] << priv.service_id+"/"+priv.privilege_name
687
+ else
688
+ bok["import"] << ids[priv.service_id]+"/"+priv.privilege_name
689
+ end
690
+ }
691
+ bok['import'].sort! # at least be legible
692
+ end
693
+ else # otherwise it's a GCP IAM role of some kind
694
+
695
+ return nil if cloud_desc.stage == "DISABLED"
696
+ if cloud_desc.name.match(/^roles\/([^\/]+)$/)
697
+ name = Regexp.last_match[1]
698
+ bok['name'] = name.gsub(/[^a-z0-9]/i, '-')
699
+ bok['role_source'] = "canned"
700
+ elsif cloud_desc.name.match(/^([^\/]+?)\/([^\/]+?)\/roles\/(.*)/)
701
+ junk, type, parent, name = Regexp.last_match.to_a
702
+ bok['name'] = name.gsub(/[^a-z0-9]/i, '-')
703
+ bok['role_source'] = type == "organizations" ? "org" : "project"
704
+ if bok['role_source'] == "project"
705
+ bok['project'] = parent
706
+ end
707
+ if cloud_desc.included_permissions and cloud_desc.included_permissions.size > 0
708
+ bok['import'] = cloud_desc.included_permissions
709
+ end
710
+
711
+ else
712
+ raise MuError, "I don't know how to parse GCP IAM role identifier #{cloud_desc.name}"
713
+ end
714
+
715
+ if !cloud_desc.description.nil? and !cloud_desc.description.empty?
716
+ bok["description"] = cloud_desc.description
717
+ end
718
+ bok["display_name"] = cloud_desc.title
719
+
720
+ bindings = MU::Cloud::Google::Role.getAllBindings(@config['credentials'])["by_role"][@cloud_id]
721
+
722
+ if bindings
723
+ refmap = {}
724
+ bindings.keys.each { |scopetype|
725
+ bindings[scopetype].each_pair { |scope_id, entity_types|
726
+ # If we've been given a habitat filter, skip over bindings
727
+ # that don't match it.
728
+ if scopetype == "projects" and habitats and
729
+ !habitats.empty? and !habitats.include?(scope_id)
730
+ next
731
+ end
732
+
733
+ entity_types.each_pair { |entity_type, entities|
734
+ mu_entitytype = (entity_type == "serviceAccount" ? "user" : entity_type)+"s"
735
+ entities.each { |entity|
736
+ entity_ref = if entity_type == "organizations"
737
+ { "id" => ((org == my_org.name and @config['credentials']) ? @config['credentials'] : org) }
738
+ elsif entity_type == "domain"
739
+ { "id" => entity }
740
+ else
741
+ MU::Config::Ref.get(
742
+ id: entity,
743
+ cloud: "Google",
744
+ type: mu_entitytype
745
+ )
746
+ end
747
+ refmap ||= {}
748
+ refmap[entity_ref] ||= {}
749
+ refmap[entity_ref][scopetype] ||= []
750
+ mu_scopetype = scopetype == "projects" ? "habitats" : scopetype
751
+ if scopetype == "organizations" or scopetype == "domains" # XXX singular? plural? barf
752
+ refmap[entity_ref][scopetype] << ((scope_id == my_org.name and @config['credentials']) ? @config['credentials'] : scope_id)
753
+ else
754
+ refmap[entity_ref][scopetype] << MU::Config::Ref.get(
755
+ id: scope_id,
756
+ cloud: "Google",
757
+ type: mu_scopetype
758
+ )
759
+ end
760
+ refmap[entity_ref][scopetype].uniq!
761
+ }
762
+ }
763
+ }
764
+ }
765
+ bok["bindings"] ||= []
766
+ refmap.each_pair { |entity, scopes|
767
+ newbinding = { "entity" => entity }
768
+ scopes.keys.each { |scopetype|
769
+ newbinding[scopetype] = scopes[scopetype].sort
770
+ }
771
+ bok["bindings"] << newbinding
772
+ }
773
+ end
774
+ end
775
+
776
+ # Our only reason for declaring canned roles is so we can put their
777
+ # bindings somewhere. If there aren't any, then we don't need
778
+ # to bother with them.
779
+ if bok['role_source'] == "canned" and
780
+ (bok['bindings'].nil? or bok['bindings'].empty?)
781
+ return nil
782
+ end
783
+
784
+ bok
785
+ end
786
+
787
+
788
+ # Schema used by +user+ and +group+ entities to reference role
789
+ # assignments and their scopes.
790
+ # @return [<Hash>]
791
+ def self.ref_schema
792
+ {
793
+ "type" => "object",
794
+ "description" => "One or more Google IAM roles to associate with this entity. IAM roles in Google can be associated at the project (+Habitat+), folder, or organization level, so we must specify not only role, but each container in which it is granted to the entity in question.",
795
+ "properties" => {
796
+ "role" => MU::Config::Ref.schema(type: "roles"),
797
+ "projects" => {
798
+ "type" => "array",
799
+ "items" => MU::Config::Ref.schema(type: "habitats")
800
+ },
801
+ "folders" => {
802
+ "type" => "array",
803
+ "items" => MU::Config::Ref.schema(type: "folders")
804
+ },
805
+ "organizations" => {
806
+ "type" => "array",
807
+ "items" => {
808
+ "type" => "string",
809
+ "description" => "Either an organization cloud identifier, like +organizations/123456789012+, or the name of set of Mu credentials listed in +mu.yaml+, which can be used as an alias to the organization to which they authenticate."
810
+ }
811
+ }
812
+ }
813
+ }
814
+ end
815
+
816
+ @@binding_semaphore = Mutex.new
817
+ @@bindings_by_role = {}
818
+ @@bindings_by_entity = {}
819
+ @@bindings_by_scope = {}
820
+
821
+ # Retrieve IAM role bindings for all entities throughout our
822
+ # organization, map them in useful ways, and cache the result.
823
+ def self.getAllBindings(credentials = nil, refresh: false)
824
+ my_org = MU::Cloud::Google.getOrg(credentials)
825
+ @@binding_semaphore.synchronize {
826
+ if @@bindings_by_role.size > 0 and !refresh
827
+ return {
828
+ "by_role" => @@bindings_by_role.dup,
829
+ "by_scope" => @@bindings_by_scope.dup,
830
+ "by_entity" => @@bindings_by_entity.dup
831
+ }
832
+ end
833
+
834
+ def self.insertBinding(scopetype, scope, binding = nil, member_type: nil, member_id: nil, role_id: nil)
835
+ role_id = binding.role if binding
836
+ @@bindings_by_scope[scopetype] ||= {}
837
+ @@bindings_by_scope[scopetype][scope] ||= {}
838
+ @@bindings_by_scope[scopetype][scope][role_id] ||= {}
839
+ @@bindings_by_role[role_id] ||= {}
840
+ @@bindings_by_role[role_id][scopetype] ||= {}
841
+ @@bindings_by_role[role_id][scopetype][scope] ||= {}
842
+
843
+ do_binding = Proc.new { |type, id|
844
+ @@bindings_by_role[role_id][scopetype][scope][type] ||= []
845
+ @@bindings_by_role[role_id][scopetype][scope][type] << id
846
+ @@bindings_by_scope[scopetype][scope][role_id][type] ||= []
847
+ @@bindings_by_scope[scopetype][scope][role_id][type] << id
848
+ @@bindings_by_entity[type] ||= {}
849
+ @@bindings_by_entity[type][id] ||= {}
850
+ @@bindings_by_entity[type][id][role_id] ||= {}
851
+ @@bindings_by_entity[type][id][role_id][scopetype] ||= []
852
+ @@bindings_by_entity[type][id][role_id][scopetype] << scope
853
+ }
854
+
855
+ if binding
856
+ binding.members.each { |member|
857
+ member_type, member_id = member.split(/:/)
858
+ do_binding.call(member_type, member_id)
859
+ }
860
+ elsif member_type and member_id
861
+ do_binding.call(member_type, member_id)
862
+ end
863
+
864
+ end
865
+
866
+ if my_org
867
+ resp = MU::Cloud::Google.admin_directory(credentials: credentials).list_role_assignments(MU::Cloud::Google.customerID(credentials))
868
+
869
+ resp.items.each { |binding|
870
+
871
+ begin
872
+ user = MU::Cloud::Google.admin_directory(credentials: credentials).get_user(binding.assigned_to)
873
+ insertBinding("directories", my_org.name, member_id: user.primary_email, member_type: "user", role_id: binding.role_id.to_s)
874
+ next
875
+ rescue ::Google::Apis::ClientError # notFound
876
+ end
877
+
878
+ begin
879
+ group = MU::Cloud::Google.admin_directory(credentials: credentials).get_group(binding.assigned_to)
880
+ MU.log "GROUP", MU::NOTICE, details: group
881
+ # insertBinding("directories", my_org.name, member_id: group.primary_email, member_type: "group", role_id: binding.role_id.to_s)
882
+ next
883
+ rescue ::Google::Apis::ClientError # notFound
884
+ end
885
+
886
+ role = MU::Cloud::Google.admin_directory(credentials: credentials).get_role(MU::Cloud::Google.customerID(credentials), binding.role_id)
887
+ MU.log "Failed to find entity #{binding.assigned_to} referenced in GSuite/Cloud Identity binding to role #{role.role_name}", MU::WARN, details: role
888
+ }
889
+
890
+ resp = MU::Cloud::Google.resource_manager(credentials: credentials).get_organization_iam_policy(my_org.name)
891
+ resp.bindings.each { |binding|
892
+ insertBinding("organizations", my_org.name, binding)
893
+ }
894
+
895
+ MU::Cloud::Google::Folder.find(credentials: credentials).keys.each { |folder|
896
+ MU::Cloud::Google::Folder.bindings(folder, credentials: credentials).each { |binding|
897
+ insertBinding("folders", folder, binding)
898
+ }
899
+ }
900
+ end
901
+ MU::Cloud::Google::Habitat.find(credentials: credentials).keys.each { |project|
902
+ begin
903
+ MU::Cloud::Google::Habitat.bindings(project, credentials: credentials).each { |binding|
904
+ insertBinding("projects", project, binding)
905
+ }
906
+ rescue ::Google::Apis::ClientError => e
907
+ if e.message.match(/forbidden: /)
908
+ MU.log "Do not have permissions to retrieve bindings in project #{project}, skipping", MU::WARN
909
+ else
910
+ raise e
911
+ end
912
+ end
913
+
914
+ }
915
+
916
+ return {
917
+ "by_role" => @@bindings_by_role.dup,
918
+ "by_scope" => @@bindings_by_scope.dup,
919
+ "by_entity" => @@bindings_by_entity.dup
920
+ }
921
+ }
922
+ end
923
+
924
+ # Convert a list of bindings of the type returned by {MU::Cloud::Google::Role.getAllBindings} into valid configuration language.
925
+ # @param roles [Hash]
926
+ # @param credentials [String]
927
+ # @return [Hash]
928
+ def self.entityBindingsToSchema(roles, credentials: nil)
929
+ my_org = MU::Cloud::Google.getOrg(credentials)
930
+ role_cfg = []
931
+
932
+ roles.each_pair { |role, scopes|
933
+ rolemap = { }
934
+ rolemap["role"] = if !role.is_a?(Integer) and role.match(/^roles\//)
935
+ # generally referring to a canned GCP role
936
+ { "id" => role.to_s }
937
+ elsif role.is_a?(Integer) or role.match(/^\d+$/)
938
+ # If this is a GSuite/Cloud Identity system role, reference it by
939
+ # its human-readable name intead of its numeric id
940
+ role_desc = MU::Cloud::Google::Role.find(cloud_id: role, credentials: credentials).values.first
941
+ if role_desc.is_system_role
942
+ { "id" => role_desc.role_name }
943
+ else
944
+ MU::Config::Ref.get(
945
+ id: role,
946
+ cloud: "Google",
947
+ credentials: credentials,
948
+ type: "roles"
949
+ )
950
+ end
951
+ else
952
+ # Possi-probably something we're declaring elsewhere in this
953
+ # adopted Mu stack
954
+ MU::Config::Ref.get(
955
+ id: role,
956
+ cloud: "Google",
957
+ credentials: credentials,
958
+ type: "roles"
959
+ )
960
+ end
961
+ scopes.each_pair { |scopetype, places|
962
+ if places.size > 0
963
+ rolemap[scopetype] = []
964
+ if scopetype == "organizations" or scopetype == "directories"
965
+ places.each { |org|
966
+ rolemap[scopetype] << ((org == my_org.name and credentials) ? credentials : org)
967
+ }
968
+ else
969
+ places.each { |place|
970
+ mu_type = scopetype == "projects" ? "habitats" : scopetype
971
+ rolemap[scopetype] << MU::Config::Ref.get(
972
+ id: place,
973
+ cloud: "Google",
974
+ credentials: credentials,
975
+ type: mu_type
976
+ )
977
+ }
978
+ end
979
+ end
980
+ }
981
+ role_cfg << rolemap
982
+ }
983
+
984
+ role_cfg
985
+ end
986
+
987
+ # Cloud-specific configuration properties.
988
+ # @param config [MU::Config]: The calling MU::Config object
989
+ # @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource
990
+ def self.schema(config)
991
+ toplevel_required = []
992
+ schema = {
993
+ "name" => {
994
+ "pattern" => '^[a-zA-Z0-9\-\.\/]+$'
995
+ },
996
+ "display_name" => {
997
+ "type" => "string",
998
+ "description" => "A human readable name for this role. If not specified, will default to our long-form deploy-generated name."
999
+ },
1000
+ "role_source" => {
1001
+ "type" => "string",
1002
+ "description" => "Google effectively has four types of roles:
1003
+
1004
+ +directory+: An admin role in GSuite or Cloud Identity
1005
+
1006
+ +org+: A custom organization-level IAM role. Note that these are only valid in GSuite or Cloud Identity environments
1007
+
1008
+ +project+: A custom project-level IAM role.
1009
+
1010
+ +canned+: A reference to one of the standard pre-defined IAM roles, usually only declared to apply {bindings} to other artifacts.
1011
+
1012
+ If this value is not specified, and the role name matches the name of an existing +canned+ role, we will assume it should be +canned+. If it does not, and we have credentials which map to a valid organization, we will assume +org+; if the credentials do not map to an organization, we will assume +project+.",
1013
+ "enum" => ["directory", "org", "project", "canned"]
1014
+ },
1015
+ "description" => {
1016
+ "type" => "string",
1017
+ "description" => "Detailed human-readable description of this role's purpose"
1018
+ },
1019
+ "bindings" => {
1020
+ "type" => "array",
1021
+ "items" => {
1022
+ "type" => "object",
1023
+ "description" => "One or more entities (+user+, +group+, etc) to associate with this role. IAM roles in Google can be associated at the project (+Habitat+), folder, or organization level, so we must specify not only the target entity, but each container in which it is granted to the entity in question.",
1024
+ "properties" => {
1025
+ "entity" => MU::Config::Ref.schema,
1026
+ "projects" => {
1027
+ "type" => "array",
1028
+ "items" => MU::Config::Ref.schema(type: "habitats")
1029
+ },
1030
+ "folders" => {
1031
+ "type" => "array",
1032
+ "items" => MU::Config::Ref.schema(type: "folders")
1033
+ },
1034
+ "organizations" => {
1035
+ "type" => "array",
1036
+ "items" => {
1037
+ "type" => "string",
1038
+ "description" => "Either an organization cloud identifier, like +organizations/123456789012+, or the name of set of Mu credentials, which can be used as an alias to the organization to which they authenticate."
1039
+ }
1040
+ }
1041
+ }
1042
+ }
1043
+ }
1044
+ }
1045
+ [toplevel_required, schema]
1046
+ end
1047
+
1048
+ # Cloud-specific pre-processing of {MU::Config::BasketofKittens::roles}, bare and unvalidated.
1049
+ # @param role [Hash]: The resource to process and validate
1050
+ # @param configurator [MU::Config]: The overall deployment configurator of which this resource is a member
1051
+ # @return [Boolean]: True if validation succeeded, False otherwise
1052
+ def self.validateConfig(role, configurator)
1053
+ ok = true
1054
+
1055
+ credcfg = MU::Cloud::Google.credConfig(role['credentials'])
1056
+
1057
+ my_org = MU::Cloud::Google.getOrg(role['credentials'])
1058
+ if !role['role_source']
1059
+ begin
1060
+ lookup_name = role['name'].dup
1061
+ if !lookup_name.match(/^roles\//)
1062
+ lookup_name = "roles/"+lookup_name
1063
+ end
1064
+ canned = MU::Cloud::Google.iam(credentials: role['credentials']).get_role(lookup_name)
1065
+ MU.log "Role #{role['name']} appears to be a referenced to canned role #{role.name} (#{role.title})", MU::NOTICE
1066
+ role['role_source'] = "canned"
1067
+ rescue ::Google::Apis::ClientError
1068
+ role['role_source'] = my_org ? "org" : "project"
1069
+ end
1070
+ end
1071
+
1072
+ if role['role_source'] == "canned"
1073
+ if role['bindings'].nil? or role['bindings'].empty?
1074
+ MU.log "Role #{role['name']} appears to refer to a canned role, but does not have any bindings declared- this will effectively do nothing.", MU::WARN
1075
+ end
1076
+ end
1077
+
1078
+ if role['role_source'] == "directory"
1079
+
1080
+ if role['import'] and role['import'].size > 0
1081
+ mappings, missing = map_directory_privileges(role['import'], credentials: role['credentials'])
1082
+ if mappings.size == 0
1083
+ MU.log "None of the directory service privileges available to credentials #{role['credentials']} map to the ones declared for role #{role['name']}", MU::ERR, details: role['import'].sort
1084
+ ok = false
1085
+ elsif missing.size > 0
1086
+ MU.log "Some directory service privileges declared for role #{role['name']} aren't available to credentials #{role['credentials']}, will skip", MU::WARN, details: missing
1087
+ end
1088
+ end
1089
+ end
1090
+
1091
+ if role['role_source'] == "directory" or role['role_source'] == "org"
1092
+ if !my_org
1093
+ MU.log "Role #{role['name']} requires an organization/directory, but credential set #{role['credentials']} doesn't appear to have access to one", MU::ERR
1094
+ ok = false
1095
+ end
1096
+ end
1097
+
1098
+ if role['role_source'] == "project"
1099
+ role['project'] ||= MU::Cloud::Google.defaultProject(role['credentials'])
1100
+ if configurator.haveLitterMate?(role['project'], "habitats")
1101
+ role['dependencies'] ||= []
1102
+ role['dependencies'] << {
1103
+ "type" => "habitat",
1104
+ "name" => role['project']
1105
+ }
1106
+ end
1107
+ end
1108
+
1109
+ if role['bindings']
1110
+ role['bindings'].each { |binding|
1111
+ if binding['entity'] and binding['entity']['name'] and
1112
+ configurator.haveLitterMate?(binding['entity']['name'], binding['entity']['type'])
1113
+ role['dependencies'] ||= []
1114
+ role['dependencies'] << {
1115
+ "type" => binding['entity']['type'].sub(/s$/, ''),
1116
+ "name" => binding['entity']['name']
1117
+ }
1118
+
1119
+ end
1120
+ }
1121
+ end
1122
+
1123
+ ok
1124
+ end
1125
+
1126
+ private
1127
+
1128
+ @@service_id_to_name = {}
1129
+ @@service_id_to_privs = {}
1130
+ @@service_name_to_id = {}
1131
+ @@service_name_map_semaphore = Mutex.new
1132
+
1133
+ def self.privilege_service_to_name(credentials = nil)
1134
+
1135
+ customer = MU::Cloud::Google.customerID(credentials)
1136
+ @@service_name_map_semaphore.synchronize {
1137
+ if !@@service_id_to_name[credentials] or
1138
+ !@@service_id_to_privs[credentials] or
1139
+ !@@service_name_to_id[credentials]
1140
+ @@service_id_to_name[credentials] ||= {}
1141
+ @@service_id_to_privs[credentials] ||= {}
1142
+ @@service_name_to_id[credentials] ||= {}
1143
+ resp = MU::Cloud::Google.admin_directory(credentials: credentials).list_privileges(customer)
1144
+
1145
+ def self.id_map_recurse(items, parent_name = nil)
1146
+ id_to_name = {}
1147
+ name_to_id = {}
1148
+ id_to_privs = {}
1149
+
1150
+ items.each { |p|
1151
+ svcname = p.service_name || parent_name
1152
+ if svcname
1153
+ id_to_name[p.service_id] ||= svcname
1154
+ name_to_id[svcname] ||= p.service_id
1155
+ else
1156
+ # MU.log "FREAKING #{p.service_id} HAS NO NAME", MU::WARN
1157
+ end
1158
+ id_to_privs[p.service_id] ||= []
1159
+ id_to_privs[p.service_id] << p.privilege_name
1160
+ if p.child_privileges
1161
+ ids, names, privs = id_map_recurse(p.child_privileges, svcname)
1162
+ id_to_name.merge!(ids)
1163
+ name_to_id.merge!(names)
1164
+ privs.each_pair { |id, childprivs|
1165
+ id_to_privs[id] ||= []
1166
+ id_to_privs[id].concat(childprivs)
1167
+ }
1168
+ end
1169
+ }
1170
+
1171
+ [id_to_name, name_to_id, id_to_privs]
1172
+ end
1173
+
1174
+ @@service_id_to_name[credentials], @@service_id_to_privs[credentials], @@service_name_to_id[credentials] = self.id_map_recurse(resp.items)
1175
+ end
1176
+
1177
+ return [@@service_id_to_name[credentials], @@service_id_to_privs[credentials], @@service_name_to_id[credentials]]
1178
+ }
1179
+ end
1180
+
1181
+ def self.map_directory_privileges(roles, credentials: nil)
1182
+ rolepriv_objs = []
1183
+ notfound = []
1184
+ if roles
1185
+ ids, names, privlist = MU::Cloud::Google::Role.privilege_service_to_name(credentials)
1186
+ roles.each { |p|
1187
+ service, privilege = p.split(/\//)
1188
+ if !names[service] and !ids[service]
1189
+ notfound << service
1190
+ elsif !privlist[names[service]].include?(privilege)
1191
+ notfound << p
1192
+ elsif names[service]
1193
+ rolepriv_objs << MU::Cloud::Google.admin_directory(:Role)::RolePrivilege.new(
1194
+ privilege_name: privilege,
1195
+ service_id: names[service]
1196
+ )
1197
+ else
1198
+ rolepriv_objs << MU::Cloud::Google.admin_directory(:Role)::RolePrivilege.new(
1199
+ privilege_name: privilege,
1200
+ service_id: service
1201
+ )
1202
+ end
1203
+ }
1204
+ end
1205
+ [rolepriv_objs, notfound.uniq.sort]
1206
+ end
1207
+
1208
+ end
1209
+ end
1210
+ end
1211
+ end