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
@@ -15,30 +15,16 @@
15
15
  module MU
16
16
  class Cloud
17
17
  class Google
18
- # Creates an Google project as configured in {MU::Config::BasketofKittens::folders}
18
+ # Creates a Google folder as configured in {MU::Config::BasketofKittens::folders}
19
19
  class Folder < MU::Cloud::Folder
20
- @deploy = nil
21
- @config = nil
22
- @parent = nil
23
-
24
- attr_reader :mu_name
25
- attr_reader :config
26
- attr_reader :cloud_id
27
-
28
- # @param mommacat [MU::MommaCat]: A {MU::Mommacat} object containing the deploy of which this resource is/will be a member.
29
- # @param kitten_cfg [Hash]: The fully parsed and resolved {MU::Config} resource descriptor as defined in {MU::Config::BasketofKittens::folders}
30
- def initialize(mommacat: nil, kitten_cfg: nil, mu_name: nil, cloud_id: nil)
31
- @deploy = mommacat
32
- @config = MU::Config.manxify(kitten_cfg)
33
- @cloud_id ||= cloud_id
34
-
35
- if !mu_name.nil?
36
- @mu_name = mu_name
37
- elsif @config['scrub_mu_isms']
38
- @mu_name = @config['name']
39
- else
40
- @mu_name = @deploy.getResourceName(@config['name'])
41
- end
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 this maybe isn't my job
26
+
27
+ @mu_name ||= @deploy.getResourceName(@config['name'])
42
28
  end
43
29
 
44
30
  # Called automatically by {MU::Deploy#createResources}
@@ -54,6 +40,9 @@ module MU
54
40
  display_name: name_string
55
41
  }
56
42
 
43
+ if @config['parent']['name'] and !@config['parent']['id']
44
+ @config['parent']['deploy_id'] = @deploy.deploy_id
45
+ end
57
46
  parent = MU::Cloud::Google::Folder.resolveParent(@config['parent'], credentials: @config['credentials'])
58
47
 
59
48
  folder_obj = MU::Cloud::Google.folder(:Folder).new(params)
@@ -79,6 +68,20 @@ module MU
79
68
  end
80
69
  end while found.size == 0
81
70
 
71
+ @habitat = parent
72
+
73
+ end
74
+
75
+ # Retrieve the IAM bindings for this folder (associates between IAM roles and groups/users)
76
+ def bindings
77
+ MU::Cloud::Google::Folder.bindings(@cloud_id, credentials: @config['credentials'])
78
+ end
79
+
80
+ # Retrieve the IAM bindings for this folder (associates between IAM roles and groups/users)
81
+ # @param folder [String]:
82
+ # @param credentials [String]:
83
+ def self.bindings(folder, credentials: nil)
84
+ MU::Cloud::Google.folder(credentials: credentials).get_folder_iam_policy(folder).bindings
82
85
  end
83
86
 
84
87
  # Given a {MU::Config::Folder.reference} configuration block, resolve
@@ -88,9 +91,9 @@ module MU
88
91
  # @return [String]
89
92
  def self.resolveParent(parentblock, credentials: nil)
90
93
  my_org = MU::Cloud::Google.getOrg(credentials)
91
- if !parentblock or parentblock['id'] == my_org.name or
94
+ if my_org and (!parentblock or parentblock['id'] == my_org.name or
92
95
  parentblock['name'] == my_org.display_name or (parentblock['id'] and
93
- "organizations/"+parentblock['id'] == my_org.name)
96
+ "organizations/"+parentblock['id'] == my_org.name))
94
97
  return my_org.name
95
98
  end
96
99
 
@@ -103,12 +106,12 @@ module MU
103
106
  name: parentblock['name']
104
107
  ).first
105
108
  if sib_folder
106
- return "folders/"+sib_folder.cloudobj.cloud_id
109
+ return sib_folder.cloud_desc.name
107
110
  end
108
111
  end
109
112
 
110
113
  begin
111
- found = MU::Cloud::Google::Folder.find(cloud_id: parentblock['id'], credentials: credentials, flags: { 'display_name' => parentblock['name'] })
114
+ found = MU::Cloud::Google::Folder.find(cloud_id: parentblock['id'], credentials: credentials, flags: { 'display_name' => parentblock['name'] })
112
115
  rescue ::Google::Apis::ClientError => e
