cloud-mu 2.0.0.pre.beta2 → 2.0.0.pre.beta3

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/Berksfile.lock +1 -1
  3. data/cloud-mu.gemspec +4 -3
  4. data/cookbooks/mu-master/templates/default/mu.rc.erb +2 -2
  5. data/cookbooks/mu-tools/files/default/Mu_CA.pem +18 -19
  6. data/cookbooks/mu-tools/recipes/rsyslog.rb +1 -1
  7. data/modules/mu/cleanup.rb +14 -1
  8. data/modules/mu/cloud.rb +40 -22
  9. data/modules/mu/clouds/aws/alarm.rb +6 -0
  10. data/modules/mu/clouds/aws/bucket.rb +29 -0
  11. data/modules/mu/clouds/aws/cache_cluster.rb +6 -0
  12. data/modules/mu/clouds/aws/container_cluster.rb +6 -0
  13. data/modules/mu/clouds/aws/database.rb +6 -0
  14. data/modules/mu/clouds/aws/dnszone.rb +6 -0
  15. data/modules/mu/clouds/aws/endpoint.rb +6 -0
  16. data/modules/mu/clouds/aws/firewall_rule.rb +6 -0
  17. data/modules/mu/clouds/aws/folder.rb +6 -0
  18. data/modules/mu/clouds/aws/function.rb +6 -0
  19. data/modules/mu/clouds/aws/group.rb +6 -0
  20. data/modules/mu/clouds/aws/loadbalancer.rb +6 -0
  21. data/modules/mu/clouds/aws/log.rb +6 -0
  22. data/modules/mu/clouds/aws/msg_queue.rb +6 -0
  23. data/modules/mu/clouds/aws/nosqldb.rb +6 -0
  24. data/modules/mu/clouds/aws/notifier.rb +6 -0
  25. data/modules/mu/clouds/aws/role.rb +97 -11
  26. data/modules/mu/clouds/aws/search_domain.rb +6 -0
  27. data/modules/mu/clouds/aws/server.rb +6 -0
  28. data/modules/mu/clouds/aws/server_pool.rb +6 -0
  29. data/modules/mu/clouds/aws/storage_pool.rb +6 -0
  30. data/modules/mu/clouds/aws/user.rb +6 -0
  31. data/modules/mu/clouds/aws/vpc.rb +25 -1
  32. data/modules/mu/clouds/google.rb +86 -16
  33. data/modules/mu/clouds/google/bucket.rb +78 -3
  34. data/modules/mu/clouds/google/container_cluster.rb +12 -0
  35. data/modules/mu/clouds/google/database.rb +15 -1
  36. data/modules/mu/clouds/google/firewall_rule.rb +18 -2
  37. data/modules/mu/clouds/google/folder.rb +183 -16
  38. data/modules/mu/clouds/google/group.rb +7 -1
  39. data/modules/mu/clouds/google/habitat.rb +139 -24
  40. data/modules/mu/clouds/google/loadbalancer.rb +26 -12
  41. data/modules/mu/clouds/google/server.rb +25 -10
  42. data/modules/mu/clouds/google/server_pool.rb +16 -3
  43. data/modules/mu/clouds/google/user.rb +7 -1
  44. data/modules/mu/clouds/google/vpc.rb +87 -76
  45. data/modules/mu/config.rb +12 -0
  46. data/modules/mu/config/bucket.rb +4 -0
  47. data/modules/mu/config/folder.rb +1 -0
  48. data/modules/mu/config/habitat.rb +1 -1
  49. data/modules/mu/config/role.rb +78 -34
  50. data/modules/mu/config/vpc.rb +1 -0
  51. data/modules/mu/groomers/chef.rb +1 -1
  52. data/modules/mu/kittens.rb +689 -283
  53. metadata +5 -4
@@ -19,6 +19,7 @@ module MU
19
19
  class Bucket < MU::Cloud::Bucket
20
20
  @deploy = nil
21
21
  @config = nil
22
+ @project_id = nil
22
23
 
23
24
  attr_reader :mu_name
24
25
  attr_reader :config
@@ -30,17 +31,28 @@ module MU
30
31
  @deploy = mommacat
31
32
  @config = MU::Config.manxify(kitten_cfg)
32
33
  @cloud_id ||= cloud_id
