terraorg 0.1.0 → 0.5.0
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 +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
|
+

|
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
|