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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9dcc3e713e1ec4a3166adfd99ecb9cccb52efd37f489fc18ff2393be0b79ac1a
4
- data.tar.gz: 0d12305ad0d7f2aa129336010f719f0d1361c0d28b2fa9466c2c1205d280830b
3
+ metadata.gz: 46107b71a1eace06c51463513c5b495b9549ec19ebc911103ec0d7f236fec6f8
4
+ data.tar.gz: b05708c67d359a3040eca8724140603d5c4debd6e2f1a25c87034e8a9b60e7be
5
5
  SHA512:
6
- metadata.gz: 19df8bcc655abfe6eb89a7d5e8d50d7f76298a492b14cb2f37369b17b2f396822897d264d6f861f215faabd0e74af0dc72a23756cac1e319c34d36a60c915783
7
- data.tar.gz: c3b93acbeaa7f18f6f64c4343944649acf2630a99823b53fce3789c20fb1fa8a5be8bebf9159c2a48ae39eb96b5d4af1c669b23d879087a38104f36d330aeb09
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
@@ -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', 'yourcompany.slack.com')
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
- org.validate!
99
+ strict = (STRICT_VALIDATION == 'true')
100
+ org.validate!(strict: strict)
99
101
 
100
102
  case action
101
103
  when 'generate-squads-md'
@@ -15,7 +15,8 @@
15
15
  require 'terraorg/model/util'
16
16
 
17
17
  class Org
18
- MAX_SQUADS_PER_PERSON = 2
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
- raise "Users have left the company: #{@people.inactive.map(&:id).join(', ')}" unless @people.inactive.empty?
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
- raise 'Org has no platoons or exception squads' if @member_platoons.size + @member_exception_squads.size == 0
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
- raise "Platoons are not used in the org: #{platoon_diff.to_a.sort}"
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
- raise "Squad(s) are not used in the org: #{squad_diff.to_a.sort}"
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
- raise "Squads are part of more than one platoon: #{more_than_one_platoon}"
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 at most two squads in the entire org
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.each do |member|
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 > MAX_SQUADS_PER_PERSON
105
+ count > MAX_MEMBER_SQUADS_PER_PERSON
91
106
  end
92
107
  if !more_than_max_squads.empty?
93
- # TODO(joshk): Enforce after consulting with Sean
94
- $stderr.puts "WARNING: Members are part of more than #{MAX_SQUADS_PER_PERSON} squads: #{more_than_max_squads}"
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
- raise "Exception members are also squad members: #{exception_and_squad_member}"
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|Eng Product Owner|Members|',
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 generate_tf
168
- tf = @member_platoons.map { |p| p.generate_tf(@id) }.join("\n")
169
- File.write('auto.platoons.tf', tf)
205
+ def generate_tf_platoons
206
+ @member_platoons.map { |p| p.generate_tf(@id) }.join("\n")
207
+ end
170
208
 
171
- tf = @member_exception_squads.map { |s| s.generate_tf(@id) }.join("\n")
172
- File.write('auto.exception_squads.tf', tf)
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 = "#{@name} organization members based in #{l} (terraorg)"
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
@@ -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
- @location = parsed_data.fetch('location')
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
- { 'location' => @location, 'members' => @members.map(&:id).sort }
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
- def members(location: nil)
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 { |l, t|
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.members}]
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' => 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
- raise 'Squad has no members' if members.size == 0
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, # people, squad manager, eng product owner, members
114
- "|#{platoon_name}|#{@name}|#{pm}|[#{email}](#{email})|#{sme}|#{slack}|#{members.size}|#{manager}|#{epo}|#{subteam_members}|"
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)
@@ -13,5 +13,5 @@
13
13
  # limitations under the License.
14
14
 
15
15
  module Terraorg
16
- VERSION = '0.1.0'
16
+ VERSION = '0.5.0'
17
17
  end
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.1.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-03-30 00:00:00.000000000 Z
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.3
121
+ rubygems_version: 3.0.8
94
122
  signing_key:
95
123
  specification_version: 4
96
124
  summary: terraorg