34
+ if mu_name
35
+ @mu_name = mu_name
36
+ @config['project'] ||= MU::Cloud::Google.defaultProject(@config['credentials'])
37
+ if !@project_id
38
+ project = MU::Cloud::Google.projectLookup(@config['project'], @deploy, sibling_only: true, raise_on_fail: false)
39
+ @project_id = project.nil? ? @config['project'] : project.cloudobj.cloud_id
40
+ end
41
+ end
33
42
  @mu_name ||= @deploy.getResourceName(@config["name"])
34
43
  end
35
44
 
36
45
  # Called automatically by {MU::Deploy#createResources}
37
46
  def create
38
- MU::Cloud::Google.storage(credentials: credentials).insert_bucket(@config['project'], bucket_descriptor)
47
+ @project_id = MU::Cloud::Google.projectLookup(@config['project'], @deploy).cloudobj.cloud_id
48
+ MU::Cloud::Google.storage(credentials: credentials).insert_bucket(@project_id, bucket_descriptor)
39
49
  @cloud_id = @mu_name.downcase
40
50
  end
41
51
 
42
52
  # Called automatically by {MU::Deploy#createResources}
43
53
  def groom
54
+ @project_id = MU::Cloud::Google.projectLookup(@config['project'], @deploy).cloudobj.cloud_id
55
+
44
56
  current = cloud_desc
45
57
  changed = false
46
58
 
@@ -63,6 +75,52 @@ module MU
63
75
  if changed
64
76
  MU::Cloud::Google.storage(credentials: credentials).patch_bucket(@cloud_id, bucket_descriptor)
65
77
  end
78
+
79
+ if @config['policies']
80
+ @config['policies'].each { |pol|
81
+ pol['grant_to'].each { |grantee|
82
+ entity = if grantee["type"]
83
+ sibling = deploy_obj.findLitterMate(
84
+ name: grantee["identifier"],
85
+ type: grantee["type"]
86
+ )
87
+ if sibling
88
+ sibling.cloudobj.cloud_id
89
+ else
90
+ raise MuError, "Couldn't find a #{grantee["type"]} named #{grantee["identifier"]} when generating Cloud Storage access policy"
91
+ end
92
+ else
93
+ pol['grant_to'].first['identifier']
94
+ end
95
+
96
+ if entity.match(/@/) and !entity.match(/^(group|user)\-/)
97
+ entity = "user-"+entity if entity.match(/@/)
98
+ end
99
+
100
+ bucket_acl_obj = MU::Cloud::Google.storage(:BucketAccessControl).new(
101
+ bucket: @cloud_id,
102
+ role: pol['permissions'].first,
103
+ entity: entity
104
+ )
105
+ MU.log "Adding Cloud Storage policy to bucket #{@cloud_id}", MU::NOTICE, details: bucket_acl_obj
106
+ MU::Cloud::Google.storage(credentials: credentials).insert_bucket_access_control(
107
+ @cloud_id,
108
+ bucket_acl_obj
109
+ )
110
+
111
+ acl_obj = MU::Cloud::Google.storage(:ObjectAccessControl).new(
112
+ bucket: @cloud_id,
113
+ role: pol['permissions'].first,
114
+ entity: entity
115
+ )
116
+ MU::Cloud::Google.storage(credentials: credentials).insert_default_object_access_control(
117
+ @cloud_id,
118
+ acl_obj
119
+ )
120
+ }
121
+ }
122
+
123
+ end
66
124
  end
67
125
 
68
126
  # Does this resource type exist as a global (cloud-wide) artifact, or
@@ -72,6 +130,12 @@ module MU
72
130
  true
73
131
  end
74
132
 
133
+ # Denote whether this resource implementation is experiment, ready for
134
+ # testing, or ready for production use.
135
+ def self.quality
136
+ MU::Cloud::BETA
137
+ end
138
+
75
139
  # Remove all buckets associated with the currently loaded deployment.
76
140
  # @param noop [Boolean]: If true, will only print what would be done
77
141
  # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
@@ -96,7 +160,9 @@ module MU
96
160
  # Return the metadata for this user cofiguration
97
161
  # @return [Hash]
98
162
  def notify
99
- MU.structToHash(cloud_desc)
163
+ desc = MU.structToHash(cloud_desc)
164
+ desc["project_id"] = @project_id
165
+ desc
100
166
  end
101
167
 
102
168
  # Locate an existing bucket.
@@ -104,7 +170,7 @@ module MU
104
170
  # @param region [String]: The cloud provider region.
105
171
  # @param flags [Hash]: Optional flags
106
172
  # @return [OpenStruct]: The cloud provider's complete descriptions of matching bucket.