113
116
  if !e.message.match(/Invalid request status_code: 404/)
114
117
  raise e
@@ -123,11 +126,14 @@ module MU
123
126
  end
124
127
 
125
128
  # Return the cloud descriptor for the Folder
129
+ # @return [Google::Apis::Core::Hashable]
126
130
  def cloud_desc
127
- MU::Cloud::Google::Folder.find(cloud_id: @cloud_id).values.first
131
+ @cached_cloud_desc ||= MU::Cloud::Google::Folder.find(cloud_id: @cloud_id, credentials: @config['credentials']).values.first
132
+ @habitat_id ||= @cached_cloud_desc.parent.sub(/^(folders|organizations)\//, "")
133
+ @cached_cloud_desc
128
134
  end
129
135
 
130
- # Return the metadata for this project's configuration
136
+ # Return the metadata for this folders's configuration
131
137
  # @return [Hash]
132
138
  def notify
133
139
  desc = MU.structToHash(MU::Cloud::Google.folder(credentials: @config['credentials']).get_folder("folders/"+@cloud_id))
@@ -147,7 +153,7 @@ module MU
147
153
  # Denote whether this resource implementation is experiment, ready for
148
154
  # testing, or ready for production use.
149
155
  def self.quality
150
- MU::Cloud::BETA
156
+ MU::Cloud::RELEASE
151
157
  end
152
158
 
153
159
  # Remove all Google projects associated with the currently loaded deployment. Try to, anyway.
@@ -158,54 +164,70 @@ module MU
158
164
  # We can't label GCP folders, and their names are too short to encode
159
165
  # Mu deploy IDs, so all we can do is rely on flags['known'] passed in
160
166
  # from cleanup, which relies on our metadata to know what's ours.
161
-
167
+ #noop = true
162
168
  if flags and flags['known']
169
+ threads = []
163
170
  flags['known'].each { |cloud_id|
164
- found = self.find(cloud_id: cloud_id, credentials: credentials)
165
- if found.size > 0 and found.values.first.lifecycle_state == "ACTIVE"
166
- MU.log "Deleting folder #{found.values.first.display_name} (#{found.keys.first})"
167
- if !noop
168
- max_retries = 10
169
- retries = 0
170
- success = false
171
- begin
172
- MU::Cloud::Google.folder(credentials: credentials).delete_folder(
173
- "folders/"+found.keys.first
174
- )
175
- found = self.find(cloud_id: cloud_id, credentials: credentials)
176
- if found and found.size > 0 and found.values.first.lifecycle_state != "DELETE_REQUESTED"
177
- if retries < max_retries
171
+ threads << Thread.new {
172
+
173
+ found = self.find(cloud_id: cloud_id, credentials: credentials)
174
+
175
+ if found.size > 0 and found.values.first.lifecycle_state == "ACTIVE"
176
+ MU.log "Deleting folder #{found.values.first.display_name} (#{found.keys.first})"
177
+ if !noop
178
+ max_retries = 10
179
+ retries = 0
180
+ success = false
181
+ begin
182
+ MU::Cloud::Google.folder(credentials: credentials).delete_folder(
183
+ "folders/"+found.keys.first
184
+ )
185
+ found = self.find(cloud_id: cloud_id, credentials: credentials)
186
+ if found and found.size > 0 and found.values.first.lifecycle_state != "DELETE_REQUESTED"
187
+ if retries < max_retries
188
+ sleep 30
189
+ retries += 1
190
+ puts retries
191
+ else
192
+ MU.log "Folder #{cloud_id} still exists after #{max_retries.to_s} attempts to delete", MU::ERR
193
+ break
194
+ end
195
+ else
196
+ success = true
197
+ end
198
+
199
+ rescue ::Google::Apis::ClientError => e
200
+ # XXX maybe see if the folder has disappeared already?
201
+ # XXX look for child folders that haven't been deleted, that's what this tends
202
+ # to mean
203
+ if e.message.match(/failedPrecondition/) and retries < max_retries
178
204
  sleep 30
179
205
  retries += 1
180
- puts retries
206
+ retry
181
207
  else
182
- MU.log "Folder #{cloud_id} still exists after #{max_retries.to_s} attempts to delete", MU::ERR
208
+ MU.log "Got 'failedPrecondition' a bunch while trying to delete #{found.values.first.display_name} (#{found.keys.first})", MU::ERR
183
209
  break
184
210
  end
185
- else
186
- success = true
187
- end
188
-
189
- rescue ::Google::Apis::ClientError => e
190
- if e.message.match(/failedPrecondition/) and retries < max_retries
191
- sleep 30
192
- retries += 1
193
- retry
194
- else
195
- raise e
196
- end
197
- end while !success
211
+ end while !success
212
+ end
198
213
  end
199
- end
214
+ }
215
+ }
216
+ threads.each { |t|
217
+ t.join
200
218
  }
201
219
  end
202
220
  end
203
221
 
204
- # Locate an existing project
205
- # @param cloud_id [String]: The cloud provider's identifier for this resource.
206
- # @param flags [Hash]: Optional flags
207
- # @return [OpenStruct]: The cloud provider's complete descriptions of matching project
208
- def self.find(cloud_id: nil, credentials: nil, flags: {}, tag_key: nil, tag_value: nil)
222
+ # Locate and return cloud provider descriptors of this resource type
223
+ # which match the provided parameters, or all visible resources if no
224
+ # filters are specified. At minimum, implementations of +find+ must
225
+ # honor +credentials+ and +cloud_id+ arguments. We may optionally
226
+ # support other search methods, such as +tag_key+ and +tag_value+, or
227
+ # cloud-specific arguments like +project+. See also {MU::MommaCat.findStray}.
228
+ # @param args [Hash]: Hash of named arguments passed via Ruby's double-splat
229
+ # @return [Hash<String,OpenStruct>]: The cloud provider's complete descriptions of matching resources
230
+ def self.find(**args)
209
231
  found = {}
210
232
 
211
233
  # Recursively search a GCP folder hierarchy for a folder matching our
@@ -227,33 +249,88 @@ module MU
227
249
  nil
228
250
  end
229
251
 
230
- if cloud_id
231
- found[cloud_id.sub(/^folders\//, "")] = MU::Cloud::Google.folder(credentials: credentials).get_folder("folders/"+cloud_id.sub(/^folders\//, ""))
232
- elsif flags['display_name']
233
- parent = if flags['parent_id']
234
- flags['parent_id']
235
- else
236
- my_org = MU::Cloud::Google.getOrg(credentials)
237
- my_org.name
238
- end
252
+ parent = if args[:flags] and args[:flags]['parent_id']
253
+ args[:flags]['parent_id']
254
+ else
255
+ my_org = MU::Cloud::Google.getOrg(args[:credentials])
256
+ my_org.name
257
+ end
258
+
259
+ if args[:cloud_id]
260
+ raw_id = args[:cloud_id].sub(/^folders\//, "")
261
+ resp = MU::Cloud::Google.folder(credentials: args[:credentials]).get_folder("folders/"+raw_id)
262
+ found[resp.name] = resp if resp
263
+
264
+ elsif args[:flags] and args[:flags]['display_name']
239
265
 
240
266
  if parent
241
- resp = self.find_matching_folder(parent, name: flags['display_name'], credentials: credentials)
267
+ resp = self.find_matching_folder(parent, name: args[:flags]['display_name'], credentials: args[:credentials])
242
268
  if resp
243
- found[resp.name.sub(/^folders\//, "")] = resp
269
+ found[resp.name] = resp
244
270
  end
245
271
  end
272
+ else
273
+ resp = MU::Cloud::Google.folder(credentials: args[:credentials]).list_folders(parent: parent)
274
+ if resp and resp.folders
275
+ resp.folders.each { |folder|
276
+ next if folder.lifecycle_state == "DELETE_REQUESTED"
277
+ found[folder.name] = folder
278
+ # recurse so that we'll pick up child folders
279
+ children = self.find(
280
+ credentials: args[:credentials],
281
+ flags: { 'parent_id' => folder.name }
282
+ )
283
+ if !children.nil? and !children.empty?
284
+ found.merge!(children)
285
+ end
286
+ }
287
+ end
246
288
  end
247
-
289
+
248
290
  found
249
291
  end
250
292
 
293
+ # Reverse-map our cloud description into a runnable config hash.
294
+ # We assume that any values we have in +@config+ are placeholders, and
295
+ # calculate our own accordingly based on what's live in the cloud.
296
+ def toKitten(rootparent: nil, billing: nil, habitats: nil)
297
+ bok = {
298
+ "cloud" => "Google",
299
+ "credentials" => @config['credentials']
300
+ }
301
+
302
+ bok['display_name'] = cloud_desc.display_name
303
+ bok['cloud_id'] = cloud_desc.name
304
+ bok['name'] = cloud_desc.display_name#+bok['cloud_id'] # only way to guarantee uniqueness
305
+ if cloud_desc.parent.match(/^folders\/(.*)/)
306
+ MU.log bok['display_name']+" generating reference", MU::NOTICE, details: cloud_desc.parent
307
+ bok['parent'] = MU::Config::Ref.get(
308
+ id: cloud_desc.parent,
309
+ cloud: "Google",
310
+ credentials: @config['credentials'],
311
+ type: "folders"
312
+ )
313
+ elsif rootparent
314
+ bok['parent'] = {
315
+ 'id' => rootparent.is_a?(String) ? rootparent : rootparent.cloud_desc.name
316
+ }
317
+ else
318
+ bok['parent'] = { 'id' => cloud_desc.parent }
319
+ end
320
+
321
+ bok
322
+ end
323
+
251
324
  # Cloud-specific configuration properties.
252
325
  # @param config [MU::Config]: The calling MU::Config object
253
326
  # @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource
254
327
  def self.schema(config)
255
328
  toplevel_required = []
256
329
  schema = {
330
+ "display_name" => {
331
+ "type" => "string",
332
+ "description" => "The +display_name+ field of this folder, specified only if we want it to be something other than the automatically-generated string derived from the +name+ field.",
333
+ }
257
334
  }
258
335
  [toplevel_required, schema]
259
336
  end
@@ -266,7 +343,7 @@ module MU
266
343
  ok = true
267
344
 
268
345
  if !MU::Cloud::Google.getOrg(folder['credentials'])
269
- 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
346
+ MU.log "Cannot manage Google Cloud folders in environments without an organization", MU::ERR
270
347
  ok = false
271
348
  end
272
349
 
@@ -17,36 +17,102 @@ module MU
17
17
  class Google
18
18
  # A group as configured in {MU::Config::BasketofKittens::groups}
19
19
  class Group < MU::Cloud::Group
20
- @deploy = nil
21
- @config = nil
22
- attr_reader :mu_name
23
- attr_reader :config
24
- attr_reader :cloud_id
25
-
26
- # @param mommacat [MU::MommaCat]: A {MU::Mommacat} object containing the deploy of which this resource is/will be a member.
27
- # @param kitten_cfg [Hash]: The fully parsed and resolved {MU::Config} resource descriptor as defined in {MU::Config::BasketofKittens::groups}
28
- def initialize(mommacat: nil, kitten_cfg: nil, mu_name: nil, cloud_id: nil)
29
- @deploy = mommacat
30
- @config = MU::Config.manxify(kitten_cfg)
31
- @cloud_id ||= cloud_id
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
+
32
26
  @mu_name ||= @deploy.getResourceName(@config["name"])
33
27
  end
34
28
 
35
29
  # Called automatically by {MU::Deploy#createResources}
36
30
  def create
37
- bind_group
31
+ if !@config['external']
32
+ if !@config['email']
33
+ domains = MU::Cloud::Google.admin_directory(credentials: @credentials).list_domains(@customer)
34
+ @config['email'] = @mu_name.downcase+"@"+domains.domains.first.domain_name
35
+ end
36
+ group_obj = MU::Cloud::Google.admin_directory(:Group).new(
37
+ name: @mu_name,
38
+ email: @config['email'],
39
+ description: @deploy.deploy_id
40
+ )
41
+
42
+ MU.log "Creating group #{@mu_name}", details: group_obj
43
+
44
+ resp = MU::Cloud::Google.admin_directory(credentials: @credentials).insert_group(group_obj)
45
+ @cloud_id = resp.email
46
+
47
+ MU::Cloud::Google::Role.bindFromConfig("group", @cloud_id, @config['roles'], credentials: @config['credentials'])
48
+ else
49
+ @cloud_id = @config['name'].sub(/@.*/, "")+"@"+@config['domain']
50
+ end
38
51
  end
39
52
 
40
53
  # Called automatically by {MU::Deploy#createResources}
41
54
  def groom
42
- bind_group
55
+ MU::Cloud::Google::Role.bindFromConfig("group", @cloud_id, @config['roles'], credentials: @config['credentials'], debug: true)
56
+
57
+ if @config['members']
58
+ resolved_desired = []
59
+ @config['members'].each { |m|
60
+ sibling_user = @deploy.findLitterMate(name: m, type: "users")
61
+ usermail = if sibling_user
62
+ sibling_user.cloud_id
63
+ elsif !m.match(/@/)
64
+ domains = MU::Cloud::Google.admin_directory(credentials: @credentials).list_domains(@customer)
65
+ m+"@"+domains.domains.first.domain_name
66
+ else
67
+ m
68
+ end
69
+ resolved_desired << usermail
70
+ next if members.include?(usermail)
71
+ MU.log "Adding user #{usermail} to group #{@mu_name}"
72
+ MU::Cloud::Google.admin_directory(credentials: @credentials).insert_member(
73
+ @cloud_id,
74
+ MU::Cloud::Google.admin_directory(:Member).new(
75
+ email: usermail
76
+ )
77
+ )
78
+ }
79
+
80
+ deletia = members - resolved_desired
81
+ deletia.each { |m|
82
+ MU.log "Removing user #{m} from group #{@mu_name}", MU::NOTICE
83
+ MU::Cloud::Google.admin_directory(credentials: @credentials).delete_member(@cloud_id, m)
84
+ }
85
+
86
+ # Theoretically there can be a delay
87
+ begin
88
+ if members.sort != resolved_desired.sort
89
+ sleep 3
90
+ end
91
+ end while members.sort != resolved_desired.sort
92
+ end
93
+
94
+ end
95
+
96
+ # Retrieve a list of users (by cloud id) of this group
97
+ def members
98
+ resp = MU::Cloud::Google.admin_directory(credentials: @credentials).list_members(@cloud_id)
99
+ members = []
100
+ if resp and resp.members
101
+ members = resp.members.map { |m| m.email }
102
+ # XXX reject status != "ACTIVE" ?
103
+ end
104
+ members
43
105
  end
44
106
 
45
107
  # Return the metadata for this group configuration
46
108
  # @return [Hash]
47
109
  def notify
48
- {
49
- }
110
+ if !@config['external']
111
+ base = MU.structToHash(cloud_desc)
112
+ end
113
+ base ||= {}
114
+
115
+ base
50
116
  end
51
117
 
52
118
  # Does this resource type exist as a global (cloud-wide) artifact, or
@@ -56,10 +122,18 @@ module MU
56
122
  true
57
123
  end
58
124
 
125
+ # Return the list of "container" resource types in which this resource
126
+ # can reside. The list will include an explicit nil if this resource
127
+ # can exist outside of any container.
128
+ # @return [Array<Symbol,nil>]
129
+ def self.canLiveIn
130
+ [nil]
131
+ end
132
+
59
133
  # Denote whether this resource implementation is experiment, ready for
60
134
  # testing, or ready for production use.
61
135
  def self.quality
62
- MU::Cloud::ALPHA
136
+ MU::Cloud::BETA
63
137
  end
64
138
 
65
139
  # Remove all groups associated with the currently loaded deployment.
@@ -68,19 +142,90 @@ module MU
68
142
  # @param region [String]: The cloud provider region
69
143
  # @return [void]
70
144
  def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
145
+ my_domains = MU::Cloud::Google.getDomains(credentials)
146
+ my_org = MU::Cloud::Google.getOrg(credentials)
147
+
148
+ if my_org
149
+ groups = MU::Cloud::Google.admin_directory(credentials: credentials).list_groups(customer: MU::Cloud::Google.customerID(credentials)).groups
150
+ if groups
151
+ groups.each { |group|
152
+ if group.description == MU.deploy_id
153
+ MU.log "Deleting group #{group.name} from #{my_org.display_name}", details: group
154
+ if !noop
155
+ MU::Cloud::Google.admin_directory(credentials: credentials).delete_group(group.id)
156
+ end
157
+ end
158
+ }
159
+ end
160
+ end
161
+
162
+ if flags['known']
163
+ flags['known'].each { |group|
164
+ MU::Cloud::Google::Role.removeBindings("group", group, credentials: credentials, noop: noop)
165
+ }
166
+ end
71
167
  end
72
168
 
73
- # Locate an existing group group.
74
- # @param cloud_id [String]: The cloud provider's identifier for this resource.
75
- # @param region [String]: The cloud provider region.
76
- # @param flags [Hash]: Optional flags
77
- # @return [OpenStruct]: The cloud provider's complete descriptions of matching group group.
78
- def self.find(cloud_id: nil, region: MU.curRegion, credentials: nil, flags: {}, tag_key: nil, tag_value: nil)
79
- flags["project"] ||= MU::Cloud::Google.defaultProject(credentials)
80
- found = nil
169
+ # Locate and return cloud provider descriptors of this resource type
170
+ # which match the provided parameters, or all visible resources if no
171
+ # filters are specified. At minimum, implementations of +find+ must
172
+ # honor +credentials+ and +cloud_id+ arguments. We may optionally
173
+ # support other search methods, such as +tag_key+ and +tag_value+, or
174
+ # cloud-specific arguments like +project+. See also {MU::MommaCat.findStray}.
175
+ # @param args [Hash]: Hash of named arguments passed via Ruby's double-splat
176
+ # @return [Hash<String,OpenStruct>]: The cloud provider's complete descriptions of matching resources
177
+ def self.find(**args)
178
+ found = {}
179
+
180
+ # The API treats the email address field as its main identifier, so
181
+ # we'll go ahead and respect that.
182
+ if args[:cloud_id]
183
+ begin
184
+ resp = MU::Cloud::Google.admin_directory(credentials: args[:credentials]).get_group(args[:cloud_id])
185
+ found[resp.email] = resp if resp
186
+ rescue ::Google::Apis::ClientError => e
187
+ raise e if !e.message.match(/forbidden: /)
188
+ end
189
+ else
190
+ resp = MU::Cloud::Google.admin_directory(credentials: args[:credentials]).list_groups(customer: MU::Cloud::Google.customerID(args[:credentials]))
191
+ if resp and resp.groups
192
+ found = Hash[resp.groups.map { |g| [g.email, g] }]
193
+ end
194
+ end
195
+ # XXX what about Google Groups groups and other external groups? Where do we fish for those? Do we even need to?
81
196
  found
82
197
  end
83
198
 
199
+ # Reverse-map our cloud description into a runnable config hash.
200
+ # We assume that any values we have in +@config+ are placeholders, and
201
+ # calculate our own accordingly based on what's live in the cloud.
202
+ def toKitten(rootparent: nil, billing: nil, habitats: nil)
203
+
204
+ bok = {
205
+ "cloud" => "Google",
206
+ "credentials" => @config['credentials']
207
+ }
208
+
209
+ bok['name'] = cloud_desc.name
210
+ bok['cloud_id'] = cloud_desc.email
211
+ bok['members'] = members
212
+ bok['members'].each { |m|
213
+ m = MU::Config::Ref.get(
214
+ id: m,
215
+ cloud: "Google",
216
+ credentials: @config['credentials'],
217
+ type: "users"
218
+ )
219
+ }
220
+ group_roles = MU::Cloud::Google::Role.getAllBindings(@config['credentials'])["by_entity"]
221
+ if group_roles["group"] and group_roles["group"][bok['cloud_id']] and
222
+ group_roles["group"][bok['cloud_id']].size > 0
223
+ bok['roles'] = MU::Cloud::Google::Role.entityBindingsToSchema(group_roles["group"][bok['cloud_id']], credentials: @config['credentials'])
224
+ end
225
+
226
+ bok
227
+ end
228
+
84
229
  # Cloud-specific configuration properties.
85
230
  # @param config [MU::Config]: The calling MU::Config object
86
231
  # @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource
@@ -89,20 +234,32 @@ module MU
89
234
  schema = {
90
235
  "name" => {
91
236
  "type" => "string",
92
- "description" => "This must be the email address of an existing Google Group (+foo@googlegroups.com+), or of a federated GSuite or Cloud Identity domain group from your organization."
237
+ "description" => "This can include an optional @domain component (<tt>foo@example.com</tt>).
238
+
239
+ If the domain portion is not specified, and we manage exactly one GSuite or Cloud Identity domain, we will attempt to create the group in that domain.
240
+
241
+ If we do not manage any domains, and none are specified, we will assume <tt>@googlegroups.com</tt> for the domain and attempt to bind an existing external Google Group to roles under our jurisdiction.
242
+
243
+ If the domain portion is specified, and our credentials can manage that domain via GSuite or Cloud Identity, we will attempt to create the group in that domain.
244
+
245
+ If it is a domain we do not manage, we will attempt to bind an existing external group from that domain to roles under our jurisdiction.
246
+
247
+ If we are binding (rather than creating) a group and no roles are specified, we will default to +roles/viewer+ at the organization scope. If our credentials do not manage an organization, we will grant this role in our default project.
248
+
249
+ "
250
+ },
251
+ "domain" => {
252
+ "type" => "string",
253
+ "description" => "The domain from which the group originates or in which it should be created. This can instead be embedded in the {name} field: +foo@example.com+."
93
254
  },
255
+ "external" => {
256
+ "type" => "boolean",
257
+ "description" => "Explicitly flag this group as originating from an external domain. This should always autodetect correctly."
258
+ },
259
+
94
260
  "roles" => {
95
261
  "type" => "array",
96
- "description" => "One or more Google IAM roles to associate with this group.",
97
- "default" => ["roles/viewer"],
98
- "items" => {
99
- "type" => "string",
100
- "description" => "One or more Google IAM roles to associate with this group. Google Cloud groups are not created directly; pre-existing Google Groups are associated with a project by being bound to one or more roles in that project. If no roles are specified, we default to +roles/viewer+, which permits read-only access project-wide."
101
- }
102
- },
103
- "project" => {
104
- "type" => "string",
105
- "description" => "The project into which to deploy resources"
262
+ "items" => MU::Cloud::Google::Role.ref_schema
106
263
  }
107
264
  }
108
265
  [toplevel_required, schema]
@@ -114,64 +271,97 @@ module MU
114
271
  # @return [Boolean]: True if validation succeeded, False otherwise
115
272
  def self.validateConfig(group, configurator)
116
273
  ok = true
117
- if group['members'] and group['members'].size > 0 and
118
- !$MU_CFG['google']['masquerade_as']
119
- MU.log "Cannot change Google group memberships in non-GSuite environments.\nVisit https://groups.google.com to manage groups.", MU::ERR
120
- ok = false
121
- end
122
274
 
123
- ok
124
- end
275
+ my_domains = MU::Cloud::Google.getDomains(group['credentials'])
276
+ my_org = MU::Cloud::Google.getOrg(group['credentials'])
125
277
 
126
- private
278
+ if group['name'].match(/@(.*+)$/)
279
+ domain = Regexp.last_match[1].downcase
280
+ if domain and group['domain'] and domain != group['domain'].downcase
281
+ MU.log "Group #{group['name']} had a domain component, but the domain field was also specified (#{group['domain']}) and they don't match."
282
+ ok = false
283
+ end
284
+ group['domain'] = domain
285
+
286
+ if !my_domains or !my_domains.include?(domain)
287
+ group['external'] = true
127
288
 
128
- def bind_group
129
- bindings = []
130
- ext_policy = MU::Cloud::Google.resource_manager(credentials: @config['credentials']).get_project_iam_policy(
131
- @config['project']
132
- )
133
-
134
- change_needed = false
135
- @config['roles'].each { |role|
136
- seen = false
137
- ext_policy.bindings.each { |b|
138
- if b.role == role
139
- seen = true
140
- if !b.members.include?("user:"+@config['name'])
141
- change_needed = true
142
- b.members << "group:"+@config['name']
289
+ if !["googlegroups.com", "google.com"].include?(domain)
290
+ MU.log "#{group['name']} appears to be a member of a domain that our credentials (#{group['credentials']}) do not manage; attempts to grant access for this group may fail!", MU::WARN
291
+ end
292
+
293
+ if !group['roles'] or group['roles'].empty?
294
+ group['roles'] = [
295
+ {
296
+ "role" => {
297
+ "id" => "roles/viewer"
298
+ }
299
+ }
300
+ ]
301
+ if my_org
302
+ group['roles'][0]["organizations"] = [my_org.name]
303
+ else
304
+ group['roles'][0]["projects"] = {
305
+ "id" => group["project"]
306
+ }
143
307
  end
308
+ MU.log "External Google group specified with no role binding, will grant 'viewer' in #{my_org ? "organization #{my_org.display_name}" : "project #{group['project']}"}", MU::WARN
144
309
  end
145
- }
146
- if !seen
147
- ext_policy.bindings << MU::Cloud::Google.resource_manager(:Binding).new(
148
- role: role,
149
- members: ["group:"+@config['name']]
150
- )
151
- change_needed = true
152
310
  end
153
- }
311
+ else
312
+ if !group['domain']
313
+ if my_domains.size == 1
314
+ group['domain'] = my_domains.first
315
+ elsif my_domains.size > 1
316
+ MU.log "Google interactive User #{group['name']} did not specify a domain, and we have multiple defaults available. Must specify exactly one.", MU::ERR, details: my_domains
317
+ ok = false
318
+ else
319
+ group['domain'] = "googlegroups.com"
320
+ end
321
+ end
322
+ end
154
323
 
155
- if change_needed
156
- req_obj = MU::Cloud::Google.resource_manager(:SetIamPolicyRequest).new(
157
- policy: ext_policy
158
- )
159
- MU.log "Adding group #{@config['name']} to Google Cloud project #{@config['project']}", details: @config['roles']
160
324
 
161
- begin
162
- MU::Cloud::Google.resource_manager(credentials: @config['credentials']).set_project_iam_policy(
163
- @config['project'],
164
- req_obj
165
- )
166
- rescue ::Google::Apis::ClientError => e
167
- if e.message.match(/does not exist/i) and !MU::Cloud::Google.credConfig(@config['credentials'])['masquerade_as']
168
- raise MuError, "Group #{@config['name']} does not exist, and we cannot create Google groups in non-GSuite environments.\nVisit https://groups.google.com to manage groups."
169
- end
170
- raise e
325
+ credcfg = MU::Cloud::Google.credConfig(group['credentials'])
326
+
327
+ if group['external'] and group['members']
328
+ MU.log "Cannot manage memberships for external group #{group['name']}", MU::ERR
329
+ if group['domain'] == "googlegroups.com"
330
+ MU.log "Visit https://groups.google.com to manage Google Groups.", MU::ERR
171
331
  end
332
+ ok = false
172
333
  end
334
+
335
+ if group['members']
336
+ group['members'].each { |m|
337
+ if configurator.haveLitterMate?(m, "users")
338
+ group['dependencies'] ||= []
339
+ group['dependencies'] << {
340
+ "name" => m,
341
+ "type" => "user"
342
+ }
343
+ end
344
+ }
345
+ end
346
+
347
+ if group['roles']
348
+ group['roles'].each { |r|
349
+ if r['role'] and r['role']['name'] and
350
+ (!r['role']['deploy_id'] and !r['role']['id'])
351
+ group['dependencies'] ||= []
352
+ group['dependencies'] << {
353
+ "type" => "role",
354
+ "name" => r['role']['name']
355
+ }
356
+ end
357
+ }
358
+ end
359
+
360
+ ok
173
361
  end
174
362
 
363
+ private
364
+
175
365
  end
176
366
  end
177
367
  end