cloud-mu 2.1.0beta → 3.0.0beta

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