107
- def self.find(cloud_id: nil, region: MU.curRegion, credentials: nil, flags: {})
173
+ def self.find(cloud_id: nil, region: MU.curRegion, credentials: nil, flags: {}, tag_key: nil, tag_value: nil)
108
174
  found = {}
109
175
  if cloud_id
110
176
  found[cloud_id] = MU::Cloud::Google.storage(credentials: credentials).get_bucket(cloud_id)
@@ -135,6 +201,15 @@ module MU
135
201
  def self.validateConfig(bucket, configurator)
136
202
  ok = true
137
203
 
204
+ if bucket['policies']
205
+ bucket['policies'].each { |pol|
206
+ if !pol['permissions'] or pol['permissions'].empty?
207
+ pol['permissions'] = ["READER"]
208
+ end
209
+ }
210
+ # XXX validate READER OWNER EDITOR w/e
211
+ end
212
+
138
213
  ok
139
214
  end
140
215
 
@@ -38,6 +38,11 @@ module MU
38
38
  @mu_name = mu_name
39
39
  deploydata = describe[2]
40
40
  @config['availability_zone'] = deploydata['zone']
41
+ @config['project'] ||= MU::Cloud::Google.defaultProject(@config['credentials'])
42
+ if !@project_id
43
+ project = MU::Cloud::Google.projectLookup(@config['project'], @deploy, sibling_only: true, raise_on_fail: false)
44
+ @project_id = project.nil? ? @config['project'] : project.cloudobj.cloud_id
45
+ end
41
46
  else
42
47
  @mu_name ||= @deploy.getResourceName(@config["name"], max_length: 40)
43
48
  end
@@ -185,6 +190,7 @@ puts @config['credentials']
185
190
  desc = MU.structToHash(MU::Cloud::Google.container(credentials: @config['credentials']).get_zone_cluster(@config["project"], @config['availability_zone'], @mu_name.downcase))
186
191
  desc["project"] = @config['project']
187
192
  desc["cloud_id"] = @cloud_id
193
+ desc["project_id"] = @project_id
188
194
  desc["mu_name"] = @mu_name.downcase
189
195
  desc
190
196
  end
@@ -196,6 +202,12 @@ puts @config['credentials']
196
202
  false
197
203
  end
198
204
 
205
+ # Denote whether this resource implementation is experiment, ready for
206
+ # testing, or ready for production use.
207
+ def self.quality
208
+ MU::Cloud::ALPHA
209
+ end
210
+
199
211
  # Called by {MU::Cleanup}. Locates resources that were created by the
200
212
  # currently-loaded deployment, and purges them.
201
213
  # @param noop [Boolean]: If true, will only print what would be done
@@ -18,6 +18,7 @@ module MU
18
18
  # A database as configured in {MU::Config::BasketofKittens::databases}
19
19
  class Database < MU::Cloud::Database
20
20
  @deploy = nil
21
+ @project_id = nil
21
22
  @config = nil
22
23
  attr_reader :mu_name
23
24
  attr_reader :cloud_id
@@ -36,6 +37,11 @@ module MU
36
37
 
37
38
  if !mu_name.nil?
38
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
39
45
  else
40
46
  @mu_name ||=
41
47
  if @config and @config['engine'] and @config["engine"].match(/^sqlserver/)
@@ -51,6 +57,7 @@ module MU
51
57
  # Called automatically by {MU::Deploy#createResources}
52
58
  # @return [String]: The cloud provider's identifier for this database instance.
53
59
  def create
60
+ @project_id = MU::Cloud::Google.projectLookup(@config['project'], @deploy).cloudobj.cloud_id
54
61
  labels = {}
55
62
  MU::MommaCat.listStandardTags.each_pair { |name, value|
56
63
  if !value.nil?
@@ -74,7 +81,7 @@ module MU
74
81
  instance_type: "CLOUD_SQL_INSTANCE" # TODO: READ_REPLICA_INSTANCE
75
82
  )
76
83
  pp instance_desc
77
- pp MU::Cloud::Google.sql(credentials: @config['credentials']).insert_instance(@config['project'], instance_desc)
84
+ pp MU::Cloud::Google.sql(credentials: @config['credentials']).insert_instance(@project_id, instance_desc)
78
85
  end
79
86
 
80
87
  # Locate an existing Database or Databases and return an array containing matching GCP resource descriptors for those that match.
@@ -90,6 +97,7 @@ module MU
90
97
 
