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.
- checksums.yaml +5 -5
- data/Berksfile +4 -5
- data/Berksfile.lock +179 -0
- data/README.md +1 -6
- data/ansible/roles/geerlingguy.firewall/templates/firewall.bash.j2 +0 -0
- data/ansible/roles/mu-installer/README.md +33 -0
- data/ansible/roles/mu-installer/defaults/main.yml +2 -0
- data/ansible/roles/mu-installer/handlers/main.yml +2 -0
- data/ansible/roles/mu-installer/meta/main.yml +60 -0
- data/ansible/roles/mu-installer/tasks/main.yml +13 -0
- data/ansible/roles/mu-installer/tests/inventory +2 -0
- data/ansible/roles/mu-installer/tests/test.yml +5 -0
- data/ansible/roles/mu-installer/vars/main.yml +2 -0
- data/bin/mu-adopt +125 -0
- data/bin/mu-aws-setup +4 -4
- data/bin/mu-azure-setup +265 -0
- data/bin/mu-azure-tests +43 -0
- data/bin/mu-cleanup +20 -8
- data/bin/mu-configure +224 -98
- data/bin/mu-deploy +8 -3
- data/bin/mu-gcp-setup +16 -8
- data/bin/mu-gen-docs +92 -8
- data/bin/mu-load-config.rb +52 -12
- data/bin/mu-momma-cat +36 -0
- data/bin/mu-node-manage +34 -27
- data/bin/mu-self-update +2 -2
- data/bin/mu-ssh +12 -8
- data/bin/mu-upload-chef-artifacts +11 -4
- data/bin/mu-user-manage +3 -0
- data/cloud-mu.gemspec +8 -11
- data/cookbooks/firewall/libraries/helpers_iptables.rb +2 -2
- data/cookbooks/firewall/metadata.json +1 -1
- data/cookbooks/firewall/recipes/default.rb +5 -9
- data/cookbooks/mu-firewall/attributes/default.rb +2 -0
- data/cookbooks/mu-firewall/metadata.rb +1 -1
- data/cookbooks/mu-glusterfs/templates/default/mu-gluster-client.erb +0 -0
- data/cookbooks/mu-master/Berksfile +2 -2
- data/cookbooks/mu-master/files/default/check_mem.pl +0 -0
- data/cookbooks/mu-master/files/default/cloudamatic.png +0 -0
- data/cookbooks/mu-master/metadata.rb +5 -4
- data/cookbooks/mu-master/recipes/389ds.rb +1 -1
- data/cookbooks/mu-master/recipes/basepackages.rb +30 -10
- data/cookbooks/mu-master/recipes/default.rb +59 -7
- data/cookbooks/mu-master/recipes/firewall-holes.rb +1 -1
- data/cookbooks/mu-master/recipes/init.rb +65 -47
- data/cookbooks/mu-master/recipes/{eks-kubectl.rb → kubectl.rb} +4 -10
- data/cookbooks/mu-master/recipes/sssd.rb +2 -1
- data/cookbooks/mu-master/recipes/update_nagios_only.rb +6 -6
- data/cookbooks/mu-master/templates/default/web_app.conf.erb +2 -2
- data/cookbooks/mu-master/templates/mods/ldap.conf.erb +4 -0
- data/cookbooks/mu-php54/Berksfile +1 -2
- data/cookbooks/mu-php54/metadata.rb +4 -5
- data/cookbooks/mu-php54/recipes/default.rb +1 -1
- data/cookbooks/mu-splunk/templates/default/splunk-init.erb +0 -0
- data/cookbooks/mu-tools/Berksfile +3 -2
- data/cookbooks/mu-tools/files/default/Mu_CA.pem +33 -0
- data/cookbooks/mu-tools/libraries/helper.rb +20 -8
- data/cookbooks/mu-tools/metadata.rb +5 -2
- data/cookbooks/mu-tools/recipes/apply_security.rb +2 -3
- data/cookbooks/mu-tools/recipes/eks.rb +1 -1
- data/cookbooks/mu-tools/recipes/gcloud.rb +5 -30
- data/cookbooks/mu-tools/recipes/nagios.rb +1 -1
- data/cookbooks/mu-tools/recipes/rsyslog.rb +1 -0
- data/cookbooks/mu-tools/recipes/selinux.rb +19 -0
- data/cookbooks/mu-tools/recipes/split_var_partitions.rb +0 -1
- data/cookbooks/mu-tools/recipes/windows-client.rb +256 -122
- data/cookbooks/mu-tools/resources/disk.rb +3 -1
- data/cookbooks/mu-tools/templates/amazon/sshd_config.erb +1 -1
- data/cookbooks/mu-tools/templates/default/etc_hosts.erb +1 -1
- data/cookbooks/mu-tools/templates/default/{kubeconfig.erb → kubeconfig-eks.erb} +0 -0
- data/cookbooks/mu-tools/templates/default/kubeconfig-gke.erb +27 -0
- data/cookbooks/mu-tools/templates/windows-10/sshd_config.erb +137 -0
- data/cookbooks/mu-utility/recipes/nat.rb +4 -0
- data/extras/alpha.png +0 -0
- data/extras/beta.png +0 -0
- data/extras/clean-stock-amis +2 -2
- data/extras/generate-stock-images +131 -0
- data/extras/git-fix-permissions-hook +0 -0
- data/extras/image-generators/AWS/centos6.yaml +17 -0
- data/extras/image-generators/{aws → AWS}/centos7-govcloud.yaml +0 -0
- data/extras/image-generators/{aws → AWS}/centos7.yaml +0 -0
- data/extras/image-generators/{aws → AWS}/rhel7.yaml +0 -0
- data/extras/image-generators/{aws → AWS}/win2k12.yaml +0 -0
- data/extras/image-generators/{aws → AWS}/win2k16.yaml +0 -0
- data/extras/image-generators/{aws → AWS}/windows.yaml +0 -0
- data/extras/image-generators/{gcp → Google}/centos6.yaml +1 -0
- data/extras/image-generators/Google/centos7.yaml +18 -0
- data/extras/python_rpm/build.sh +0 -0
- data/extras/release.png +0 -0
- data/extras/ruby_rpm/build.sh +0 -0
- data/extras/ruby_rpm/muby.spec +1 -1
- data/install/README.md +43 -5
- data/install/deprecated-bash-library.sh +0 -0
- data/install/installer +1 -1
- data/install/jenkinskeys.rb +0 -0
- data/install/mu-master.yaml +55 -0
- data/modules/mommacat.ru +41 -7
- data/modules/mu.rb +444 -149
- data/modules/mu/adoption.rb +500 -0
- data/modules/mu/cleanup.rb +235 -158
- data/modules/mu/cloud.rb +675 -138
- data/modules/mu/clouds/aws.rb +156 -24
- data/modules/mu/clouds/aws/alarm.rb +4 -14
- data/modules/mu/clouds/aws/bucket.rb +60 -18
- data/modules/mu/clouds/aws/cache_cluster.rb +8 -20
- data/modules/mu/clouds/aws/collection.rb +12 -22
- data/modules/mu/clouds/aws/container_cluster.rb +209 -118
- data/modules/mu/clouds/aws/database.rb +120 -45
- data/modules/mu/clouds/aws/dnszone.rb +7 -18
- data/modules/mu/clouds/aws/endpoint.rb +5 -15
- data/modules/mu/clouds/aws/firewall_rule.rb +144 -72
- data/modules/mu/clouds/aws/folder.rb +4 -11
- data/modules/mu/clouds/aws/function.rb +6 -16
- data/modules/mu/clouds/aws/group.rb +4 -12
- data/modules/mu/clouds/aws/habitat.rb +11 -13
- data/modules/mu/clouds/aws/loadbalancer.rb +40 -28
- data/modules/mu/clouds/aws/log.rb +5 -13
- data/modules/mu/clouds/aws/msg_queue.rb +9 -24
- data/modules/mu/clouds/aws/nosqldb.rb +4 -12
- data/modules/mu/clouds/aws/notifier.rb +6 -13
- data/modules/mu/clouds/aws/role.rb +69 -40
- data/modules/mu/clouds/aws/search_domain.rb +17 -20
- data/modules/mu/clouds/aws/server.rb +184 -94
- data/modules/mu/clouds/aws/server_pool.rb +33 -38
- data/modules/mu/clouds/aws/storage_pool.rb +5 -12
- data/modules/mu/clouds/aws/user.rb +59 -33
- data/modules/mu/clouds/aws/userdata/linux.erb +18 -30
- data/modules/mu/clouds/aws/userdata/windows.erb +9 -9
- data/modules/mu/clouds/aws/vpc.rb +214 -145
- data/modules/mu/clouds/azure.rb +978 -44
- data/modules/mu/clouds/azure/container_cluster.rb +413 -0
- data/modules/mu/clouds/azure/firewall_rule.rb +500 -0
- data/modules/mu/clouds/azure/habitat.rb +167 -0
- data/modules/mu/clouds/azure/loadbalancer.rb +205 -0
- data/modules/mu/clouds/azure/role.rb +211 -0
- data/modules/mu/clouds/azure/server.rb +810 -0
- data/modules/mu/clouds/azure/user.rb +257 -0
- data/modules/mu/clouds/azure/userdata/README.md +4 -0
- data/modules/mu/clouds/azure/userdata/linux.erb +137 -0
- data/modules/mu/clouds/azure/userdata/windows.erb +275 -0
- data/modules/mu/clouds/azure/vpc.rb +782 -0
- data/modules/mu/clouds/cloudformation.rb +12 -9
- data/modules/mu/clouds/cloudformation/firewall_rule.rb +5 -13
- data/modules/mu/clouds/cloudformation/server.rb +10 -1
- data/modules/mu/clouds/cloudformation/server_pool.rb +1 -0
- data/modules/mu/clouds/cloudformation/vpc.rb +0 -2
- data/modules/mu/clouds/google.rb +554 -117
- data/modules/mu/clouds/google/bucket.rb +173 -32
- data/modules/mu/clouds/google/container_cluster.rb +1112 -157
- data/modules/mu/clouds/google/database.rb +24 -47
- data/modules/mu/clouds/google/firewall_rule.rb +344 -89
- data/modules/mu/clouds/google/folder.rb +156 -79
- data/modules/mu/clouds/google/group.rb +272 -82
- data/modules/mu/clouds/google/habitat.rb +177 -52
- data/modules/mu/clouds/google/loadbalancer.rb +9 -34
- data/modules/mu/clouds/google/role.rb +1211 -0
- data/modules/mu/clouds/google/server.rb +491 -227
- data/modules/mu/clouds/google/server_pool.rb +233 -48
- data/modules/mu/clouds/google/user.rb +479 -125
- data/modules/mu/clouds/google/userdata/linux.erb +3 -3
- data/modules/mu/clouds/google/userdata/windows.erb +9 -9
- data/modules/mu/clouds/google/vpc.rb +381 -223
- data/modules/mu/config.rb +689 -214
- data/modules/mu/config/bucket.rb +1 -1
- data/modules/mu/config/cache_cluster.rb +1 -1
- data/modules/mu/config/cache_cluster.yml +0 -4
- data/modules/mu/config/container_cluster.rb +18 -9
- data/modules/mu/config/database.rb +6 -23
- data/modules/mu/config/firewall_rule.rb +9 -15
- data/modules/mu/config/folder.rb +22 -21
- data/modules/mu/config/habitat.rb +22 -21
- data/modules/mu/config/loadbalancer.rb +2 -2
- data/modules/mu/config/role.rb +9 -40
- data/modules/mu/config/server.rb +26 -5
- data/modules/mu/config/server_pool.rb +1 -1
- data/modules/mu/config/storage_pool.rb +2 -2
- data/modules/mu/config/user.rb +4 -0
- data/modules/mu/config/vpc.rb +350 -110
- data/modules/mu/defaults/{amazon_images.yaml → AWS.yaml} +37 -39
- data/modules/mu/defaults/Azure.yaml +17 -0
- data/modules/mu/defaults/Google.yaml +24 -0
- data/modules/mu/defaults/README.md +1 -1
- data/modules/mu/deploy.rb +168 -125
- data/modules/mu/groomer.rb +2 -1
- data/modules/mu/groomers/ansible.rb +104 -32
- data/modules/mu/groomers/chef.rb +96 -44
- data/modules/mu/kittens.rb +20602 -0
- data/modules/mu/logger.rb +38 -11
- data/modules/mu/master.rb +90 -8
- data/modules/mu/master/chef.rb +2 -3
- data/modules/mu/master/ldap.rb +0 -1
- data/modules/mu/master/ssl.rb +250 -0
- data/modules/mu/mommacat.rb +917 -513
- data/modules/scratchpad.erb +1 -1
- data/modules/tests/super_complex_bok.yml +0 -0
- data/modules/tests/super_simple_bok.yml +0 -0
- data/roles/mu-master.json +2 -1
- data/spec/azure_creds +5 -0
- data/spec/mu.yaml +56 -0
- data/spec/mu/clouds/azure_spec.rb +164 -27
- data/spec/spec_helper.rb +5 -0
- data/test/clean_up.py +0 -0
- data/test/exec_inspec.py +0 -0
- data/test/exec_mu_install.py +0 -0
- data/test/exec_retry.py +0 -0
- data/test/smoke_test.rb +0 -0
- metadata +90 -118
- data/cookbooks/mu-jenkins/Berksfile +0 -14
- data/cookbooks/mu-jenkins/CHANGELOG.md +0 -13
- data/cookbooks/mu-jenkins/LICENSE +0 -37
- data/cookbooks/mu-jenkins/README.md +0 -105
- data/cookbooks/mu-jenkins/attributes/default.rb +0 -42
- data/cookbooks/mu-jenkins/files/default/cleanup_deploy_config.xml +0 -73
- data/cookbooks/mu-jenkins/files/default/deploy_config.xml +0 -44
- data/cookbooks/mu-jenkins/metadata.rb +0 -21
- data/cookbooks/mu-jenkins/recipes/default.rb +0 -195
- data/cookbooks/mu-jenkins/recipes/node-ssh-config.rb +0 -54
- data/cookbooks/mu-jenkins/recipes/public_key.rb +0 -24
- data/cookbooks/mu-jenkins/templates/default/example_job.config.xml.erb +0 -24
- data/cookbooks/mu-jenkins/templates/default/org.jvnet.hudson.plugins.SSHBuildWrapper.xml.erb +0 -14
- data/cookbooks/mu-jenkins/templates/default/ssh_config.erb +0 -6
- data/cookbooks/nagios/Berksfile +0 -11
- data/cookbooks/nagios/CHANGELOG.md +0 -589
- data/cookbooks/nagios/CONTRIBUTING.md +0 -11
- data/cookbooks/nagios/LICENSE +0 -37
- data/cookbooks/nagios/README.md +0 -328
- data/cookbooks/nagios/TESTING.md +0 -2
- data/cookbooks/nagios/attributes/config.rb +0 -171
- data/cookbooks/nagios/attributes/default.rb +0 -228
- data/cookbooks/nagios/chefignore +0 -102
- data/cookbooks/nagios/definitions/command.rb +0 -33
- data/cookbooks/nagios/definitions/contact.rb +0 -33
- data/cookbooks/nagios/definitions/contactgroup.rb +0 -33
- data/cookbooks/nagios/definitions/host.rb +0 -33
- data/cookbooks/nagios/definitions/hostdependency.rb +0 -33
- data/cookbooks/nagios/definitions/hostescalation.rb +0 -34
- data/cookbooks/nagios/definitions/hostgroup.rb +0 -33
- data/cookbooks/nagios/definitions/nagios_conf.rb +0 -38
- data/cookbooks/nagios/definitions/resource.rb +0 -33
- data/cookbooks/nagios/definitions/service.rb +0 -33
- data/cookbooks/nagios/definitions/servicedependency.rb +0 -33
- data/cookbooks/nagios/definitions/serviceescalation.rb +0 -34
- data/cookbooks/nagios/definitions/servicegroup.rb +0 -33
- data/cookbooks/nagios/definitions/timeperiod.rb +0 -33
- data/cookbooks/nagios/libraries/base.rb +0 -314
- data/cookbooks/nagios/libraries/command.rb +0 -91
- data/cookbooks/nagios/libraries/contact.rb +0 -230
- data/cookbooks/nagios/libraries/contactgroup.rb +0 -112
- data/cookbooks/nagios/libraries/custom_option.rb +0 -36
- data/cookbooks/nagios/libraries/data_bag_helper.rb +0 -23
- data/cookbooks/nagios/libraries/default.rb +0 -90
- data/cookbooks/nagios/libraries/host.rb +0 -412
- data/cookbooks/nagios/libraries/hostdependency.rb +0 -181
- data/cookbooks/nagios/libraries/hostescalation.rb +0 -173
- data/cookbooks/nagios/libraries/hostgroup.rb +0 -119
- data/cookbooks/nagios/libraries/nagios.rb +0 -282
- data/cookbooks/nagios/libraries/resource.rb +0 -59
- data/cookbooks/nagios/libraries/service.rb +0 -455
- data/cookbooks/nagios/libraries/servicedependency.rb +0 -215
- data/cookbooks/nagios/libraries/serviceescalation.rb +0 -195
- data/cookbooks/nagios/libraries/servicegroup.rb +0 -144
- data/cookbooks/nagios/libraries/timeperiod.rb +0 -160
- data/cookbooks/nagios/libraries/users_helper.rb +0 -54
- data/cookbooks/nagios/metadata.rb +0 -25
- data/cookbooks/nagios/recipes/_load_databag_config.rb +0 -153
- data/cookbooks/nagios/recipes/_load_default_config.rb +0 -241
- data/cookbooks/nagios/recipes/apache.rb +0 -48
- data/cookbooks/nagios/recipes/default.rb +0 -204
- data/cookbooks/nagios/recipes/nginx.rb +0 -82
- data/cookbooks/nagios/recipes/pagerduty.rb +0 -143
- data/cookbooks/nagios/recipes/server_package.rb +0 -40
- data/cookbooks/nagios/recipes/server_source.rb +0 -164
- data/cookbooks/nagios/templates/default/apache2.conf.erb +0 -96
- data/cookbooks/nagios/templates/default/cgi.cfg.erb +0 -266
- data/cookbooks/nagios/templates/default/commands.cfg.erb +0 -13
- data/cookbooks/nagios/templates/default/contacts.cfg.erb +0 -37
- data/cookbooks/nagios/templates/default/hostgroups.cfg.erb +0 -25
- data/cookbooks/nagios/templates/default/hosts.cfg.erb +0 -15
- data/cookbooks/nagios/templates/default/htpasswd.users.erb +0 -6
- data/cookbooks/nagios/templates/default/nagios.cfg.erb +0 -22
- data/cookbooks/nagios/templates/default/nginx.conf.erb +0 -62
- data/cookbooks/nagios/templates/default/pagerduty.cgi.erb +0 -185
- data/cookbooks/nagios/templates/default/resource.cfg.erb +0 -27
- data/cookbooks/nagios/templates/default/servicedependencies.cfg.erb +0 -15
- data/cookbooks/nagios/templates/default/servicegroups.cfg.erb +0 -14
- data/cookbooks/nagios/templates/default/services.cfg.erb +0 -14
- data/cookbooks/nagios/templates/default/templates.cfg.erb +0 -31
- data/cookbooks/nagios/templates/default/timeperiods.cfg.erb +0 -13
- data/extras/image-generators/aws/centos6.yaml +0 -18
- data/modules/mu/defaults/google_images.yaml +0 -16
- 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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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)
|
|
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:
|
|
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 #{
|
|
83
|
-
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
108
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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::
|
|
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.
|
|
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
|
-
|
|
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
|
-
# @
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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
|
-
|
|
207
|
-
resp.
|
|
208
|
-
|
|
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
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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::
|
|
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::
|
|
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
|