terraorg 0.0.7
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 +7 -0
- data/LICENSE.txt +20 -0
- data/README.md +124 -0
- data/bin/terraorg +100 -0
- data/lib/terraorg/model/org.rb +231 -0
- data/lib/terraorg/model/people.rb +44 -0
- data/lib/terraorg/model/person.rb +63 -0
- data/lib/terraorg/model/platoon.rb +98 -0
- data/lib/terraorg/model/platoons.rb +43 -0
- data/lib/terraorg/model/squad.rb +129 -0
- data/lib/terraorg/model/squads.rb +28 -0
- data/lib/terraorg/model/util.rb +49 -0
- data/lib/terraorg/version.rb +3 -0
- metadata +97 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8ab3d94ce6dcc408a54773d2922f6939cb3ecb2c1f712c15e6bf38d5b4991262
|
4
|
+
data.tar.gz: 697c67ca943cc95ea5aad8a569968f204d0c6ed54e5747b4e99a55090c8faa6c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: eea771dfd0e78861b6bc4d40bd6a55a978ee05a7a2739948bfa1ce7890dd26bd59a534395973b28fa9caff76c06d143cf482bb9d22278f89fb4ba9b92cb06172
|
7
|
+
data.tar.gz: a2199d855c507de8ed37a22a62a20d65a36796637ff7ed55003fd205380655ffb2eebcbbc51720eec747ea46252705a00fa9a528581cc4f5756dab5d27bb1f2c
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2020- Joshua Kwan
|
2
|
+
Copyright 2020 LiveRamp Holdings, Inc.
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
5
|
+
this software and associated documentation files (the "Software"), to deal in
|
6
|
+
the Software without restriction, including without limitation the rights to
|
7
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
8
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
9
|
+
so, subject to the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
12
|
+
copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
20
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
[](https://travis-ci.com/joshk0/terraorg)
|
2
|
+
|
3
|
+
# terraorg
|
4
|
+
|
5
|
+
This tool helps define organizational structure using flat JSON files as a
|
6
|
+
source of truth for who is a member of an organization. Traditionally, there
|
7
|
+
are multiple sources of truth that need to be separately maintained. These JSON
|
8
|
+
files are processed into Terraform files which are meant to create nested
|
9
|
+
groups representing the organization that are usable in Okta and G Suite.
|
10
|
+
|
11
|
+
Additionally, the tool facilitates using this source of truth to generate
|
12
|
+
Markdown documentation about the org as well.
|
13
|
+
|
14
|
+
Based on the org that this tool was originally designed for, orgs are expected
|
15
|
+
to have three levels:
|
16
|
+
|
17
|
+
* *squads*: the base unit of team-dom, containing people, who may be in
|
18
|
+
different geographical regions.
|
19
|
+
* *platoons*: a unit which contains squads and exceptional people who are
|
20
|
+
members of the platoon, but not part of any squad
|
21
|
+
* *org*: The whole organization, including its manager, any exceptional squads
|
22
|
+
not organized into a platoon, and any exceptional people not part of any
|
23
|
+
squad at all but still part of the org.
|
24
|
+
|
25
|
+
The tool generates groups for each granular unit of organization in Okta and G
|
26
|
+
Suite in Terraform. With patching, it could be possible for more organizational
|
27
|
+
systems to be supported.
|
28
|
+
|
29
|
+
## How it works
|
30
|
+
|
31
|
+
Firstly, take your entire existing organization and define it using the
|
32
|
+
constructs of squads, platoons and organization. Groups will be generated;
|
33
|
+
*use these groups to assign access to resources* rather than individually
|
34
|
+
assigning access.
|
35
|
+
|
36
|
+
Whenever a person joins your organization, it should be the responsibility of
|
37
|
+
managers in the organization, or their delegates, to update the organizational
|
38
|
+
JSON files. If done correctly, updating the file, re-running terraorg to
|
39
|
+
generate source code, and applying the resulting source code would instantly
|
40
|
+
grant the person access to required services. Similarly, if a person leaves,
|
41
|
+
remove that person from the file and run the tool again.
|
42
|
+
|
43
|
+
## Model definitions
|
44
|
+
|
45
|
+
See `examples/` directory for examples of `squads.json`, `platoons.json` and
|
46
|
+
`org.json`.
|
47
|
+
|
48
|
+
## Running the tool
|
49
|
+
|
50
|
+
### Environment variables
|
51
|
+
|
52
|
+
The tool expects the following environment variables to be defined and fails if
|
53
|
+
they are missing:
|
54
|
+
|
55
|
+
* `OKTA_API_TOKEN`: An API token with read/write admin access to your Okta
|
56
|
+
organization.
|
57
|
+
* `OKTA_ORG_NAME`: Name of the Okta organization.
|
58
|
+
* `OKTA_BASE_URL`: either `okta.com` or `oktapreview.com` depending on your
|
59
|
+
Okta configuration.
|
60
|
+
* `GSUITE_DOMAIN`: The G Suite domain suffix e.g. `yourcompany.com` which
|
61
|
+
is used by Google Groups.
|
62
|
+
|
63
|
+
The following environment variables are optional; if not specified, ensure
|
64
|
+
that your runtime setup matches the defaults.
|
65
|
+
|
66
|
+
* `TERRAORG_SQUADS`: defaulting to `squads.json`, location of the squads
|
67
|
+
definition.
|
68
|
+
* `TERRAORG_PLATOONS`: defaulting to `platoons.json`, location of the platoons
|
69
|
+
definition.
|
70
|
+
* `TERRAORG_ROOT`: defaulting to `org.json`, location of the root
|
71
|
+
organizational unit.
|
72
|
+
* `TERRAORG_OKTA_CACHE`: defaulting to `okta_cache.json`, location of the Okta
|
73
|
+
name, email and user id lookup cache. Delete this file to force a refresh.
|
74
|
+
* `SLACK_DOMAIN`: defaulting to `yourdomain.slack.com`, prefix of your Slack
|
75
|
+
domain. This is only required for doc generation and not for Terraform.
|
76
|
+
|
77
|
+
### Subcommands
|
78
|
+
|
79
|
+
When running `terraorg`, pass a second argument containing a subcommand.
|
80
|
+
Valid subcommands:
|
81
|
+
|
82
|
+
* `fmt`: Idiomatic formatting for all of your org files which will be
|
83
|
+
written back to the JSON files. Similar behavior to `terraform fmt`.
|
84
|
+
`auto.XXXX.tf`.
|
85
|
+
* `generate-platoons-md`: Generate Markdown documentation for Squads.
|
86
|
+
* `generate-squads-md`: Generate Markdown documentation for Squads.
|
87
|
+
* `generate-tf`: Generate Terraform files. Output files will be of the form
|
88
|
+
* `validate`: Ensure the input JSON files represent a valid, self-consistent
|
89
|
+
organization.
|
90
|
+
|
91
|
+
## Combining with terraform
|
92
|
+
|
93
|
+
See `examples/` directory for how to set up your Terraform workspace for plans
|
94
|
+
and applies of the files generated by this tool. At a minimum, you will need to
|
95
|
+
furnish [articulate/terraform-provider-okta] and
|
96
|
+
[DeviaVir/terraform-provider-gsuite], and enable those as providers.
|
97
|
+
|
98
|
+
Please consult the respective Terraform modules' home pages for more
|
99
|
+
information on how to configure the providers.
|
100
|
+
|
101
|
+
[articulate/terraform-provider-okta]: https://github.com/articulate/terraform-provider-okta
|
102
|
+
[DeviaVir/terraform-provider-gsuite]: https://github.com/DeviaVir/terraform-provider-gsuite
|
103
|
+
|
104
|
+
## Suggested process
|
105
|
+
|
106
|
+
At [LiveRamp], a pull request based workflow leveraging [Atlantis] is used to
|
107
|
+
interact with terraorg. The repository containing LiveRamp's engineering
|
108
|
+
organizational structure is locked down and requires approval from
|
109
|
+
organizational leadership for any change.
|
110
|
+
|
111
|
+
Once approved, Atlantis leverages its API access to Okta and G Suite in order
|
112
|
+
to effect the group changes.
|
113
|
+
|
114
|
+
Similar process can be defined using CI tools such as Jenkins and Circle CI.
|
115
|
+
Implementation of such process is left as an exercise to the reader.
|
116
|
+
|
117
|
+
[LiveRamp]: https://github.com/LiveRamp/
|
118
|
+
[Atlantis]: https://www.runatlantis.io/
|
119
|
+
|
120
|
+
## Future
|
121
|
+
|
122
|
+
* terraorg could generate and push GitHub Teams automatically that line up
|
123
|
+
with existing squad structure.
|
124
|
+
* Terraform and Markdown generation could be factored out to ERB.
|
data/bin/terraorg
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'neatjson'
|
5
|
+
require 'oktakit'
|
6
|
+
|
7
|
+
require 'terraorg/model/org'
|
8
|
+
require 'terraorg/model/people'
|
9
|
+
require 'terraorg/model/platoons'
|
10
|
+
require 'terraorg/model/squads'
|
11
|
+
|
12
|
+
ACTIONS = [
|
13
|
+
'fmt',
|
14
|
+
'generate-platoons-md',
|
15
|
+
'generate-squads-md',
|
16
|
+
'generate-tf',
|
17
|
+
'validate'
|
18
|
+
].freeze
|
19
|
+
|
20
|
+
SQUADS_FILE = ENV.fetch('TERRAORG_SQUADS', 'squads.json')
|
21
|
+
PLATOONS_FILE = ENV.fetch('TERRAORG_PLATOONS', 'platoons.json')
|
22
|
+
ORG_FILE = ENV.fetch('TERRAORG_ROOT', 'org.json')
|
23
|
+
CACHE_FILE = ENV.fetch('TERRAORG_OKTA_CACHE', 'okta_cache.json')
|
24
|
+
GSUITE_DOMAIN = ENV.fetch('GSUITE_DOMAIN')
|
25
|
+
SLACK_DOMAIN = ENV.fetch('SLACK_DOMAIN', 'yourcompany.slack.com')
|
26
|
+
OKTA_ORG_NAME = ENV.fetch('OKTA_ORG_NAME')
|
27
|
+
OKTA_API_TOKEN = ENV.fetch('OKTA_API_TOKEN')
|
28
|
+
OKTA_BASE_URL = ENV.fetch('OKTA_BASE_URL', 'okta.com')
|
29
|
+
|
30
|
+
action = ARGV[0]
|
31
|
+
raise "Invalid action: '#{action}' (valid: #{ACTIONS})" unless ACTIONS.member?(action)
|
32
|
+
|
33
|
+
### Actions that can run without Okta
|
34
|
+
case action
|
35
|
+
when 'fmt'
|
36
|
+
# This People object is created with neither an okta cache nor a okta client.
|
37
|
+
# It accepts any user for the purposes of format validation.
|
38
|
+
people = People.new
|
39
|
+
squads_data = File.read(SQUADS_FILE)
|
40
|
+
squads = Squads.new(JSON.parse(squads_data), people, GSUITE_DOMAIN, SLACK_DOMAIN)
|
41
|
+
platoons_data = File.read(PLATOONS_FILE)
|
42
|
+
platoons = Platoons.new(JSON.parse(platoons_data), squads, people, GSUITE_DOMAIN)
|
43
|
+
org_data = File.read(ORG_FILE)
|
44
|
+
org = Org.new(JSON.parse(org_data), platoons, squads, people, GSUITE_DOMAIN)
|
45
|
+
|
46
|
+
options = {wrap: true, sort: true, after_colon: 1, after_comma: 1}
|
47
|
+
new_squads_data = JSON.neat_generate(squads.to_h, **options) + "\n"
|
48
|
+
new_platoons_data = JSON.neat_generate(platoons.to_h, **options) + "\n"
|
49
|
+
new_org_data = JSON.neat_generate(org.to_h, **options) + "\n"
|
50
|
+
|
51
|
+
if new_squads_data != squads_data
|
52
|
+
puts SQUADS_FILE
|
53
|
+
File.write(SQUADS_FILE, new_squads_data)
|
54
|
+
end
|
55
|
+
|
56
|
+
if new_platoons_data != platoons_data
|
57
|
+
puts PLATOONS_FILE
|
58
|
+
File.write(PLATOONS_FILE, new_platoons_data)
|
59
|
+
end
|
60
|
+
|
61
|
+
if new_org_data != org_data
|
62
|
+
puts ORG_FILE
|
63
|
+
File.write(ORG_FILE, new_org_data)
|
64
|
+
end
|
65
|
+
|
66
|
+
exit
|
67
|
+
end
|
68
|
+
|
69
|
+
### Actions that require Okta credentials
|
70
|
+
if OKTA_BASE_URL == 'oktapreview.com'
|
71
|
+
okta = Oktakit.new(token: OKTA_API_TOKEN, api_endpoint: "https://#{OKTA_ORG_NAME}.#{OKTA_BASE_URL}/api/v1")
|
72
|
+
else
|
73
|
+
okta = Oktakit.new(token: OKTA_API_TOKEN, organization: OKTA_ORG_NAME)
|
74
|
+
end
|
75
|
+
|
76
|
+
people = People.new(okta: okta, cache_file: CACHE_FILE)
|
77
|
+
squads_data = File.read(SQUADS_FILE)
|
78
|
+
squads = Squads.new(JSON.parse(squads_data), people, GSUITE_DOMAIN, SLACK_DOMAIN)
|
79
|
+
platoons_data = File.read(PLATOONS_FILE)
|
80
|
+
platoons = Platoons.new(JSON.parse(platoons_data), squads, people, GSUITE_DOMAIN)
|
81
|
+
org_data = File.read(ORG_FILE)
|
82
|
+
org = Org.new(JSON.parse(org_data), platoons, squads, people, GSUITE_DOMAIN)
|
83
|
+
|
84
|
+
org.validate!
|
85
|
+
|
86
|
+
case action
|
87
|
+
when 'generate-squads-md'
|
88
|
+
# Generate CSV file mirroring the 'Engineering Squads' Squads sheet.
|
89
|
+
puts org.get_squads_md
|
90
|
+
when 'generate-platoons-md'
|
91
|
+
# Generate CSV file mirroring the 'Engineering Squads' Platoons sheet.
|
92
|
+
puts org.get_platoons_md
|
93
|
+
when 'generate-tf'
|
94
|
+
# Generate Terraform code to apply to Okta.
|
95
|
+
org.generate_tf
|
96
|
+
when 'validate'
|
97
|
+
# Validation is a prerequisite, so just output success. It would have
|
98
|
+
# failed earlier otherwise
|
99
|
+
puts 'Organization is valid!'
|
100
|
+
end
|
@@ -0,0 +1,231 @@
|
|
1
|
+
require 'terraorg/model/util'
|
2
|
+
|
3
|
+
class Org
|
4
|
+
MAX_SQUADS_PER_PERSON = 2
|
5
|
+
SCHEMA_VERSION = 'v1'.freeze
|
6
|
+
|
7
|
+
def initialize(parsed_data, platoons, squads, people, gsuite_domain)
|
8
|
+
@people = people
|
9
|
+
@id = parsed_data.fetch('id')
|
10
|
+
@name = parsed_data.fetch('name')
|
11
|
+
@metadata = parsed_data.fetch('metadata', {})
|
12
|
+
@manager = @people.get_or_create!(parsed_data.fetch('manager'))
|
13
|
+
@manager_location = parsed_data.fetch('manager_location')
|
14
|
+
# @member_exception_people is a Hash of Squad::Teams, like the teams attribute in a Squad.
|
15
|
+
@member_exception_people = Hash[parsed_data.fetch('exception_people').map { |p|
|
16
|
+
[p.fetch('location'), Squad::Team.new(p, @people)]
|
17
|
+
}]
|
18
|
+
@member_exception_squad_names = []
|
19
|
+
@member_exception_squads = parsed_data.fetch('exception_squads').map do |s|
|
20
|
+
@member_exception_squad_names.push(s)
|
21
|
+
squads.lookup!(s)
|
22
|
+
end
|
23
|
+
@member_platoon_names = []
|
24
|
+
@member_platoons = parsed_data.fetch('platoons').map do |p|
|
25
|
+
@member_platoon_names.push(p)
|
26
|
+
platoons.lookup!(p)
|
27
|
+
end
|
28
|
+
|
29
|
+
# used to generate a group
|
30
|
+
@gsuite_domain = gsuite_domain
|
31
|
+
|
32
|
+
# used for validate!
|
33
|
+
@platoons = platoons
|
34
|
+
@squads = squads
|
35
|
+
end
|
36
|
+
|
37
|
+
def validate!
|
38
|
+
# Do not allow the JSON files to contain any people who have left.
|
39
|
+
raise "Users have left the company: #{@people.inactive.map(&:id).join(', ')}" unless @people.inactive.empty?
|
40
|
+
|
41
|
+
# Do not allow the org to be totally empty.
|
42
|
+
raise 'Org has no platoons or exception squads' if @member_platoons.size + @member_exception_squads.size == 0
|
43
|
+
|
44
|
+
# Require all platoons to be part of the org.
|
45
|
+
platoon_diff = Set.new(@platoons.all_names) - Set.new(@member_platoon_names)
|
46
|
+
unless platoon_diff.empty?
|
47
|
+
raise "Platoons are not used in the org: #{platoon_diff.to_a.sort}"
|
48
|
+
end
|
49
|
+
|
50
|
+
# Require all squads to be used in the org.
|
51
|
+
squad_diff = Set.new(@squads.all_names) - Set.new(@platoons.all_squad_names) - Set.new(@member_exception_squad_names)
|
52
|
+
unless squad_diff.empty?
|
53
|
+
raise "Squad(s) are not used in the org: #{squad_diff.to_a.sort}"
|
54
|
+
end
|
55
|
+
|
56
|
+
all_squads = (@member_platoons.map(&:member_squads) + @member_exception_squads).flatten
|
57
|
+
seen_squads = {}
|
58
|
+
|
59
|
+
# Validate that a squad is not part of more than one platoon
|
60
|
+
all_squads.map(&:id).each do |id|
|
61
|
+
seen_squads[id] = seen_squads.fetch(id, 0) + 1
|
62
|
+
end
|
63
|
+
more_than_one_platoon = seen_squads.select do |squad, count|
|
64
|
+
count > 1
|
65
|
+
end
|
66
|
+
if !more_than_one_platoon.empty?
|
67
|
+
raise "Squads are part of more than one platoon: #{more_than_one_platoon}"
|
68
|
+
end
|
69
|
+
|
70
|
+
# Validate that a squad member belongs to at most two squads in the entire org
|
71
|
+
squad_count = {}
|
72
|
+
all_squads.map(&:teams).flatten.map(&:values).flatten.map(&:members).flatten.each do |member|
|
73
|
+
squad_count[member.id] = squad_count.fetch(member.id, 0) + 1
|
74
|
+
end
|
75
|
+
more_than_max_squads = squad_count.select do |member, count|
|
76
|
+
count > MAX_SQUADS_PER_PERSON
|
77
|
+
end
|
78
|
+
if !more_than_max_squads.empty?
|
79
|
+
# TODO(joshk): Enforce after consulting with Sean
|
80
|
+
$stderr.puts "WARNING: Members are part of more than #{MAX_SQUADS_PER_PERSON} squads: #{more_than_max_squads}"
|
81
|
+
end
|
82
|
+
|
83
|
+
# Validate that a squad member is not also an org exception
|
84
|
+
exceptions = Set.new(@member_exception_people.map { |_, t| t.members }.flatten.map(&:id))
|
85
|
+
exception_and_squad_member = squad_count.keys.select do |member|
|
86
|
+
exceptions.member? member
|
87
|
+
end
|
88
|
+
if !exception_and_squad_member.empty?
|
89
|
+
raise "Exception members are also squad members: #{exception_and_squad_member}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def members
|
94
|
+
Set.new([@manager] + (@member_platoons + @member_exception_squads).map(&:members).flatten + @member_exception_people.map { |_, t| t.members }.flatten).to_a
|
95
|
+
end
|
96
|
+
|
97
|
+
def get_acl_groups(attr: :id)
|
98
|
+
# Return a LIST_NAME => [MEMBER1, MEMBER2...] hash of ACL groups
|
99
|
+
{ unique_name => members.map(&attr).sort }.
|
100
|
+
merge(get_platoon_acl_groups(attr: attr)).
|
101
|
+
merge(get_exception_squad_acl_groups(attr: attr))
|
102
|
+
end
|
103
|
+
|
104
|
+
def get_platoon_acl_groups(attr: :id)
|
105
|
+
@member_platoons.map { |p| p.get_acl_groups(@id, attr: attr) }.reduce({}, :merge)
|
106
|
+
end
|
107
|
+
|
108
|
+
def get_exception_squad_acl_groups(attr: :id)
|
109
|
+
@member_exception_squads.map { |p| p.get_acl_groups(@id) }.map(&attr).reduce({}, :merge)
|
110
|
+
end
|
111
|
+
|
112
|
+
def unique_name
|
113
|
+
"#{@id}-all"
|
114
|
+
end
|
115
|
+
|
116
|
+
def get_platoons_md
|
117
|
+
# 90 degree rotated version of the Platoons page of the legacy Engineering Squads sheet
|
118
|
+
# Format:
|
119
|
+
# Platoon,Total,Members
|
120
|
+
# [ORG_HUMAN_NAME],[COUNT]
|
121
|
+
# [PLAT1_HUMAN_NAME],[PLAT1_COUNT],[PLAT1_MEMBER1],[PLAT1_MEMBER2],...
|
122
|
+
# [PLAT2_HUMAN_NAME],[PLAT2_COUNT],[PLAT2_MEMBER1],[PLAT2_MEMBER2],...
|
123
|
+
md_lines = [
|
124
|
+
'# Engineering Platoons List',
|
125
|
+
'',
|
126
|
+
'|Platoon|Manager|Total|Members|',
|
127
|
+
'|---|---|---|---|',
|
128
|
+
"|_#{@name} Total_|#{@manager.name}|#{members.size}|",
|
129
|
+
]
|
130
|
+
md_lines += @member_platoons.map(&:get_platoons_psv_row)
|
131
|
+
|
132
|
+
raise "Users have left the company: #{@people.inactive.map(&:id).join(', ')}" unless @people.inactive.empty?
|
133
|
+
md_lines.join("\n")
|
134
|
+
end
|
135
|
+
|
136
|
+
def get_squads_md
|
137
|
+
# Format:
|
138
|
+
# platoon name, squad name, PM, email list, SME, slack, # people, squad manager, eng product owner, members
|
139
|
+
md_lines = [
|
140
|
+
'# Engineering Squads List',
|
141
|
+
'',
|
142
|
+
'|Platoon|Squad|PM|Mailing list|TS SME|Slack|# Engineers|Squad Manager|Eng Product Owner|Members|',
|
143
|
+
'|---|---|---|---|---|---|---|---|---|---|',
|
144
|
+
]
|
145
|
+
md_lines += @member_platoons.map { |s| s.get_squads_psv_rows(@id) }
|
146
|
+
md_lines += @member_exception_squads.map { |s| s.to_md('_No Platoon_', @id) }
|
147
|
+
md_lines.push "|_No Platoon_|_No Squad_|||||#{@member_exception_people.size}|||#{@member_exception_people.values.map(&:to_md).join(' / ')}|"
|
148
|
+
|
149
|
+
raise "Users have left the company: #{@people.inactive.map(&:id).join(', ')}" unless @people.inactive.empty?
|
150
|
+
md_lines.join("\n")
|
151
|
+
end
|
152
|
+
|
153
|
+
def generate_tf
|
154
|
+
tf = @member_platoons.map { |p| p.generate_tf(@id) }.join("\n")
|
155
|
+
File.write('auto.platoons.tf', tf)
|
156
|
+
|
157
|
+
tf = @member_exception_squads.map { |s| s.generate_tf(@id) }.join("\n")
|
158
|
+
File.write('auto.exception_squads.tf', tf)
|
159
|
+
|
160
|
+
# Roll all platoons and exception squads into the org.
|
161
|
+
roll_up_to_org = \
|
162
|
+
@member_exception_squads.map { |s| s.unique_name(@id, nil) } + \
|
163
|
+
@member_platoons.map { |p| p.unique_name(@id) }
|
164
|
+
|
165
|
+
# Generate the org, which contains:
|
166
|
+
# - exception people (added manually to group)
|
167
|
+
# - the org manager (added manually to group)
|
168
|
+
# - all the platoons (via rule)
|
169
|
+
# - all exception squads (via rule)
|
170
|
+
org_condition = roll_up_to_org.map {
|
171
|
+
|n| "\\\"${okta_group.#{n}.id}\\\""
|
172
|
+
}.join(',')
|
173
|
+
|
174
|
+
description = "#{@name} organization worldwide members (terraorg)"
|
175
|
+
tf = <<-EOF
|
176
|
+
# Okta for Organization: #{@name}
|
177
|
+
resource "okta_group" "#{unique_name}" {
|
178
|
+
name = "#{unique_name}"
|
179
|
+
description = "#{description}"
|
180
|
+
users = #{Util.persons_tf(members)}
|
181
|
+
}
|
182
|
+
|
183
|
+
#{Util.gsuite_group_tf(unique_name, @gsuite_domain, members, description)}
|
184
|
+
EOF
|
185
|
+
|
186
|
+
# Generate a special group for all org members grouped by country
|
187
|
+
all_squads = (@member_platoons.map(&:member_squads) + @member_exception_squads).flatten
|
188
|
+
all_locations = {}
|
189
|
+
(all_squads.map(&:teams).flatten + [@member_exception_people]).each do |team|
|
190
|
+
team.each do |location, subteam|
|
191
|
+
all_locations[location] = all_locations.fetch(location, Set.new).merge(subteam.members)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# Manually add the manager to a specific location
|
196
|
+
all_locations[@manager_location] = all_locations.fetch(@manager_location, Set.new).add(@manager)
|
197
|
+
|
198
|
+
all_locations.each do |l, m|
|
199
|
+
name = "#{unique_name}-#{l.downcase}"
|
200
|
+
tf += <<-EOF
|
201
|
+
resource "okta_group" "#{name}" {
|
202
|
+
name = "#{name}"
|
203
|
+
description = "#{@name} organization members based in #{l} (terraorg)"
|
204
|
+
users = #{Util.persons_tf(m)}
|
205
|
+
}
|
206
|
+
EOF
|
207
|
+
end
|
208
|
+
|
209
|
+
File.write('auto.org.tf', tf)
|
210
|
+
end
|
211
|
+
|
212
|
+
# Output a canonical (sorted, formatted) version of this organization.
|
213
|
+
# - Sort the platoon names lexically
|
214
|
+
# - Sort the exception squad ids lexically
|
215
|
+
# - Sort the exception people ids lexically
|
216
|
+
def to_h
|
217
|
+
obj = { 'version' => SCHEMA_VERSION, 'id' => @id, 'name' => @name, 'manager_location' => @manager_location, 'manager' => @manager.id }
|
218
|
+
obj['platoons'] = @platoons.all.map(&:id).sort
|
219
|
+
if @metadata
|
220
|
+
obj['metadata'] = @metadata
|
221
|
+
end
|
222
|
+
if @member_exception_people
|
223
|
+
obj['exception_people'] = @member_exception_people.values.sort_by(&:location).map(&:to_h)
|
224
|
+
end
|
225
|
+
if @member_exception_squads
|
226
|
+
obj['exception_squads'] = @member_exception_squads.map(&:id).sort
|
227
|
+
end
|
228
|
+
|
229
|
+
obj
|
230
|
+
end
|
231
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'terraorg/model/person'
|
2
|
+
|
3
|
+
class People
|
4
|
+
attr_accessor :inactive
|
5
|
+
|
6
|
+
def initialize(okta: nil, cache_file: nil)
|
7
|
+
@okta = okta
|
8
|
+
@people = {}
|
9
|
+
@inactive = []
|
10
|
+
@cache_file = cache_file
|
11
|
+
|
12
|
+
load_cache! if @cache_file
|
13
|
+
end
|
14
|
+
|
15
|
+
def get_or_create!(id)
|
16
|
+
p = @people[id]
|
17
|
+
return p if p
|
18
|
+
|
19
|
+
p = Person.new(id, okta: @okta)
|
20
|
+
if p.active?
|
21
|
+
@people[id] = p
|
22
|
+
else
|
23
|
+
@people.delete(id)
|
24
|
+
@inactive.push p
|
25
|
+
end
|
26
|
+
save_cache! if @cache_file
|
27
|
+
return p
|
28
|
+
end
|
29
|
+
|
30
|
+
def load_cache!
|
31
|
+
return unless File.exist?(@cache_file)
|
32
|
+
|
33
|
+
JSON.parse(File.read(@cache_file)).each do |id, cache|
|
34
|
+
@people[id] = Person.new(id, cached: cache)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def save_cache!
|
39
|
+
# Atomic write cache file
|
40
|
+
cf_new = "#{@cache_file}.new"
|
41
|
+
File.write(cf_new, @people.to_json)
|
42
|
+
File.rename(cf_new, @cache_file)
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
|
3
|
+
class Person
|
4
|
+
ACTIVE_USER_STATUSES = ['ACTIVE', 'PROVISIONED'].freeze
|
5
|
+
|
6
|
+
attr_accessor :id, :name, :okta_id, :email, :status
|
7
|
+
|
8
|
+
def initialize(uid, okta: nil, cached: nil)
|
9
|
+
@id = uid
|
10
|
+
|
11
|
+
if cached
|
12
|
+
@name = cached.fetch('name')
|
13
|
+
@okta_id = cached.fetch('okta_id')
|
14
|
+
@email = cached.fetch('email')
|
15
|
+
@status = cached.fetch('status')
|
16
|
+
|
17
|
+
return
|
18
|
+
elsif !okta
|
19
|
+
# We could just be running in fmt mode, so lie about everything
|
20
|
+
@name = "real name of #{@id}"
|
21
|
+
@okta_id = "fake okta id for #{@id}"
|
22
|
+
@email = "#{@id}@my.domain"
|
23
|
+
@status = 'PROVISIONED'
|
24
|
+
|
25
|
+
return
|
26
|
+
end
|
27
|
+
|
28
|
+
# Retrieve from okta
|
29
|
+
tries = 1
|
30
|
+
total_tries = 5
|
31
|
+
|
32
|
+
begin
|
33
|
+
o = okta.get_user(uid)
|
34
|
+
rescue Faraday::ConnectionFailed => e
|
35
|
+
if tries <= total_tries
|
36
|
+
puts "looking up user #{uid}: #{e} (try #{tries}/#{total_tries})"
|
37
|
+
tries += 1
|
38
|
+
retry
|
39
|
+
end
|
40
|
+
raise
|
41
|
+
end
|
42
|
+
|
43
|
+
if tries > 1
|
44
|
+
puts "looking up user #{uid}: success!"
|
45
|
+
end
|
46
|
+
|
47
|
+
# NOTE: allows users in states other than ACTIVE
|
48
|
+
# if you want to check that, do it outside of here
|
49
|
+
obj = o[0].to_hash
|
50
|
+
@name = obj.fetch(:profile).fetch(:displayName)
|
51
|
+
@okta_id = obj.fetch(:id)
|
52
|
+
@email = obj.fetch(:profile).fetch(:email)
|
53
|
+
@status = obj.fetch(:status)
|
54
|
+
end
|
55
|
+
|
56
|
+
def active?
|
57
|
+
ACTIVE_USER_STATUSES.member?(@status)
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_json(options = nil)
|
61
|
+
{'id' => @id, 'name' => @name, 'okta_id' => @okta_id, 'email' => @email, 'status' => @status}.to_json
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'terraorg/model/util'
|
2
|
+
|
3
|
+
class Platoon
|
4
|
+
attr_accessor :id, :name, :member_exceptions, :member_squads
|
5
|
+
|
6
|
+
def initialize(parsed_data, squads, people, gsuite_domain)
|
7
|
+
@id = parsed_data.fetch('id')
|
8
|
+
@metadata = parsed_data.fetch('metadata', {})
|
9
|
+
@name = parsed_data.fetch('name')
|
10
|
+
@manager = people.get_or_create!(parsed_data.fetch('manager'))
|
11
|
+
@member_exceptions = parsed_data.fetch('exceptions', []).map do |n|
|
12
|
+
people.get_or_create!(n)
|
13
|
+
end
|
14
|
+
@member_squad_names = []
|
15
|
+
@member_squads = parsed_data.fetch('squads').map do |s|
|
16
|
+
@member_squad_names.push(s)
|
17
|
+
squads.lookup!(s)
|
18
|
+
end
|
19
|
+
@gsuite_domain = gsuite_domain
|
20
|
+
end
|
21
|
+
|
22
|
+
def validate!
|
23
|
+
raise 'Platoon has no squads' if @member_squads.size == 0
|
24
|
+
end
|
25
|
+
|
26
|
+
def squad_names
|
27
|
+
@member_squad_names
|
28
|
+
end
|
29
|
+
|
30
|
+
def members
|
31
|
+
Set.new([@manager] + @member_squads.map(&:members).flatten + @member_exceptions).to_a
|
32
|
+
end
|
33
|
+
|
34
|
+
def unique_name(org_id)
|
35
|
+
"#{org_id}-platoon-#{@id}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def get_acl_groups(org_id, platoon: true)
|
39
|
+
if platoon
|
40
|
+
rv = { unique_name(org_id) => {'name' => "#{@name} platoon members worldwide", 'members' => members} }
|
41
|
+
else
|
42
|
+
rv = {}
|
43
|
+
end
|
44
|
+
|
45
|
+
@member_squads.map { |s| s.get_acl_groups(org_id) }.reduce(rv, :merge)
|
46
|
+
end
|
47
|
+
|
48
|
+
def get_platoons_psv_row
|
49
|
+
"|#{@name}|#{@manager.name}|#{members.size}|#{members.map(&:name).sort.join(', ')}|"
|
50
|
+
end
|
51
|
+
|
52
|
+
def get_squads_psv_rows(org_id)
|
53
|
+
@member_squads.map { |s| s.to_md(@name, org_id) }
|
54
|
+
end
|
55
|
+
|
56
|
+
def generate_tf(org_id)
|
57
|
+
tf_id = unique_name(org_id)
|
58
|
+
|
59
|
+
# tf formatted, comma separated references to the group ids for the
|
60
|
+
# squads in this platoon
|
61
|
+
squads_condition = get_acl_groups(org_id, platoon: false).map {
|
62
|
+
|n, _| "\\\"${okta_group.#{n}.id}\\\""
|
63
|
+
}.join(',')
|
64
|
+
|
65
|
+
# tf containing the platoon declaration
|
66
|
+
description = "#{@name} platoon members (terraorg)"
|
67
|
+
rv = <<-EOF
|
68
|
+
# Platoon: #{@name}
|
69
|
+
# Squads: #{squad_names.join(', ')}
|
70
|
+
|
71
|
+
resource "okta_group" "#{tf_id}" {
|
72
|
+
name = "#{tf_id}"
|
73
|
+
description = "#{description}"
|
74
|
+
users = #{Util.persons_tf(members)}
|
75
|
+
}
|
76
|
+
|
77
|
+
#{Util.gsuite_group_tf(tf_id, @gsuite_domain, members, description)}
|
78
|
+
EOF
|
79
|
+
|
80
|
+
# tf containing squads and their members
|
81
|
+
rv += @member_squads.map { |s| s.generate_tf(org_id) }.join("\n")
|
82
|
+
end
|
83
|
+
|
84
|
+
# Output a canonical (sorted, formatted) version of this object.
|
85
|
+
# - Sort the squad ids lexically
|
86
|
+
# - Sort the exceptions lexically
|
87
|
+
def to_h
|
88
|
+
obj = { 'id' => @id, 'name' => @name, 'manager' => @manager.id, 'squads' => @member_squads.map(&:id) }
|
89
|
+
unless @member_exceptions.empty?
|
90
|
+
obj['exceptions'] = @member_exceptions.map(&:id)
|
91
|
+
end
|
92
|
+
unless @metadata.empty?
|
93
|
+
obj['metadata'] = @metadata
|
94
|
+
end
|
95
|
+
|
96
|
+
obj
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'terraorg/model/platoon'
|
2
|
+
|
3
|
+
class Platoons
|
4
|
+
SCHEMA_VERSION = 'v1'.freeze
|
5
|
+
|
6
|
+
def initialize(parsed_data, squads, people, gsuite_domain)
|
7
|
+
version = parsed_data.fetch('version')
|
8
|
+
raise "Unsupported schema version: #{version}" if version != SCHEMA_VERSION
|
9
|
+
|
10
|
+
@platoons = {}
|
11
|
+
parsed_data.fetch('platoons').each do |platoon_raw|
|
12
|
+
p = Platoon.new(platoon_raw, squads, people, gsuite_domain)
|
13
|
+
@platoons[p.id] = p
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def lookup!(name)
|
18
|
+
@platoons.fetch(name)
|
19
|
+
end
|
20
|
+
|
21
|
+
def validate!
|
22
|
+
end
|
23
|
+
|
24
|
+
def all
|
25
|
+
@platoons.values
|
26
|
+
end
|
27
|
+
|
28
|
+
def all_squad_names
|
29
|
+
@platoons.values.map(&:squad_names).flatten
|
30
|
+
end
|
31
|
+
|
32
|
+
def all_names
|
33
|
+
@platoons.keys
|
34
|
+
end
|
35
|
+
|
36
|
+
def members
|
37
|
+
@platoons.map(&:members).flatten
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_h
|
41
|
+
{ 'version' => SCHEMA_VERSION , 'platoons' => @platoons.values.sort_by(&:id).map(&:to_h) }
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'terraorg/model/people'
|
2
|
+
require 'terraorg/model/util'
|
3
|
+
|
4
|
+
class Squad
|
5
|
+
attr_accessor :id, :name, :metadata, :teams
|
6
|
+
|
7
|
+
class Team
|
8
|
+
attr_accessor :location, :members
|
9
|
+
|
10
|
+
def initialize(parsed_data, people)
|
11
|
+
@location = parsed_data.fetch('location')
|
12
|
+
@members = parsed_data.fetch('members', []).map do |n|
|
13
|
+
people.get_or_create!(n)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Output a canonical (sorted, formatted) version of this Team.
|
18
|
+
# - Sort the members in each team
|
19
|
+
def to_h
|
20
|
+
{ 'location' => @location, 'members' => @members.map(&:id).sort }
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_md
|
24
|
+
"**#{@location}**: #{@members.map(&:name).sort.join(', ')}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(id, parsed_data, people, gsuite_domain, slack_domain)
|
29
|
+
@gsuite_domain = gsuite_domain
|
30
|
+
@slack_domain = slack_domain
|
31
|
+
@id = id
|
32
|
+
@metadata = parsed_data.fetch('metadata', {})
|
33
|
+
@name = parsed_data.fetch('name')
|
34
|
+
@people = people
|
35
|
+
|
36
|
+
teams_arr = parsed_data.fetch('team', []).map do |t|
|
37
|
+
Team.new(t, people)
|
38
|
+
end
|
39
|
+
@teams = Hash[teams_arr.map { |t| [t.location, t] }]
|
40
|
+
end
|
41
|
+
|
42
|
+
def members(location: nil)
|
43
|
+
@teams.select { |l, t|
|
44
|
+
location == nil || l == location
|
45
|
+
}.map { |l, t|
|
46
|
+
t.members
|
47
|
+
}.flatten
|
48
|
+
end
|
49
|
+
|
50
|
+
def get_acl_groups(org_id)
|
51
|
+
# each geographically located subteam
|
52
|
+
groups = Hash[@teams.map { |location, team|
|
53
|
+
[unique_name(org_id, location), {'name' => "#{@name} squad members based in #{location}", 'members' => team.members}]
|
54
|
+
}]
|
55
|
+
|
56
|
+
# combination of all subteams
|
57
|
+
groups[unique_name(org_id, nil)] = {'name' => "#{@name} squad worldwide members", 'members' => members}
|
58
|
+
|
59
|
+
groups
|
60
|
+
end
|
61
|
+
|
62
|
+
def unique_name(org_id, location)
|
63
|
+
if location
|
64
|
+
"#{org_id}-squad-#{@id}-#{location.downcase}"
|
65
|
+
else
|
66
|
+
"#{org_id}-squad-#{@id}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def validate!
|
71
|
+
raise 'Squad has no members' if members.size == 0
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_md(platoon_name, org_id)
|
75
|
+
pm = @metadata.fetch('pm', [])
|
76
|
+
pm = pm.map { |p| @people.get_or_create!(p).name }.join(', ')
|
77
|
+
|
78
|
+
sme = @metadata.fetch('sme', '')
|
79
|
+
if !sme.empty?
|
80
|
+
sme = @people.get_or_create!(sme).name
|
81
|
+
end
|
82
|
+
|
83
|
+
epo = @metadata.fetch('epo', '')
|
84
|
+
if !epo.empty?
|
85
|
+
epo = @people.get_or_create!(epo).name
|
86
|
+
end
|
87
|
+
|
88
|
+
manager = @metadata.fetch('manager', '')
|
89
|
+
if !manager.empty?
|
90
|
+
manager = @people.get_or_create!(manager).name
|
91
|
+
end
|
92
|
+
|
93
|
+
subteam_members = @teams.values.map(&:to_md).join(' / ')
|
94
|
+
email = "#{unique_name(org_id, nil)}@#{@gsuite_domain}"
|
95
|
+
slack = @metadata.fetch('slack', '')
|
96
|
+
if slack
|
97
|
+
slack = "[#{slack}](https://#{@slack_domain}/app_redirect?channel=#{slack.gsub(/^#/, '')})"
|
98
|
+
end
|
99
|
+
# platoon name, squad name, PM, email list, SME, slack, # people, squad manager, eng product owner, members
|
100
|
+
"|#{platoon_name}|#{@name}|#{pm}|[#{email}](#{email})|#{sme}|#{slack}|#{members.size}|#{manager}|#{epo}|#{subteam_members}|"
|
101
|
+
end
|
102
|
+
|
103
|
+
def generate_tf(org_id)
|
104
|
+
groups = get_acl_groups(org_id)
|
105
|
+
|
106
|
+
groups.map { |id, group|
|
107
|
+
description = "#{group.fetch('name')} (terraorg)"
|
108
|
+
<<-EOF
|
109
|
+
resource "okta_group" "#{id}" {
|
110
|
+
name = "#{id}"
|
111
|
+
description = "#{description}"
|
112
|
+
users = #{Util.persons_tf(group.fetch('members'))}
|
113
|
+
}
|
114
|
+
|
115
|
+
#{Util.gsuite_group_tf(id, @gsuite_domain, group.fetch('members'), description)}
|
116
|
+
EOF
|
117
|
+
}.join("\n\n")
|
118
|
+
end
|
119
|
+
|
120
|
+
def to_h
|
121
|
+
# Output a canonical (sorted, formatted) version of this Squad.
|
122
|
+
# - Subteams are sorted by location lexically
|
123
|
+
obj = { 'id' => @id, 'name' => @name }
|
124
|
+
obj['team'] = @teams.values.sort_by { |t| t.location }.map(&:to_h)
|
125
|
+
obj['metadata'] = @metadata if @metadata
|
126
|
+
|
127
|
+
obj
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'terraorg/model/squad'
|
2
|
+
|
3
|
+
class Squads
|
4
|
+
SCHEMA_VERSION = 'v1'.freeze
|
5
|
+
|
6
|
+
def initialize(parsed_data, people, gsuite_domain, slack_domain)
|
7
|
+
version = parsed_data.fetch('version')
|
8
|
+
raise "Unsupported squads schema version: #{version}" if version != SCHEMA_VERSION
|
9
|
+
|
10
|
+
@squads = {}
|
11
|
+
parsed_data.fetch('squads').each do |squad|
|
12
|
+
id = squad.fetch('id')
|
13
|
+
@squads[id] = Squad.new(id, squad, people, gsuite_domain, slack_domain)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def all_names
|
18
|
+
@squads.keys
|
19
|
+
end
|
20
|
+
|
21
|
+
def lookup!(name)
|
22
|
+
@squads.fetch(name)
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_h
|
26
|
+
{ 'version' => SCHEMA_VERSION, 'squads' => @squads.values.sort_by(&:id).map(&:to_h) }
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
class Util
|
2
|
+
# Take a list of Persons and turn it into a newline delimited, comma
|
3
|
+
# separated array definition suitable for inclusion in terraform. Each line
|
4
|
+
# contains an okta id and a comment indicating the person's name.
|
5
|
+
def self.persons_tf(persons)
|
6
|
+
"[\n" + persons.map { |p| " \"#{p.okta_id}\", # #{p.name}" }.join("\n") + "\n]\n"
|
7
|
+
end
|
8
|
+
|
9
|
+
# Take a list of Persons and generate a gsuite_group containing all of those
|
10
|
+
# members with expected organizational settings.
|
11
|
+
def self.gsuite_group_tf(name, domain, persons, description)
|
12
|
+
email = "#{name}@#{domain}"
|
13
|
+
tf = <<-TERRAFORM
|
14
|
+
# G Suite group for #{email}
|
15
|
+
resource "gsuite_group" "#{name}" {
|
16
|
+
email = "#{email}"
|
17
|
+
name = "#{name}"
|
18
|
+
description = "#{description}"
|
19
|
+
}
|
20
|
+
|
21
|
+
resource "gsuite_group_settings" "#{name}" {
|
22
|
+
email = gsuite_group.#{name}.email
|
23
|
+
who_can_discover_group = "ALL_IN_DOMAIN_CAN_DISCOVER"
|
24
|
+
who_can_view_membership = "ALL_IN_DOMAIN_CAN_VIEW"
|
25
|
+
who_can_leave_group = "NONE_CAN_LEAVE"
|
26
|
+
who_can_join = "INVITED_CAN_JOIN"
|
27
|
+
who_can_post_message = "ALL_IN_DOMAIN_CAN_POST"
|
28
|
+
}
|
29
|
+
|
30
|
+
resource "gsuite_group_members" "#{name}" {
|
31
|
+
group_email = gsuite_group.#{name}.email
|
32
|
+
TERRAFORM
|
33
|
+
|
34
|
+
# Add a member block for everyone
|
35
|
+
# downcase is used as internal G Suite representation is always lowercase
|
36
|
+
# this avoids unnecessary state churn
|
37
|
+
persons.each do |p|
|
38
|
+
tf += <<-TERRAFORM
|
39
|
+
member {
|
40
|
+
email = "#{p.email.downcase}"
|
41
|
+
role = "MEMBER"
|
42
|
+
}
|
43
|
+
TERRAFORM
|
44
|
+
end
|
45
|
+
|
46
|
+
tf += "\n}"
|
47
|
+
tf
|
48
|
+
end
|
49
|
+
end
|
metadata
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: terraorg
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.7
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Joshua Kwan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-03-04 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: faraday
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: neatjson
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.9'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.9'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: oktakit
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.2'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.2'
|
55
|
+
description: Manage an organizational structure with Okta and G-Suite using Terraform
|
56
|
+
email: joshk@triplehelix.org
|
57
|
+
executables:
|
58
|
+
- terraorg
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- LICENSE.txt
|
63
|
+
- README.md
|
64
|
+
- bin/terraorg
|
65
|
+
- lib/terraorg/model/org.rb
|
66
|
+
- lib/terraorg/model/people.rb
|
67
|
+
- lib/terraorg/model/person.rb
|
68
|
+
- lib/terraorg/model/platoon.rb
|
69
|
+
- lib/terraorg/model/platoons.rb
|
70
|
+
- lib/terraorg/model/squad.rb
|
71
|
+
- lib/terraorg/model/squads.rb
|
72
|
+
- lib/terraorg/model/util.rb
|
73
|
+
- lib/terraorg/version.rb
|
74
|
+
homepage: https://github.com/LiveRamp/terraorg
|
75
|
+
licenses:
|
76
|
+
- MIT
|
77
|
+
metadata: {}
|
78
|
+
post_install_message:
|
79
|
+
rdoc_options: []
|
80
|
+
require_paths:
|
81
|
+
- lib
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '2.3'
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '0'
|
92
|
+
requirements: []
|
93
|
+
rubygems_version: 3.0.3
|
94
|
+
signing_key:
|
95
|
+
specification_version: 4
|
96
|
+
summary: terraorg
|
97
|
+
test_files: []
|