91
98
  # Called automatically by {MU::Deploy#createResources}
92
99
  def groom
100
+ @project_id = MU::Cloud::Google.projectLookup(@config['project'], @deploy).cloudobj.cloud_id
93
101
  end
94
102
 
95
103
  # Register a description of this database instance with this deployment's metadata.
@@ -111,6 +119,12 @@ module MU
111
119
  false
112
120
  end
113
121
 
122
+ # Denote whether this resource implementation is experiment, ready for
123
+ # testing, or ready for production use.
124
+ def self.quality
125
+ MU::Cloud::ALPHA
126
+ end
127
+
114
128
  # Called by {MU::Cleanup}. Locates resources that were created by the
115
129
  # currently-loaded deployment, and purges them.
116
130
  # @param noop [Boolean]: If true, will only print what would be done
@@ -21,6 +21,7 @@ module MU
21
21
 
22
22
  @deploy = nil
23
23
  @config = nil
24
+ @project_id = nil
24
25
  @admin_sgs = Hash.new
25
26
  @admin_sg_semaphore = Mutex.new
26
27
 
@@ -38,6 +39,11 @@ module MU
38
39
  @mu_name = mu_name
39
40
  # This is really a placeholder, since we "own" multiple rule sets
40
41
  @cloud_id ||= MU::Cloud::Google.nameStr(@mu_name+"-ingress-allow")
42
+ @config['project'] ||= MU::Cloud::Google.defaultProject(@config['credentials'])
43
+ if !@project_id
44
+ project = MU::Cloud::Google.projectLookup(@config['project'], @deploy, sibling_only: true, raise_on_fail: false)
45
+ @project_id = project.nil? ? @config['project'] : project.cloudobj.cloud_id
46
+ end
41
47
  else
42
48
  if !@vpc.nil?
43
49
  @mu_name = @deploy.getResourceName(@config['name'], need_unique_string: true, max_length: 61)
@@ -52,6 +58,8 @@ module MU
52
58
 
53
59
  # Called by {MU::Deploy#createResources}
54
60
  def create
61
+ @project_id = MU::Cloud::Google.projectLookup(@config['project'], @deploy).cloudobj.cloud_id
62
+
55
63
  vpc_id = @vpc.cloudobj.url if !@vpc.nil? and !@vpc.cloudobj.nil?
56
64
  vpc_id ||= @config['vpc']['vpc_id'] if @config['vpc'] and @config['vpc']['vpc_id']
57
65
 
@@ -109,8 +117,8 @@ module MU
109
117
  allrules.each_value { |fwdesc|
110
118
  threads << Thread.new {
111
119
  fwobj = MU::Cloud::Google.compute(:Firewall).new(fwdesc)
112
- MU.log "Creating firewall #{fwdesc[:name]} in project #{@config['project']}", details: fwobj
113
- resp = MU::Cloud::Google.compute(credentials: @config['credentials']).insert_firewall(@config['project'], fwobj)
120
+ MU.log "Creating firewall #{fwdesc[:name]} in project #{@project_id}", details: fwobj
121
+ resp = MU::Cloud::Google.compute(credentials: @config['credentials']).insert_firewall(@project_id, fwobj)
114
122
  # XXX Check for empty (no hosts) sets
115
123
  # MU.log "Can't create empty firewalls in Google Cloud, skipping #{@mu_name}", MU::WARN
116
124
  }
@@ -123,6 +131,7 @@ module MU
123
131
 
124
132
  # Called by {MU::Deploy#createResources}
125
133
  def groom
134
+ @project_id = MU::Cloud::Google.projectLookup(@config['project'], @deploy).cloudobj.cloud_id
126
135
  end
127
136
 
128
137
  # Log metadata about this ruleset to the currently running deployment
@@ -132,6 +141,7 @@ module MU
132
141
  )
133
142
  sg_data ||= {}
134
143
  sg_data["group_id"] = @cloud_id
144
+ sg_data["project_id"] = @project_id
135
145
  sg_data["cloud_id"] = @cloud_id
136
146
 
137
147
  return sg_data
@@ -176,6 +186,12 @@ module MU
176
186
  true
177
187
  end
178
188
 
189
+ # Denote whether this resource implementation is experiment, ready for
190
+ # testing, or ready for production use.
191
+ def self.quality
192
+ MU::Cloud::RELEASE
193
+ end
194
+
179
195
  # Remove all security groups (firewall rulesets) associated with the currently loaded deployment.
