terraorg 0.1.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +11 -1
- data/bin/terraorg +7 -5
- data/lib/terraorg/model/org.rb +76 -21
- data/lib/terraorg/model/platoon.rb +2 -2
- data/lib/terraorg/model/squad.rb +47 -16
- data/lib/terraorg/version.rb +1 -1
- metadata +31 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 46107b71a1eace06c51463513c5b495b9549ec19ebc911103ec0d7f236fec6f8
|
4
|
+
data.tar.gz: b05708c67d359a3040eca8724140603d5c4debd6e2f1a25c87034e8a9b60e7be
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9f9286df25676340a5e3a36221f5b7de4ed21cce63281850210f18b360474544e453939b8b4559dec6cb58dfa0b9ce5facca21570f03295cb5f9d0b5c56eff57
|
7
|
+
data.tar.gz: 196d63d8df921c54216511ee7604b6ec8f8813241609a1fb0c4fa5480d965c4d2e9f0b2042a0f55a46b1fbaa8cb633f50317608afbf68f71d42b8b00fc877f83
|
data/README.md
CHANGED
@@ -34,7 +34,9 @@ Based on the org that this tool was originally designed for, orgs are expected
|
|
34
34
|
to have three levels:
|
35
35
|
|
36
36
|
* *squads*: the base unit of team-dom, containing people, who may be in
|
37
|
-
different geographical regions.
|
37
|
+
different geographical regions. Teams contain _members_ (full time heads)
|
38
|
+
and _associates_ (typically part time floaters.) Any associate of a squad
|
39
|
+
must also have a home squad for which they are a full time member.
|
38
40
|
* *platoons*: a unit which contains squads and exceptional people who are
|
39
41
|
members of the platoon, but not part of any squad
|
40
42
|
* *org*: The whole organization, including its manager, any exceptional squads
|
@@ -45,6 +47,10 @@ The tool generates groups for each granular unit of organization in Okta and G
|
|
45
47
|
Suite in Terraform. With patching, it could be possible for more organizational
|
46
48
|
systems to be supported.
|
47
49
|
|
50
|
+
## Diagram
|
51
|
+
|
52
|
+
![Diagram of org structure](img/diagram.png)
|
53
|
+
|
48
54
|
## How it works
|
49
55
|
|
50
56
|
Firstly, take your entire existing organization and define it using the
|
@@ -120,6 +126,10 @@ information on how to configure the providers.
|
|
120
126
|
[articulate/terraform-provider-okta]: https://github.com/articulate/terraform-provider-okta
|
121
127
|
[DeviaVir/terraform-provider-gsuite]: https://github.com/DeviaVir/terraform-provider-gsuite
|
122
128
|
|
129
|
+
## Running tests
|
130
|
+
There are a limited number of tests that can be invoked with
|
131
|
+
`ruby -I lib test/terraorg/model/org_test.rb `
|
132
|
+
|
123
133
|
## Suggested process
|
124
134
|
|
125
135
|
At [LiveRamp], a pull request based workflow leveraging [Atlantis] is used to
|
data/bin/terraorg
CHANGED
@@ -31,14 +31,15 @@ ACTIONS = [
|
|
31
31
|
'validate'
|
32
32
|
].freeze
|
33
33
|
|
34
|
+
STRICT_VALIDATION = ENV.fetch('TERRAORG_STRICT_VALIDATION', 'true')
|
34
35
|
SQUADS_FILE = ENV.fetch('TERRAORG_SQUADS', 'squads.json')
|
35
36
|
PLATOONS_FILE = ENV.fetch('TERRAORG_PLATOONS', 'platoons.json')
|
36
37
|
ORG_FILE = ENV.fetch('TERRAORG_ROOT', 'org.json')
|
37
38
|
CACHE_FILE = ENV.fetch('TERRAORG_OKTA_CACHE', 'okta_cache.json')
|
38
|
-
GSUITE_DOMAIN = ENV.fetch('GSUITE_DOMAIN')
|
39
|
-
SLACK_DOMAIN = ENV.fetch('SLACK_DOMAIN', '
|
40
|
-
OKTA_ORG_NAME = ENV.fetch('OKTA_ORG_NAME')
|
41
|
-
OKTA_API_TOKEN = ENV.fetch('OKTA_API_TOKEN')
|
39
|
+
GSUITE_DOMAIN = ENV.fetch('GSUITE_DOMAIN', 'fillmein_gsuite.com')
|
40
|
+
SLACK_DOMAIN = ENV.fetch('SLACK_DOMAIN', 'fillmein.slack.com')
|
41
|
+
OKTA_ORG_NAME = ENV.fetch('OKTA_ORG_NAME', 'fillmein_okta')
|
42
|
+
OKTA_API_TOKEN = ENV.fetch('OKTA_API_TOKEN', 'fillmein_okta_api_token')
|
42
43
|
OKTA_BASE_URL = ENV.fetch('OKTA_BASE_URL', 'okta.com')
|
43
44
|
|
44
45
|
action = ARGV[0]
|
@@ -95,7 +96,8 @@ platoons = Platoons.new(JSON.parse(platoons_data), squads, people, GSUITE_DOMAIN
|
|
95
96
|
org_data = File.read(ORG_FILE)
|
96
97
|
org = Org.new(JSON.parse(org_data), platoons, squads, people, GSUITE_DOMAIN)
|
97
98
|
|
98
|
-
|
99
|
+
strict = (STRICT_VALIDATION == 'true')
|
100
|
+
org.validate!(strict: strict)
|
99
101
|
|
100
102
|
case action
|
101
103
|
when 'generate-squads-md'
|
data/lib/terraorg/model/org.rb
CHANGED
@@ -15,7 +15,8 @@
|
|
15
15
|
require 'terraorg/model/util'
|
16
16
|
|
17
17
|
class Org
|
18
|
-
|
18
|
+
MAX_MEMBER_SQUADS_PER_PERSON = 1
|
19
|
+
MAX_ASSOCIATE_SQUADS_PER_PERSON = 3
|
19
20
|
SCHEMA_VERSION = 'v1'.freeze
|
20
21
|
|
21
22
|
def initialize(parsed_data, platoons, squads, people, gsuite_domain)
|
@@ -48,23 +49,33 @@ class Org
|
|
48
49
|
@squads = squads
|
49
50
|
end
|
50
51
|
|
51
|
-
def validate!
|
52
|
+
def validate!(strict: true)
|
53
|
+
failure = false
|
54
|
+
|
52
55
|
# Do not allow the JSON files to contain any people who have left.
|
53
|
-
|
56
|
+
unless @people.inactive.empty?
|
57
|
+
$stderr.puts "ERROR: Users have left the company, or are Suspended in Okta: #{@people.inactive.map(&:id).join(', ')}"
|
58
|
+
failure = true
|
59
|
+
end
|
54
60
|
|
55
61
|
# Do not allow the org to be totally empty.
|
56
|
-
|
62
|
+
if @member_platoons.size + @member_exception_squads.size == 0
|
63
|
+
$stderr.puts 'ERROR: Org has no platoons or exception squads'
|
64
|
+
failure = true
|
65
|
+
end
|
57
66
|
|
58
67
|
# Require all platoons to be part of the org.
|
59
68
|
platoon_diff = Set.new(@platoons.all_names) - Set.new(@member_platoon_names)
|
60
69
|
unless platoon_diff.empty?
|
61
|
-
|
70
|
+
$stderr.puts "ERROR: Platoons are not used in the org: #{platoon_diff.to_a.sort}"
|
71
|
+
failure = true
|
62
72
|
end
|
63
73
|
|
64
74
|
# Require all squads to be used in the org.
|
65
75
|
squad_diff = Set.new(@squads.all_names) - Set.new(@platoons.all_squad_names) - Set.new(@member_exception_squad_names)
|
66
76
|
unless squad_diff.empty?
|
67
|
-
|
77
|
+
$stderr.puts "ERROR: Squad(s) are not used in the org: #{squad_diff.to_a.sort}"
|
78
|
+
failure = true
|
68
79
|
end
|
69
80
|
|
70
81
|
all_squads = (@member_platoons.map(&:member_squads) + @member_exception_squads).flatten
|
@@ -78,20 +89,37 @@ class Org
|
|
78
89
|
count > 1
|
79
90
|
end
|
80
91
|
if !more_than_one_platoon.empty?
|
81
|
-
|
92
|
+
$stderr.puts "ERROR: Squads are part of more than one platoon: #{more_than_one_platoon}"
|
93
|
+
failure = true
|
82
94
|
end
|
83
95
|
|
84
|
-
# Validate that a squad member belongs to
|
96
|
+
# Validate that a squad member belongs to some maximum number of squads
|
97
|
+
# across the entire org. A person can be an associate of other squads
|
98
|
+
# at a different count. See top of file for defined limits.
|
85
99
|
squad_count = {}
|
86
|
-
all_squads.map(&:teams).flatten.map(&:values).flatten.map(&:members).flatten
|
100
|
+
all_members = all_squads.map(&:teams).flatten.map(&:values).flatten.map(&:members).flatten
|
101
|
+
all_members.each do |member|
|
87
102
|
squad_count[member.id] = squad_count.fetch(member.id, 0) + 1
|
88
103
|
end
|
89
104
|
more_than_max_squads = squad_count.select do |member, count|
|
90
|
-
count >
|
105
|
+
count > MAX_MEMBER_SQUADS_PER_PERSON
|
91
106
|
end
|
92
107
|
if !more_than_max_squads.empty?
|
93
|
-
|
94
|
-
|
108
|
+
$stderr.puts "ERROR: People are members of more than #{MAX_MEMBER_SQUADS_PER_PERSON} squads: #{more_than_max_squads}"
|
109
|
+
failure = true
|
110
|
+
end
|
111
|
+
|
112
|
+
associate_count = {}
|
113
|
+
all_associates = all_squads.map(&:teams).flatten.map(&:values).flatten.map(&:associates).flatten
|
114
|
+
all_associates.each do |assoc|
|
115
|
+
associate_count[assoc.id] = associate_count.fetch(assoc.id, 0) + 1
|
116
|
+
end
|
117
|
+
more_than_max_squads = associate_count.select do |_, count|
|
118
|
+
count > MAX_ASSOCIATE_SQUADS_PER_PERSON
|
119
|
+
end
|
120
|
+
if !more_than_max_squads.empty?
|
121
|
+
$stderr.puts "ERROR: People are associates of more than #{MAX_ASSOCIATE_SQUADS_PER_PERSON} squads: #{more_than_max_squads}"
|
122
|
+
failure = true
|
95
123
|
end
|
96
124
|
|
97
125
|
# Validate that a squad member is not also an org exception
|
@@ -100,8 +128,18 @@ class Org
|
|
100
128
|
exceptions.member? member
|
101
129
|
end
|
102
130
|
if !exception_and_squad_member.empty?
|
103
|
-
|
131
|
+
$stderr.puts "ERROR: Exception members are also squad members: #{exception_and_squad_member}"
|
132
|
+
failure = true
|
133
|
+
end
|
134
|
+
|
135
|
+
# Validate that any associate is a member of some squad
|
136
|
+
associates_but_not_members = Set.new(all_associates.map(&:id)) - Set.new(all_members.map(&:id)) - exceptions
|
137
|
+
if !associates_but_not_members.empty?
|
138
|
+
$stderr.puts "ERROR: #{associates_but_not_members.map(&:id)} are associates of squads but not members of any squad"
|
139
|
+
failure = true
|
104
140
|
end
|
141
|
+
|
142
|
+
raise "CRITICAL: Validation failed due to at least one error above" if failure && strict
|
105
143
|
end
|
106
144
|
|
107
145
|
def members
|
@@ -153,8 +191,8 @@ class Org
|
|
153
191
|
md_lines = [
|
154
192
|
'# Engineering Squads List',
|
155
193
|
'',
|
156
|
-
'|Platoon|Squad|PM|Mailing list|TS SME|Slack|# Engineers|Squad Manager|
|
157
|
-
'
|
194
|
+
'|Platoon|Squad|PM|Mailing list|TS SME|Slack|# Engineers|Squad Manager|Members|',
|
195
|
+
'|---|---|---|---|---|---|---|---|---|',
|
158
196
|
]
|
159
197
|
md_lines += @member_platoons.map { |s| s.get_squads_psv_rows(@id) }
|
160
198
|
md_lines += @member_exception_squads.map { |s| s.to_md('_No Platoon_', @id) }
|
@@ -164,13 +202,16 @@ class Org
|
|
164
202
|
md_lines.join("\n")
|
165
203
|
end
|
166
204
|
|
167
|
-
def
|
168
|
-
|
169
|
-
|
205
|
+
def generate_tf_platoons
|
206
|
+
@member_platoons.map { |p| p.generate_tf(@id) }.join("\n")
|
207
|
+
end
|
170
208
|
|
171
|
-
|
172
|
-
|
209
|
+
def generate_tf_squads
|
210
|
+
@member_exception_squads.map { |s| s.generate_tf(@id) }.join("\n")
|
211
|
+
end
|
173
212
|
|
213
|
+
def generate_tf_org
|
214
|
+
tf = ''
|
174
215
|
# Roll all platoons and exception squads into the org.
|
175
216
|
roll_up_to_org = \
|
176
217
|
@member_exception_squads.map { |s| s.unique_name(@id, nil) } + \
|
@@ -210,14 +251,18 @@ EOF
|
|
210
251
|
all_locations[@manager_location] = all_locations.fetch(@manager_location, Set.new).add(@manager)
|
211
252
|
|
212
253
|
all_locations.each do |l, m|
|
254
|
+
description = "#{@name} organization members based in #{l} (terraorg)"
|
213
255
|
name = "#{unique_name}-#{l.downcase}"
|
214
256
|
tf += <<-EOF
|
215
257
|
resource "okta_group" "#{name}" {
|
216
258
|
name = "#{name}"
|
217
|
-
description = "#{
|
259
|
+
description = "#{description}"
|
218
260
|
users = #{Util.persons_tf(m)}
|
219
261
|
}
|
262
|
+
|
263
|
+
#{Util.gsuite_group_tf(name, @gsuite_domain, m, description)}
|
220
264
|
EOF
|
265
|
+
|
221
266
|
end
|
222
267
|
|
223
268
|
# Generate a special GSuite group for all managers (org, platoon, squad
|
@@ -226,7 +271,17 @@ EOF
|
|
226
271
|
all_managers = Set.new([@manager] + @platoons.all.map(&:manager) + @squads.all.map(&:manager).select { |m| m })
|
227
272
|
manager_dl = "#{@id}-managers"
|
228
273
|
tf += Util.gsuite_group_tf(manager_dl, @gsuite_domain, all_managers, "All managers of the #{@name} organization (terraorg)")
|
274
|
+
tf
|
275
|
+
end
|
276
|
+
|
277
|
+
def generate_tf
|
278
|
+
tf = generate_tf_platoons
|
279
|
+
File.write('auto.platoons.tf', tf)
|
280
|
+
|
281
|
+
tf = generate_tf_squads
|
282
|
+
File.write('auto.exception_squads.tf', tf)
|
229
283
|
|
284
|
+
tf = generate_tf_org
|
230
285
|
File.write('auto.org.tf', tf)
|
231
286
|
end
|
232
287
|
|
@@ -99,9 +99,9 @@ EOF
|
|
99
99
|
# - Sort the squad ids lexically
|
100
100
|
# - Sort the exceptions lexically
|
101
101
|
def to_h
|
102
|
-
obj = { 'id' => @id, 'name' => @name, 'manager' => @manager.id, 'squads' => @member_squads.map(&:id) }
|
102
|
+
obj = { 'id' => @id, 'name' => @name, 'manager' => @manager.id, 'squads' => @member_squads.map(&:id).sort }
|
103
103
|
unless @member_exceptions.empty?
|
104
|
-
obj['exceptions'] = @member_exceptions.map(&:id)
|
104
|
+
obj['exceptions'] = @member_exceptions.map(&:id).sort
|
105
105
|
end
|
106
106
|
unless @metadata.empty?
|
107
107
|
obj['metadata'] = @metadata
|
data/lib/terraorg/model/squad.rb
CHANGED
@@ -12,6 +12,8 @@
|
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
|
+
require 'countries'
|
16
|
+
|
15
17
|
require 'terraorg/model/people'
|
16
18
|
require 'terraorg/model/util'
|
17
19
|
|
@@ -19,23 +21,49 @@ class Squad
|
|
19
21
|
attr_accessor :id, :name, :metadata, :teams
|
20
22
|
|
21
23
|
class Team
|
22
|
-
attr_accessor :location, :members
|
24
|
+
attr_accessor :location, :members, :associates
|
23
25
|
|
24
26
|
def initialize(parsed_data, people)
|
25
|
-
|
27
|
+
location = parsed_data.fetch('location')
|
28
|
+
country = ISO3166::Country.new(location)
|
29
|
+
raise "Location is invalid: #{location}" unless country
|
30
|
+
@location = country.alpha2
|
26
31
|
@members = parsed_data.fetch('members', []).map do |n|
|
27
32
|
people.get_or_create!(n)
|
28
33
|
end
|
34
|
+
@associates = parsed_data.fetch('associates', []).map do |n|
|
35
|
+
people.get_or_create!(n)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def validate!
|
40
|
+
raise 'Subteam has no full time members' if @members.size == 0
|
41
|
+
# location validation done at initialize time
|
42
|
+
# associates can be empty
|
43
|
+
|
44
|
+
# associates and members must have zero intersection
|
45
|
+
associate_set = Set.new(@associates.map(&:id))
|
46
|
+
member_set = Set.new(@members.map(&:id))
|
47
|
+
raise 'A member cannot also be an associate of the same team' if associate_set.intersection(member_set)
|
29
48
|
end
|
30
49
|
|
31
50
|
# Output a canonical (sorted, formatted) version of this Team.
|
32
51
|
# - Sort the members in each team
|
52
|
+
# - Only add an associates field if it's present
|
33
53
|
def to_h
|
34
|
-
{
|
54
|
+
{
|
55
|
+
'associates' => @associates.map(&:id).sort,
|
56
|
+
'location' => @location,
|
57
|
+
'members' => @members.map(&:id).sort,
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
def everyone
|
62
|
+
@associates + @members
|
35
63
|
end
|
36
64
|
|
37
65
|
def to_md
|
38
|
-
"**#{@location}**: #{@members.map(&:name).sort.join(', ')}"
|
66
|
+
"**#{@location}**: #{@members.map(&:name).sort.join(', ')}, #{@associates.map { |m| "_#{m.name}_" }.sort.join(', ')}"
|
39
67
|
end
|
40
68
|
end
|
41
69
|
|
@@ -53,10 +81,18 @@ class Squad
|
|
53
81
|
@teams = Hash[teams_arr.map { |t| [t.location, t] }]
|
54
82
|
end
|
55
83
|
|
56
|
-
|
84
|
+
# Everyone including associates on all subteams in the squad.
|
85
|
+
def everyone(location: nil)
|
57
86
|
@teams.select { |l, t|
|
58
87
|
location == nil || l == location
|
59
|
-
}.map { |
|
88
|
+
}.map { |_, t|
|
89
|
+
t.everyone
|
90
|
+
}.flatten
|
91
|
+
end
|
92
|
+
|
93
|
+
# Full-time members of all subteams in this squad
|
94
|
+
def members
|
95
|
+
@teams.map { |_, t|
|
60
96
|
t.members
|
61
97
|
}.flatten
|
62
98
|
end
|
@@ -64,11 +100,11 @@ class Squad
|
|
64
100
|
def get_acl_groups(org_id)
|
65
101
|
# each geographically located subteam
|
66
102
|
groups = Hash[@teams.map { |location, team|
|
67
|
-
[unique_name(org_id, location), {'name' => "#{@name} squad members based in #{location}", 'members' => team.
|
103
|
+
[unique_name(org_id, location), {'name' => "#{@name} squad members based in #{location}", 'members' => team.everyone}]
|
68
104
|
}]
|
69
105
|
|
70
106
|
# combination of all subteams
|
71
|
-
groups[unique_name(org_id, nil)] = {'name' => "#{@name} squad worldwide members", 'members' =>
|
107
|
+
groups[unique_name(org_id, nil)] = {'name' => "#{@name} squad worldwide members", 'members' => everyone}
|
72
108
|
|
73
109
|
groups
|
74
110
|
end
|
@@ -82,7 +118,7 @@ class Squad
|
|
82
118
|
end
|
83
119
|
|
84
120
|
def validate!
|
85
|
-
|
121
|
+
@teams.each(&:validate!)
|
86
122
|
end
|
87
123
|
|
88
124
|
def to_md(platoon_name, org_id)
|
@@ -94,11 +130,6 @@ class Squad
|
|
94
130
|
sme = @people.get_or_create!(sme).name
|
95
131
|
end
|
96
132
|
|
97
|
-
epo = @metadata.fetch('epo', '')
|
98
|
-
if !epo.empty?
|
99
|
-
epo = @people.get_or_create!(epo).name
|
100
|
-
end
|
101
|
-
|
102
133
|
manager = @metadata.fetch('manager', '')
|
103
134
|
if !manager.empty?
|
104
135
|
manager = @people.get_or_create!(manager).name
|
@@ -110,8 +141,8 @@ class Squad
|
|
110
141
|
if slack
|
111
142
|
slack = "[#{slack}](https://#{@slack_domain}/app_redirect?channel=#{slack.gsub(/^#/, '')})"
|
112
143
|
end
|
113
|
-
# platoon name, squad name, PM, email list, SME, slack, #
|
114
|
-
"|#{platoon_name}|#{@name}|#{pm}|[#{email}](#{email})|#{sme}|#{slack}|#{members.size}|#{manager}|#{
|
144
|
+
# platoon name, squad name, PM, email list, SME, slack, # full time members, squad manager, members
|
145
|
+
"|#{platoon_name}|#{@name}|#{pm}|[#{email}](#{email})|#{sme}|#{slack}|#{members.size}|#{manager}|#{subteam_members}|"
|
115
146
|
end
|
116
147
|
|
117
148
|
def generate_tf(org_id)
|
data/lib/terraorg/version.rb
CHANGED
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: terraorg
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joshua Kwan
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-10-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: countries
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: faraday
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,6 +66,20 @@ dependencies:
|
|
52
66
|
- - "~>"
|
53
67
|
- !ruby/object:Gem::Version
|
54
68
|
version: '0.2'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: minitest
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '5.14'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '5.14'
|
55
83
|
description: Manage an organizational structure with Okta and G-Suite using Terraform
|
56
84
|
email: joshk@triplehelix.org
|
57
85
|
executables:
|
@@ -90,7 +118,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
90
118
|
- !ruby/object:Gem::Version
|
91
119
|
version: '0'
|
92
120
|
requirements: []
|
93
|
-
rubygems_version: 3.0.
|
121
|
+
rubygems_version: 3.0.8
|
94
122
|
signing_key:
|
95
123
|
specification_version: 4
|
96
124
|
summary: terraorg
|