180
196
  # @param noop [Boolean]: If true, will only print what would be done
181
197
  # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
@@ -19,6 +19,7 @@ module MU
19
19
  class Folder < MU::Cloud::Folder
20
20
  @deploy = nil
21
21
  @config = nil
22
+ @parent = nil
22
23
 
23
24
  attr_reader :mu_name
24
25
  attr_reader :config
@@ -30,23 +31,95 @@ module MU
30
31
  @deploy = mommacat
31
32
  @config = MU::Config.manxify(kitten_cfg)
32
33
  @cloud_id ||= cloud_id
33
- @mu_name ||= @deploy.getResourceName(@config["name"])
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
34
42
  end
35
43
 
36
44
  # Called automatically by {MU::Deploy#createResources}
37
45
  def create
38
46
 
39
- name_string = @deploy.getResourceName(@config["name"], max_length: 30).downcase
47
+ name_string = if @config['scrub_mu_isms']
48
+ @config["name"]
49
+ else
50
+ @deploy.getResourceName(@config["name"], max_length: 30).downcase
51
+ end
40
52
 
41
- folder_obj = MU::Cloud::Google.folder(:Folder).new(
42
- name: name_string,
53
+ params = {
43
54
  display_name: name_string
44
- )
45
- pp folder_obj
46
- MU.log "Creating folder #{@mu_name}", details: folder_obj
47
- resp = MU::Cloud::Google.folder(credentials: @config['credentials']).create_folder(folder_obj)
55
+ }
56
+
57
+ parent = MU::Cloud::Google::Folder.resolveParent(@config['parent'], credentials: @config['credentials'])
58
+
59
+ folder_obj = MU::Cloud::Google.folder(:Folder).new(params)
60
+
61
+ MU.log "Creating folder #{name_string} under #{parent}", details: folder_obj
62
+ resp = MU::Cloud::Google.folder(credentials: @config['credentials']).create_folder(folder_obj, parent: parent)
63
+
64
+ # Wait for list_folders output to be consistent (for the folder we
65
+ # just created to show up)
66
+ retries = 0
67
+ begin
68
+ found = MU::Cloud::Google::Folder.find(credentials: credentials, flags: { 'display_name' => name_string, 'parent_id' => parent })
69
+ if found.size > 0
70
+ @cloud_id = found.keys.first
71
+ @parent = found.values.first.parent
72
+ MU.log "Folder #{name_string} has identifier #{@cloud_id}"
73
+ else
74
+ if retries > 0 and (retries % 3) == 0
75
+ MU.log "Waiting for Google Cloud folder #{name_string} to appear in list_folder results...", MU::NOTICE
76
+ end
77
+ retries += 1
78
+ sleep 15
79
+ end
80
+ end while found.size == 0
48
81
 
49
- @cloud_id = name_string.downcase
82
+ end
83
+
84
+ # Given a {MU::Config::Folder.reference} configuration block, resolve
85
+ # to a GCP resource id and type suitable for use in API calls to manage
86
+ # projects and folders.
87
+ # @param parentblock [Hash]
88
+ # @return [String]
89
+ def self.resolveParent(parentblock, credentials: nil)
90
+ my_org = MU::Cloud::Google.getOrg(credentials)
91
+ if !parentblock or parentblock['id'] == my_org.name or
92
+ parentblock['name'] == my_org.display_name or (parentblock['id'] and
93
+ "organizations/"+parentblock['id'] == my_org.name)
94
+ return my_org.name
95
+ end
96
+
97
+ if parentblock['name']
98
+ sib_folder = MU::MommaCat.findStray(
99
+ "Google",
100
+ "folders",
101
+ deploy_id: parentblock['deploy_id'],
102
+ credentials: credentials,
103
+ name: parentblock['name']
104
+ ).first
105
+ if sib_folder
106
+ return "folders/"+sib_folder.cloudobj.cloud_id
107
+ end
108
+ end
109
+
110
+ begin
111
+ found = MU::Cloud::Google::Folder.find(cloud_id: parentblock['id'], credentials: credentials, flags: { 'display_name' => parentblock['name'] })
112
+ rescue ::Google::Apis::ClientError => e
113
+ if !e.message.match(/Invalid request status_code: 404/)
114
+ raise e
115
+ end
116
+ end
117
+
118
+ if found and found.size > 0
119
+ return found.values.first.name
120
+ end
121
+
122
+ nil
50
123
  end
51
124
 
52
125
  # Return the cloud descriptor for the Folder
@@ -57,8 +130,9 @@ pp folder_obj
57
130
  # Return the metadata for this project's configuration
58
131
  # @return [Hash]
59
132
  def notify
60
- desc = MU.structToHash(MU::Cloud::Google.folder(credentials: credentials).get_folder(@cloud_id))
133
+ desc = MU.structToHash(MU::Cloud::Google.folder(credentials: @config['credentials']).get_folder("folders/"+@cloud_id))
61
134
  desc["mu_name"] = @mu_name
135
+ desc["parent"] = @parent
62
136
  desc["cloud_id"] = @cloud_id
63
137
  desc
64
138
  end
@@ -70,24 +144,105 @@ pp folder_obj
70
144
  true
71
145
  end
72
146
 
147
+ # Denote whether this resource implementation is experiment, ready for
148
+ # testing, or ready for production use.
149
+ def self.quality
150
+ MU::Cloud::BETA
151
+ end
152
+
73
153
  # Remove all Google projects associated with the currently loaded deployment. Try to, anyway.
74
154
  # @param noop [Boolean]: If true, will only print what would be done
75
155
  # @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server
76
- # @param region [String]: The cloud provider region
77
156
  # @return [void]
78
- def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, credentials: nil, flags: {})
157
+ def self.cleanup(noop: false, ignoremaster: false, credentials: nil, flags: {}, region: MU.myRegion)
158
+ # We can't label GCP folders, and their names are too short to encode
159
+ # Mu deploy IDs, so all we can do is rely on flags['known'] passed in
160
+ # from cleanup, which relies on our metadata to know what's ours.
161
+
162
+ if flags and flags['known']
163
+ 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
178
+ sleep 30
179
+ retries += 1
180
+ puts retries
181
+ else
182
+ MU.log "Folder #{cloud_id} still exists after #{max_retries.to_s} attempts to delete", MU::ERR
183
+ break
184
+ 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
198
+ end
199
+ end
200
+ }
201
+ end
79
202
  end
80
203
 
81
204
  # Locate an existing project
82
205
  # @param cloud_id [String]: The cloud provider's identifier for this resource.
83
- # @param region [String]: The cloud provider region.
84
206
  # @param flags [Hash]: Optional flags
85
207
  # @return [OpenStruct]: The cloud provider's complete descriptions of matching project
86
- def self.find(cloud_id: nil, region: MU.curRegion, credentials: nil, flags: {})
208
+ def self.find(cloud_id: nil, credentials: nil, flags: {}, tag_key: nil, tag_value: nil)
87
209
  found = {}
210
+
211
+ # Recursively search a GCP folder hierarchy for a folder matching our
212
+ # supplied name or identifier.
213
+ def self.find_matching_folder(parent, name: nil, id: nil, credentials: nil)
214
+ resp = MU::Cloud::Google.folder(credentials: credentials).list_folders(parent: parent)
215
+ if resp and resp.folders
216
+ resp.folders.each { |f|
217
+ if name and name.downcase == f.display_name.downcase
218
+ return f
219
+ elsif id and "folders/"+id== f.name
220
+ return f
221
+ else
222
+ found = self.find_matching_folder(f.name, name: name, id: id, credentials: credentials)
223
+ return found if found
224
+ end
225
+ }
226
+ end
227
+ nil
228
+ end
229
+
88
230
  if cloud_id
89
- found[cloud_id] = MU::Cloud::Google.folder(credentials: credentials).get_folder(cloud_id)
90
- else
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
239
+
240
+ if parent
241
+ resp = self.find_matching_folder(parent, name: flags['display_name'], credentials: credentials)
242
+ if resp
243
+ found[resp.name.sub(/^folders\//, "")] = resp
244
+ end
245
+ end
91
246
  end
92
247
 
93
248
  found
@@ -110,6 +265,18 @@ pp folder_obj
110
265
  def self.validateConfig(folder, configurator)
111
266
  ok = true
112
267
 
268
+ 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
270
+ ok = false
271
+ end
272
+
273
+ if folder['parent'] and folder['parent']['name'] and !folder['parent']['deploy_id'] and configurator.haveLitterMate?(folder['parent']['name'], "folders")
274
+ folder["dependencies"] ||= []
275
+ folder["dependencies"] << {
276
+ "type" => "folder",
277
+ "name" => folder['parent']['name']
278
+ }
279
+ end
113
280
 
114
281
  ok
115
282
  end