cloudstrap-azure 0.4.6.pre

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ca27473cedeb9289319e9ae294bd2f4a6e86637ad47a6c0ee969379db4458747
4
+ data.tar.gz: 9593998936bd33a2706017452203d52f0d06d09a46ed7d92f3b026f5f0692e07
5
+ SHA512:
6
+ metadata.gz: c205b22ad86de57064e64a21f5d46bc6cea9d2af73110dfeb9cc94c95d68621a850e34cc75fbe92ff8a1fd784b7107e25a3e67be2f4bf71e772d820c9028378c
7
+ data.tar.gz: 6acdf0b6a0870714d9f2bc5f95f9361b0c8c2ff431b3df75a448474c912a4c3f9a0a36978ff81432a285232ac07cfa1de97deaa34804522af57c3fce17b586ce
checksums.yaml.gz.sig ADDED
Binary file
data.tar.gz.sig ADDED
Binary file
data/.gitignore ADDED
@@ -0,0 +1,57 @@
1
+
2
+ # Created by https://www.gitignore.io/api/ruby
3
+
4
+ ### Ruby ###
5
+ *.gem
6
+ *.rbc
7
+ /.config
8
+ /coverage/
9
+ /InstalledFiles
10
+ /pkg/
11
+ /spec/reports/
12
+ /spec/examples.txt
13
+ /test/tmp/
14
+ /test/version_tmp/
15
+ /tmp/
16
+
17
+ # Used by dotenv library to load environment variables.
18
+ # .env
19
+
20
+ ## Specific to RubyMotion:
21
+ .dat*
22
+ .repl_history
23
+ build/
24
+ *.bridgesupport
25
+ build-iPhoneOS/
26
+ build-iPhoneSimulator/
27
+
28
+ ## Specific to RubyMotion (use of CocoaPods):
29
+ #
30
+ # We recommend against adding the Pods directory to your .gitignore. However
31
+ # you should judge for yourself, the pros and cons are mentioned at:
32
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
33
+ #
34
+ # vendor/Pods/
35
+
36
+ ## Documentation cache and generated files:
37
+ /.yardoc/
38
+ /_yardoc/
39
+ /doc/
40
+ /rdoc/
41
+
42
+ ## Environment normalization:
43
+ /.bundle/
44
+ /vendor/bundle
45
+ /lib/bundler/man/
46
+
47
+ # for a library or gem, you might want to ignore these files since the code is
48
+ # intended to run in multiple environments; otherwise, check them in:
49
+ # Gemfile.lock
50
+ # .ruby-version
51
+ # .ruby-gemset
52
+
53
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
54
+ .rvmrc
55
+
56
+
57
+ # End of https://www.gitignore.io/api/ruby
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+
2
+ The MIT License (MIT)
3
+ Copyright © 2018 Chris Olstrom <chris@olstrom.com>
4
+ Copyright © 2018 SUSE LLC
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the “Software”), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in
14
+ all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ THE SOFTWARE.
data/README.org ADDED
@@ -0,0 +1,37 @@
1
+ #+TITLE: cloudstrap-azure
2
+ #+LATEX: \pagebreak
3
+
4
+ * Overview
5
+
6
+ ~cloudstrap-azure~ deploys SCF to ACS.
7
+
8
+ * Prerequisites
9
+
10
+ - Logged in to Azure via ~az login~ at least once.
11
+
12
+ * Commands
13
+
14
+ ** cloudstrap-azure environment
15
+
16
+ Deals with authentication for the Azure and Microsoft Graph services.
17
+
18
+ - Given no arguments, prints JSON containing access tokens to STDOUT.
19
+ - Given some arguments, sets up an environment containing access tokens, then execs into its arguments.
20
+
21
+ ** cloudstrap-azure configure
22
+
23
+ An interactive configuration tool for ~cloudstrap-azure~. Requires access
24
+ tokens in the environment, as produced by the ~environment~ command.
25
+
26
+ ** cloudstrap-azure deploy
27
+
28
+ A tool for deploying SCF on ACS. Requires access tokens and configuration.
29
+
30
+ * License
31
+
32
+ ~cloudstrap-azure~ is available under the [[https://tldrlegal.com/license/mit-license][MIT License]]. See ~LICENSE.txt~ for the
33
+ full text.
34
+
35
+ * Contributors
36
+
37
+ - [[https://colstrom.github.io/][Chris Olstrom]] | [[mailto:chris@olstrom.com][e-mail]] | [[https://twitter.com/ChrisOlstrom][Twitter]]
@@ -0,0 +1,40 @@
1
+ Gem::Specification.new do |gem|
2
+ tag = `git describe --tags --abbrev=0`.chomp
3
+
4
+ gem.name = 'cloudstrap-azure'
5
+ gem.homepage = 'https://github.com/colstrom/cloudstrap-azure'
6
+ gem.summary = 'Cloudstrap for Azure'
7
+
8
+ gem.version = "#{tag}.pre"
9
+ gem.licenses = ['MIT']
10
+ gem.authors = ['Chris Olstrom']
11
+ gem.email = 'chris@olstrom.com'
12
+
13
+ gem.cert_chain = ['trust/certificates/colstrom.pem']
14
+ gem.signing_key = File.expand_path ENV.fetch 'GEM_SIGNING_KEY'
15
+
16
+ gem.files = `git ls-files -z`.split("\x0")
17
+ gem.test_files = `git ls-files -z -- {test,spec,features}/*`.split("\x0")
18
+ gem.executables = `git ls-files -z -- command/[^internal/]*`.split("\x0").map { |f| File.basename(f) }
19
+
20
+ gem.require_paths = ['lib']
21
+ gem.bindir = 'command'
22
+
23
+ gem.add_runtime_dependency 'azure_graph_rbac', '~> 0.16', '>= 0.16.0'
24
+ gem.add_runtime_dependency 'azure_mgmt_authorization' '~> 0.17', '>= 0.17.0'
25
+ gem.add_runtime_dependency 'azure_mgmt_compute' '~> 0.17', '>= 0.17.0'
26
+ gem.add_runtime_dependency 'azure_mgmt_container_service', '~> 0.16', '>= 0.16.0'
27
+ gem.add_runtime_dependency 'azure_mgmt_network', '~> 0.16', '>= 0.16.0'
28
+ gem.add_runtime_dependency 'azure_mgmt_resources' '~> 0.16', '>= 0.16.0'
29
+ gem.add_runtime_dependency 'azure_mgmt_subscriptions' '~> 0.16', '>= 0.16.0'
30
+ gem.add_runtime_dependency 'chamber', '~> 2.12', '>= 2.12.0'
31
+ gem.add_runtime_dependency 'chronic', '~> 0.10', '>= 0.10.0'
32
+ gem.add_runtime_dependency 'concurrent-ruby', '~> 1.0', '>= 1.0.5'
33
+ gem.add_runtime_dependency 'pastel', '~> 0.7', '>= 0.7.0'
34
+ gem.add_runtime_dependency 'sshkey' '~> 1.9', '>= 1.9.0'
35
+ gem.add_runtime_dependency 'tty-prompt', '~> 0.16', '>= 0.16.0'
36
+ gem.add_runtime_dependency 'tty-spinner', '~> 0.8', '>= 0.8.0'
37
+ gem.add_runtime_dependency 'tty-which', '~> 0.3', '>= 0.3.0'
38
+ gem.add_runtime_dependency 'uuid', '~> 2.3', '>= 2.3.8'
39
+ gem.add_runtime_dependency 'xxhash', '~> 0.4', '>= 0.4.0'
40
+ end
@@ -0,0 +1,13 @@
1
+ #! /usr/bin/env ruby
2
+ # -*- ruby -*-
3
+
4
+ PATH = [
5
+ File.expand_path(File.join(__dir__, 'internal')),
6
+ ENV['PATH']
7
+ ].join(':')
8
+
9
+ PROGRAM = File.basename($PROGRAM_NAME)
10
+
11
+ abort unless (COMMAND = ARGV.shift)
12
+
13
+ exec({ 'PATH' => PATH }, "#{PROGRAM}.#{COMMAND}", *ARGV)
@@ -0,0 +1,201 @@
1
+ #! /usr/bin/env ruby -W0
2
+ # coding: utf-8
3
+ # -*- ruby -*-
4
+
5
+ require 'securerandom' # Ruby Standard Library
6
+ require 'yaml' # Ruby Standard Library
7
+
8
+ require 'azure_mgmt_container_service' # MIT License
9
+ require 'azure_mgmt_subscriptions' # MIT License
10
+ require 'chamber' # MIT License
11
+ require 'chronic' # MIT License
12
+ require 'pastel' # MIT License
13
+ require 'sshkey' # MIT License
14
+ require 'tty-prompt' # MIT License
15
+ require 'uuid' # MIT License
16
+ require 'xxhash' # MIT License
17
+
18
+ ####################
19
+ # Helper Functions #
20
+ ####################
21
+
22
+ ConstantsToHash = (
23
+ ->(object) {
24
+ object.constants.zip(
25
+ object.constants.map { |constant|
26
+ object.const_get(
27
+ constant) }).to_h })
28
+
29
+ SelectFromMenu = (
30
+ ->(question, choices, default: nil) {
31
+ TTY::Prompt.new.select(question, filter: true) { |menu|
32
+ (choices.is_a?(Hash) ? choices : choices.zip(choices).to_h).each { |text, value| menu.choice(text, value) }
33
+ (choices.is_a?(Hash) ? choices.values : choices).index(default).tap { |index| menu.default(1+index) if index }}})
34
+
35
+ Ask = (
36
+ ->(question, default: nil) {
37
+ TTY::Prompt.new.ask(question) { |ask| ask.default(default) if default }})
38
+
39
+ AskDate = (
40
+ ->(question, default: nil) {
41
+ (Chronic.parse(Ask.(question, default: default)) || AskDate.(question, default: default)).utc })
42
+
43
+ AskSecret = (
44
+ ->(question, default: nil) {
45
+ TTY::Prompt.new.mask(question) { |ask| ask.default(default) if default }})
46
+
47
+ Slider = (
48
+ ->(question, **options) {
49
+ TTY::Prompt.new.slider(question, options) })
50
+
51
+ Confirm = (
52
+ ->(question) {
53
+ TTY::Prompt.new.yes?(question)})
54
+
55
+ Warn = (
56
+ ->(message) {
57
+ TTY::Prompt.new.warn(message)})
58
+
59
+ Error = (
60
+ ->(message) {
61
+ TTY::Prompt.new.error(message)})
62
+
63
+ Bold = (
64
+ ->(string) {
65
+ Pastel.new.bold(string)})
66
+
67
+ InitialSetup = -> {
68
+ File.write('settings.yml', '---')
69
+ %x(chamber init)
70
+ Chamber.load}
71
+
72
+ #################
73
+ # Sanity Checks #
74
+ #################
75
+
76
+ PROGRAM = "cloudstrap-azure"
77
+
78
+ ENV.fetch('MANAGEMENT_AZURE_COM_ACCESS_TOKEN') {
79
+ Error.("Access Token for #{Bold.('management.azure.com')} not found in Environment
80
+
81
+ If you have already logged in to Azure using the #{Bold.('az login')} command,
82
+ you can use #{Bold.(PROGRAM)}'s environment wrapper:
83
+ ")
84
+ STDERR.puts Bold.("#{PROGRAM} environment -- #{PROGRAM} configure\n")
85
+ exit Errno::EINVAL::Errno}
86
+
87
+ begin
88
+ unless File.exist?('settings.yml')
89
+ Warn.("It looks like this is your first time running #{Bold.(PROGRAM)}.
90
+
91
+ Do you want to run the initial setup? This will create several files in the
92
+ current directory, including encryption keys and configuration files.
93
+ ")
94
+
95
+ InitialSetup.call if Confirm.("Write files to #{Bold.(Dir.pwd)}?")
96
+ end
97
+ rescue Interrupt, TTY::Reader::InputInterrupt
98
+ exit Errno::EINTR::Errno
99
+ end
100
+
101
+ #############
102
+ # Constants #
103
+ #############
104
+
105
+ ORCHESTRATOR_TYPES = (
106
+ ConstantsToHash.(
107
+ Azure::ContainerService::Mgmt::V2017_01_31::Models::ContainerServiceOrchestratorTypes))
108
+
109
+ VM_SIZES = (
110
+ ConstantsToHash.(
111
+ Azure::ContainerService::Mgmt::V2017_01_31::Models::ContainerServiceVMSizeTypes))
112
+
113
+ AzureSubscriptionsAPI = (
114
+ ::Azure::Subscriptions::Mgmt::V2016_06_01::SubscriptionClient.new(
115
+ ::MsRest::TokenCredentials.new(
116
+ ENV.fetch('MANAGEMENT_AZURE_COM_ACCESS_TOKEN'))))
117
+
118
+ AvailableSubscriptions = (
119
+ ->(client) {
120
+ client
121
+ .subscriptions
122
+ .list
123
+ .map { |subscription| [subscription.display_name, subscription.subscription_id] }
124
+ .to_h })
125
+
126
+ AvailableTenants = (
127
+ ->(client) {
128
+ client
129
+ .tenants
130
+ .list
131
+ .map(&:tenant_id) })
132
+
133
+ AvailableLocations = (
134
+ ->(client, subscription_id) {
135
+ client
136
+ .subscriptions
137
+ .list_locations(subscription_id)
138
+ .value
139
+ .map { |location| [location.display_name, location.name] }
140
+ .to_h })
141
+
142
+ DEFAULTS = {
143
+ 'tenant_id' => ENV['AZURE_TENANT_ID'],
144
+ 'subscription_id' => ENV['AZURE_SUBSCRIPTION_ID'],
145
+ 'location' => 'westus',
146
+ 'orchestrator_type' => 'Kubernetes',
147
+ 'vm_size' => 'Standard_D2_v2',
148
+ 'role_definition' => 'Contributor',
149
+ 'admin_username' => 'scf-admin',
150
+ 'agent_count' => 3,
151
+ 'deployment_name' => 'cloudstrap',
152
+ 'master_dns_suffix' => 'master',
153
+ 'agent_dns_suffix' => 'agent',
154
+ 'uuid' => UUID.generate,
155
+ 'credential_end_date' => 'two weeks from now',
156
+ '_secure_ssh_private_key' => SSHKey.generate.private_key,
157
+ '_secure_password' => SecureRandom.uuid,
158
+ }
159
+
160
+ LOADED = DEFAULTS.merge(Chamber.env.to_hash)
161
+
162
+ SECRETS = Set.new(Chamber.env.securable.keys)
163
+ SECRETS.each { |key| LOADED["_secure_#{key}"] = LOADED.delete(key) }
164
+
165
+ ################
166
+ # Main Program #
167
+ ################
168
+
169
+ begin
170
+ subscription_id = SelectFromMenu.('Subscription ID:', AvailableSubscriptions.(AzureSubscriptionsAPI), default: LOADED['subscription_id'])
171
+
172
+ INTERACTIVE = {
173
+ 'tenant_id' => SelectFromMenu.('Tenant ID:', AvailableTenants.(AzureSubscriptionsAPI), default: LOADED['tenant_id']),
174
+ 'location' => SelectFromMenu.('Location:', AvailableLocations.(AzureSubscriptionsAPI, subscription_id), default: LOADED['location']),
175
+ 'orchestrator_type' => SelectFromMenu.('Orchestrator Type:', ORCHESTRATOR_TYPES, default: LOADED['orchestrator_type']),
176
+ 'vm_size' => SelectFromMenu.('VM Size:', VM_SIZES, default: LOADED['vm_size']),
177
+ 'agent_count' => Slider.('Agent Count:', min: 1, max: 5, step: 2),
178
+ 'uuid' => Ask.('UUID', default: LOADED['uuid']),
179
+ 'deployment_name' => Ask.('Deployment Name:', default: LOADED['deployment_name']),
180
+ 'role_definition' => Ask.('Role Definition:', default: LOADED['role_definition']),
181
+ 'admin_username' => Ask.('Admin Username:', default: LOADED['admin_username']),
182
+ 'master_dns_suffix' => Ask.('Master DNS Suffix:', default: LOADED['master_dns_suffix']),
183
+ 'agent_dns_suffix' => Ask.('Agent DNS Suffix:', default: LOADED['agent_dns_suffix']),
184
+ '_secure_password' => AskSecret.('Password:', default: LOADED['_secure_password']),
185
+ '_secure_ssh_private_key' => AskSecret.('SSH Private Key:', default: LOADED['_secure_ssh_private_key']),
186
+ 'credential_end_date' => AskDate.('Password Valid Until:', default: LOADED['credential_end_date']),
187
+ }.merge({'subscription_id' => subscription_id})
188
+
189
+ FINAL = LOADED.merge INTERACTIVE
190
+ FINAL['identifier'] = [FINAL['deployment_name'], FINAL['uuid']].join('.')
191
+ FINAL['dns_prefix'] = [FINAL['deployment_name'], XXhash.xxh32(FINAL['identifier'])].join('-')
192
+ FINAL['credential_end_date'] = Chronic.parse(FINAL['credential_end_date'].iso8601).utc.iso8601
193
+
194
+ yaml = YAML.dump Chamber.instance.encrypt(FINAL.reject { |_, v| v.nil? }.sort.to_h)
195
+ saving = File.expand_path(File.join(Dir.pwd, 'settings.yml'))
196
+
197
+ puts Pastel.new.bold "\n#{yaml}"
198
+ File.write(saving, yaml) if Confirm.("Save to #{saving}?")
199
+ rescue Interrupt, TTY::Reader::InputInterrupt
200
+ exit Errno::EINTR::Errno
201
+ end
@@ -0,0 +1,478 @@
1
+ #! /usr/bin/env ruby -W0
2
+ # coding: utf-8
3
+ # -*- ruby -*-
4
+
5
+ require 'time' # Ruby Standard Library
6
+
7
+ require 'azure_graph_rbac' # MIT License
8
+ require 'azure_mgmt_authorization' # MIT License
9
+ require 'azure_mgmt_compute' # MIT License
10
+ require 'azure_mgmt_container_service' # MIT License
11
+ require 'azure_mgmt_network' # MIT License
12
+ require 'azure_mgmt_resources' # MIT License
13
+ require 'chamber' # MIT License
14
+ require 'concurrent' # MIT License
15
+ require 'pastel' # MIT License
16
+ require 'sshkey' # MIT License
17
+ require 'tty-prompt' # MIT License
18
+ require 'tty-spinner' # MIT License
19
+
20
+ #############
21
+ # Constants #
22
+ #############
23
+
24
+ CREDENTIALS = {
25
+ 'https://graph.windows.net' => (
26
+ MsRest::TokenCredentials.new(
27
+ ENV.fetch('GRAPH_WINDOWS_NET_ACCESS_TOKEN'))),
28
+ 'https://management.azure.com' => (
29
+ MsRest::TokenCredentials.new(
30
+ ENV.fetch('MANAGEMENT_AZURE_COM_ACCESS_TOKEN'))),
31
+ }
32
+
33
+ SUBSCRIPTION_ID = Chamber.env.subscription_id
34
+ TENANT_ID = Chamber.env.tenant_id
35
+ LOCATION = Chamber.env.location
36
+
37
+ SPINNER_FORMAT = (Chamber.env[:spinner] || :arrow_pulse).to_sym
38
+
39
+ ENABLE_SWAP_ACCOUNTING = %q{sudo sed -i 's/GRUB_CMDLINE_LINUX_DEFAULT=\"console=tty1 console=ttyS0 earlyprintk=ttyS0 rootdelay=300\"/GRUB_CMDLINE_LINUX_DEFAULT=\"console=tty1 console=ttyS0 earlyprintk=ttyS0 rootdelay=300 swapaccount=1\"/g' /etc/default/grub.d/50-cloudimg-settings.cfg}
40
+ UPDATE_GRUB = %q{sudo update-grub}
41
+
42
+ ####################
43
+ # Helper Functions #
44
+ ####################
45
+
46
+ Nothing = ->(*) { nil }
47
+ StateEnabled = ->(object) { 'Enabled' == object.state }
48
+ FirstIfOnly = ->(list) { list.first if (1 == list.size) }
49
+ Properties = ->(*properties) { ->(object) { properties.map(&Property).map { |ƒ| ƒ.(object) } } }
50
+ SelectFromMenu = ->(title, choices) { TTY::Prompt.new.select(title, filter: true) { |menu| choices.each { |choice| menu.choice(*choice) }}}
51
+ Property = ->(property, object) { object.public_send(property) if object.respond_to?(property) }.curry
52
+ RespondsTo = ->(method, object) { object.respond_to?(method) }.curry
53
+ SendTo = ->(object, *args) { object.public_send(*args) }.curry(2)
54
+ Bind = ->(name, value, object) { object.tap { SendTo.(object, "#{name}=", value) } }.curry
55
+ Bindable = ->(name, object) { RespondsTo.("#{name}=", object) }.curry
56
+ BindConstant = ->(namespace, name, value) { namespace.const_set name, value }.curry
57
+ BindConstants = ->(namespace, interfaces) { interfaces.map { |constant, function| BindConstant.(namespace, constant, function) } }.curry
58
+ Itself = ->(object) { object.itself }
59
+ If = ->(predicate, consequent, alternative = Nothing) {
60
+ ->(*arguments) {
61
+ predicate.(*arguments) ? consequent.(*arguments) : alternative.(*arguments) } }
62
+ ApplyIf = ->(predicate, consequent) { If.(predicate, consequent, Itself) }
63
+
64
+ WhileSpinning = lambda do |message, &block|
65
+ Concurrent::IVar.new.tap do |ivar|
66
+ TTY::Spinner.new("[:spinner] #{message}", format: SPINNER_FORMAT).tap do |spinner|
67
+ spinner.auto_spin
68
+ ivar.set block.call
69
+ end.success
70
+ end.value
71
+ end
72
+
73
+ BindLocation = Bind.(:location, LOCATION)
74
+
75
+ AsyncableMethods = ->(object) {
76
+ candidates = object.methods.map(&:to_s)
77
+
78
+ candidates
79
+ .select { |c| candidates.any? { |m| m == "#{c}_async" } }
80
+ .reject { |c| c.start_with? 'begin_' }
81
+ .reject { |c| c.end_with? '_next' }
82
+ .map { |c| object.method(c) }
83
+ }
84
+
85
+ MethodsReturningSiblings = ->(object) {
86
+ object
87
+ .methods
88
+ .map { |method| object.method(method) }
89
+ .select { |method| method.arity.zero? }
90
+ .select { |method| method.owner == object.class }
91
+ .select { |method| ParentClass.(method.call) == ParentClass.(object) }
92
+ }
93
+
94
+ Ancestors = ->(object) { ClassOf.(object).ancestors }
95
+ ClassName = ->(object) { ClassOf.(object).name }
96
+ ClassNameParts = ->(object) { ClassName.(object).split('::') }
97
+ ClassOf = ->(object) { object.is_a?(Class) ? object : ClassOf.(object.class) }
98
+ FormatMethodName = ->(method) { method.name.to_s.split('_').map(&:capitalize).join }
99
+ InstanceMethods = ->(object) { ClassOf.(object).instance_methods(false).map(&InstanceMethod.(object)) }
100
+ InstanceOf = ->(object) { object.instance_of?(ClassOf.(object)) ? object : ClassOf.(object).new }
101
+ OwnClass = ->(object) { ClassNameParts.(object).last }
102
+ ParentClass = ->(object) { ClassNameParts.(object).reverse.drop(1).reverse.join('::') }
103
+ RequiredArguments = ->(method) { method.parameters.count { |type, _| type == :req } }
104
+
105
+ HasAncestor = ->(ancestor, object) { Ancestors.(object).include?(ancestor) }.curry
106
+ HasInstanceMethod = ->(method, object) { InstanceMethods.(object).any? { |m| m.name == method.to_sym } }.curry
107
+ InstanceMethod = ->(object, method) { ClassOf.(object).instance_method(method) }.curry
108
+ IsDescendentOf = ->(ancestor, object) { HasAncestor.(ancestor, object) and ClassOf.(ancestor) != ClassOf.(object) }.curry
109
+
110
+ AvailableCredentials = ->(client) { true if CredentialsFor.(InstanceOf.(client).base_url) }
111
+ AzureServiceName = ->(object) { ClassNameParts.(object).drop(1).first }
112
+ BindCredentials = ->(client) { Bind.(:credentials, CredentialsFor.(client.base_url), client) }
113
+ CredentialsFor = ->(domain) { CREDENTIALS[domain] }
114
+
115
+ LatestServiceVersion = ->(_service, versions) { versions.sort_by(&:name).last }
116
+
117
+ Constants = ->(namespace) {
118
+ [
119
+ namespace,
120
+ namespace
121
+ .constants
122
+ .map { |c| namespace.const_get c }
123
+ .select { |c| c.respond_to? :constants }
124
+ .map(&Constants)
125
+ ]
126
+ .flatten
127
+ .sort_by(&:to_s)
128
+ .uniq
129
+ }
130
+
131
+ Bold = ->(string) { Pastel.new.bold(string) }
132
+ Red = ->(string) { Pastel.new.red(string) }
133
+ Blue = ->(string) { Pastel.new.blue(string) }
134
+
135
+ UsageHelp = ->(method) {
136
+ [method.name,
137
+ method.parameters.map do |type, name|
138
+ case type
139
+ when :req then "<#{name}>"
140
+ when :opt then "[#{name}]"
141
+ when :keyreq then "<#{name}:>"
142
+ when :key then "[#{name}:]"
143
+ end
144
+ end]
145
+ .flatten
146
+ .join(' ')
147
+ }
148
+
149
+ UsageError = ->(method, exception) {
150
+ STDERR.puts(
151
+ Red.("#{Bold.(exception.class.name)}: #{exception.message}"))
152
+ STDERR.puts(
153
+ Blue.("#{Bold.('Usage')}: #{UsageHelp.(method)}"))
154
+ }
155
+
156
+ BindOperation = ->(namespace, operation) {
157
+ constant = FormatMethodName.(operation)
158
+ namespace.const_set(constant, operation)
159
+ namespace.define_singleton_method(operation.name) do |*args|
160
+ operation.call(*args)
161
+ rescue ArgumentError => error
162
+ UsageError.(operation, error)
163
+ end
164
+ }.curry
165
+
166
+ AddConstantCalls = ->(namespace, blacklist: []) {
167
+ namespace
168
+ .constants
169
+ .reject { |constant| namespace.singleton_methods.include?(constant) }
170
+ .reject { |constant| blacklist.include?(constant) }
171
+ .map { |constant| namespace.define_singleton_method(constant) do |*args|
172
+ namespace.const_get(constant).call(*args)
173
+ rescue ArgumentError => error
174
+ UsageError.(namespace.const_get(constant), error)
175
+ end}}
176
+
177
+ AddInteractiveCalls = ->(namespace, **options) {
178
+ AddConstantCalls.(namespace, **options)
179
+ return :call if namespace.singleton_methods.include?(:call)
180
+
181
+ namespace.define_singleton_method(:call) do |*args|
182
+ namespace.singleton_method(
183
+ SelectFromMenu.(namespace.name, namespace.singleton_methods.select { |method| method =~ /^[[:upper:]]/ })
184
+ ).call(*args)
185
+ rescue TTY::Reader::InputInterrupt
186
+ puts
187
+ namespace
188
+ end
189
+ }
190
+
191
+ BindInterface = ->(namespace, interface) {
192
+ context = namespace.const_set(FormatMethodName.(interface), Module.new)
193
+ AsyncableMethods.(interface.call).map(&BindOperation.(context))
194
+ AddInteractiveCalls.(context)
195
+ }.curry
196
+
197
+ BindService = ->(namespace, service) {
198
+ context = namespace.const_set(AzureServiceName.(service), Module.new)
199
+ models = context.const_set('Models', ServiceModels.(service))
200
+ context.define_singleton_method(:Models) { |*args| models.const_get(SelectFromMenu.("#{context.name}::Models", models.constants)).new(*args) }
201
+ MethodsReturningSiblings.(service).map(&BindInterface.(context))
202
+ AddInteractiveCalls.(context, blacklist: [:Models])
203
+ }.curry
204
+
205
+ ServiceModels = ->(service) { Kernel.const_get(ParentClass.(service) + '::Models') }
206
+
207
+ BindSubscriptionID = Bind.(:subscription_id, SUBSCRIPTION_ID)
208
+ BindTenantID = Bind.(:tenant_id, TENANT_ID)
209
+
210
+ ########################
211
+ # Deployment Functions #
212
+ ########################
213
+
214
+ FindResourceGroup = ->(name) {
215
+ AzureAPI::Resources::ResourceGroups
216
+ .list
217
+ .find { |resource_group| resource_group.id == "/subscriptions/#{SUBSCRIPTION_ID}/resourceGroups/#{name}" }}
218
+
219
+ CreateResourceGroup = ->(name) {
220
+ AzureAPI::Resources::ResourceGroups.create_or_update(
221
+ name,
222
+ AzureAPI::Resources::Models::ResourceGroup.new.tap do |resource_group|
223
+ resource_group.location = LOCATION
224
+ end)}
225
+
226
+ FindApplication = ->(display_name) {
227
+ AzureAPI::GraphRbac::Applications
228
+ .list
229
+ .find { |application| application.display_name == display_name }}
230
+
231
+ CreateApplication = ->(display_name) {
232
+ AzureAPI::GraphRbac::Applications.create(
233
+ AzureAPI::GraphRbac::Models::ApplicationCreateParameters.new.tap do |application|
234
+ application.available_to_other_tenants = false
235
+ application.display_name = display_name
236
+ application.identifier_uris = ["http://#{display_name}"]
237
+ end)}
238
+
239
+ FindServicePrincipal = ->(application) {
240
+ AzureAPI::GraphRbac::ServicePrincipals
241
+ .list
242
+ .find { |service_principal| service_principal.app_id == application.app_id }}
243
+
244
+ CreateServicePrincipal = ->(application) {
245
+ AzureAPI::GraphRbac::ServicePrincipals.create(
246
+ AzureAPI::GraphRbac::Models::ServicePrincipalCreateParameters.new.tap do |service_principal|
247
+ service_principal.account_enabled = true
248
+ service_principal.app_id = application.app_id
249
+ end)}
250
+
251
+ FindRoleDefinition = ->(role_name) {
252
+ AzureAPI::Authorization::RoleDefinitions
253
+ .list("/subscriptions/#{SUBSCRIPTION_ID}")
254
+ .find { |role_definition| role_definition.role_name == role_name }}
255
+
256
+ FindRoleAssignment = ->(name) {
257
+ AzureAPI::Authorization::RoleAssignments
258
+ .list
259
+ .find { |role_assignment| role_assignment.name == name }}
260
+
261
+ CreateRoleAssignment = ->(role_definition, service_principal, resource_group) {
262
+ AzureAPI::Authorization::RoleAssignments.create(
263
+ resource_group.id,
264
+ Chamber.env.uuid,
265
+ AzureAPI::Authorization::Models::RoleAssignmentCreateParameters.new.tap do |role_assignment|
266
+ role_assignment.role_definition_id = role_definition.id
267
+ role_assignment.principal_id = service_principal.object_id
268
+ end)}
269
+
270
+ UpdatePassword = ->(service_principal, password) {
271
+ AzureAPI::GraphRbac::ServicePrincipals.update_password_credentials(
272
+ service_principal.object_id,
273
+ AzureAPI::GraphRbac::Models::PasswordCredentialsUpdateParameters.new.tap { |update|
274
+ update.value = [AzureAPI::GraphRbac::Models::PasswordCredential.new.tap { |credential|
275
+ credential.value = password
276
+ credential.end_date = Time.parse(
277
+ Chamber.env.credential_end_date).to_datetime}]})}
278
+
279
+ FindContainerService = ->(resource_group) {
280
+ AzureAPI::ContainerService::ContainerServices
281
+ .list_by_resource_group(resource_group.name)
282
+ .find { |container_service| container_service.name == Chamber.env.identifier }}
283
+
284
+ CreateContainerService = ->(service_principal, resource_group) {
285
+ AzureAPI::ContainerService::ContainerServices.create_or_update(
286
+ resource_group.name,
287
+ Chamber.env.identifier,
288
+ AzureAPI::ContainerService::Models::ContainerService.new.tap { |container_service|
289
+ container_service.agent_pool_profiles = [
290
+ AzureAPI::ContainerService::Models::ContainerServiceAgentPoolProfile.new.tap { |agent_pool_profile|
291
+ agent_pool_profile.count = (
292
+ Chamber.env.agent_count)
293
+ agent_pool_profile.dns_prefix = (
294
+ [Chamber.env.dns_prefix, Chamber.env.agent_dns_suffix]
295
+ .join('-'))
296
+ agent_pool_profile.name = (
297
+ Chamber.env.identifier)
298
+ agent_pool_profile.vm_size = (
299
+ Chamber.env.vm_size)}]
300
+ container_service.linux_profile = (
301
+ AzureAPI::ContainerService::Models::ContainerServiceLinuxProfile.new.tap { |linux_profile|
302
+ linux_profile.admin_username = (
303
+ Chamber.env.admin_username)
304
+ linux_profile.ssh = (
305
+ AzureAPI::ContainerService::Models::ContainerServiceSshConfiguration.new.tap { |ssh|
306
+ ssh.public_keys = [
307
+ AzureAPI::ContainerService::Models::ContainerServiceSshPublicKey.new.tap { |public_key|
308
+ public_key.key_data = (
309
+ SSHKey.new(Chamber.env.ssh_private_key).ssh_public_key)}]})})
310
+ container_service.location = Chamber.env.location
311
+ container_service.master_profile = (
312
+ AzureAPI::ContainerService::Models::ContainerServiceMasterProfile.new.tap { |master_profile|
313
+ master_profile.dns_prefix = [Chamber.env.dns_prefix, Chamber.env.master_dns_suffix].join('-')})
314
+ container_service.orchestrator_profile = (
315
+ AzureAPI::ContainerService::Models::ContainerServiceOrchestratorProfile.new.tap { |orchestrator_profile|
316
+ orchestrator_profile.orchestrator_type = Chamber.env.orchestrator_type})
317
+ container_service.service_principal_profile = (
318
+ AzureAPI::ContainerService::Models::ContainerServiceServicePrincipalProfile.new.tap { |service_principal_profile|
319
+ service_principal_profile.client_id = service_principal.app_id
320
+ service_principal_profile.secret = Chamber.env.password})})}
321
+
322
+ FindVirtualMachines = ->(resource_group) {
323
+ AzureAPI::Compute::VirtualMachines
324
+ .list(resource_group.name)}
325
+
326
+ FindVirtualMachine = ->(virtual_machine_name, resource_group) {
327
+ AzureAPI::Compute::VirtualMachines
328
+ .list(resource_group.name)
329
+ .find { |virtual_machine| virtual_machine.name == virtual_machine_name }}
330
+
331
+ KubernetesAgents = ->(resource_group) {
332
+ FindVirtualMachines
333
+ .(resource_group)
334
+ .select { |vm| vm.tags['orchestrator'] =~ /^Kubernetes:/ }
335
+ .select { |vm| vm.tags['poolName'] == 'agent' }}
336
+
337
+ # FIXME: Bad mojo of there's more than one cluster.
338
+ KubernetesMaster = ->(resource_group) {
339
+ FindVirtualMachines
340
+ .(resource_group)
341
+ .select { |vm| vm.tags['orchestrator'] =~ /^Kubernetes:/ }
342
+ .find { |vm| vm.tags['poolName'] == 'master' }}
343
+
344
+ RunShellScripts = ->(scripts, resource_group, virtual_machine) {
345
+ AzureAPI::Compute::VirtualMachines.run_command(
346
+ resource_group.name,
347
+ virtual_machine.name,
348
+ AzureAPI::Compute::Models::RunCommandInput.new.tap { |input|
349
+ input.command_id = 'RunShellScript'
350
+ input.script = scripts
351
+ })}.curry
352
+
353
+ RunShellScript = ->(script, *rest) { RunShellScripts.([script], *rest) }
354
+ RestartVirtualMachine = ->(resource_group, virtual_machine) { AzureAPI::Compute::VirtualMachines.restart(resource_group.name, virtual_machine.name) }.curry
355
+
356
+ FindPublicIPv4 = ->(resource_group) {
357
+ AzureAPI::Network::PublicIpaddresses
358
+ .list(resource_group.name)
359
+ .find { |public_ip_address| public_ip_address.name == Chamber.env.identifier }}
360
+
361
+ CreatePublicIPv4 = ->(resource_group) {
362
+ AzureAPI::Network::PublicIpaddresses.create_or_update(
363
+ resource_group.name,
364
+ Chamber.env.identifier,
365
+ AzureAPI::Network::Models::PublicIPAddress.new.tap { |public_ip_address|
366
+ public_ip_address.location = Chamber.env.location
367
+ public_ip_address.public_ipaddress_version = 'IPv4'
368
+ public_ip_address.public_ipallocation_method = 'Static'
369
+ })}
370
+
371
+ KubernetesMasterSecurityGroup = ->(resource_group) {
372
+ name = [*KubernetesMaster.(resource_group).name.split('-').first(3), 'nsg'].join('-')
373
+ AzureAPI::Network::NetworkSecurityGroups
374
+ .list(resource_group.name)
375
+ .find { |network_security_group| network_security_group.name == name }}
376
+
377
+ FindSecurityRule = ->(network_security_group) {
378
+ network_security_group
379
+ .security_rules
380
+ .find { |security_rule| security_rule.name == Chamber.env.identifier }
381
+ }
382
+
383
+ CreateSecurityRule = ->(network_security_group, resource_group) {
384
+ AzureAPI::Network::SecurityRules.create_or_update(
385
+ resource_group.name,
386
+ network_security_group.name,
387
+ Chamber.env.identifier,
388
+ AzureAPI::Network::Models::SecurityRule.new.tap { |security_rule|
389
+ security_rule.access = 'Allow'
390
+ security_rule.destination_address_prefix = '*'
391
+ security_rule.destination_port_ranges = [80,443,4443,2222,2793]
392
+ security_rule.direction = AzureAPI::Network::Models::SecurityRuleDirection::Inbound
393
+ security_rule.priority = network_security_group.security_rules.map(&:priority).max.next
394
+ security_rule.protocol = 'Tcp'
395
+ security_rule.source_address_prefix = '*'
396
+ security_rule.source_port_range = '*'})}
397
+
398
+ UpdateVirtualMachine = ->(virtual_machine, resource_group) {
399
+ AzureAPI::Compute::VirtualMachines.create_or_update(
400
+ resource_group.name,
401
+ virtual_machine.name,
402
+ virtual_machine)}
403
+
404
+ ApplyTag = ->(key, value, resource_group, virtual_machine) {
405
+ UpdateVirtualMachine.(virtual_machine.tap { virtual_machine.tags[key] = value }, resource_group)}
406
+
407
+ EnableSwapAccounting = ->(resource_group, virtual_machine) {
408
+ return if virtual_machine.tags['cloudstrap.swap_accounting'] == 'enabled'
409
+ RunShellScripts.([ENABLE_SWAP_ACCOUNTING, UPDATE_GRUB], resource_group, virtual_machine)
410
+ ApplyTag.('cloudstrap.swap_accounting', 'enabled', resource_group, virtual_machine)}.curry
411
+
412
+ RebootOnce = ->(resource_group, virtual_machine) {
413
+ return if virtual_machine.tags['cloudstrap.reboot'] == 'finished'
414
+ ApplyTag.('cloudstrap.reboot', 'started', resource_group, virtual_machine)
415
+ RestartVirtualMachine.(resource_group, virtual_machine)
416
+ ApplyTag.('cloudstrap.reboot', 'finished', resource_group, vritual_machine)}.curry
417
+
418
+ FindNetworkInterface = ->(resource_group) {
419
+ virtual_machine = KubernetesAgents.(resource_group).sort_by(&:name).first
420
+ AzureAPI::Network::NetworkInterfaces
421
+ .list(resource_group.name)
422
+ .find { |network_interface| network_interface.virtual_machine.id.end_with?(virtual_machine.name)}}
423
+
424
+ AssociatePublicIP = ->(resource_group, network_interface, public_ip_address) {
425
+ AzureAPI::Network::NetworkInterfaces.create_or_update(
426
+ resource_group.name,
427
+ network_interface.name,
428
+ network_interface.tap {
429
+ network_interface.ip_configurations[0].public_ipaddress = public_ip_address})}
430
+
431
+ ################
432
+ # Main Program #
433
+ ################
434
+
435
+ WhileSpinning.('Constructing Library') do
436
+ AzureAPI = Module.new
437
+ Constants
438
+ .(Azure)
439
+ .select(&IsDescendentOf.(MsRestAzure::AzureServiceClient))
440
+ .select(&AvailableCredentials)
441
+ .group_by(&AzureServiceName)
442
+ .map(&LatestServiceVersion)
443
+ .map(&InstanceOf)
444
+ .map(&ApplyIf.(Bindable.(:credentials), BindCredentials))
445
+ .map(&ApplyIf.(Bindable.(:tenant_id), BindTenantID))
446
+ .map(&ApplyIf.(Bindable.(:subscription_id), BindSubscriptionID))
447
+ .each(&BindService.(AzureAPI))
448
+
449
+ AddInteractiveCalls.(AzureAPI)
450
+ end
451
+
452
+ role_definition = WhileSpinning.('Find Role Definition') { FindRoleDefinition.(Chamber.env.role_definition) }
453
+ resource_group = WhileSpinning.('Find/Create Resource Group') { FindResourceGroup.(Chamber.env.identifier) || CreateResourceGroup.(Chamber.env.identifier) }
454
+ application = WhileSpinning.('Find/Create Application') { FindApplication.(Chamber.env.identifier) || CreateApplication.(Chamber.env.identifier) }
455
+ service_principal = WhileSpinning.('Find/Create Service Principal') { FindServicePrincipal.(application) || CreateServicePrincipal.(application) }
456
+
457
+ WhileSpinning.('Update Password for Service Principal') { UpdatePassword.(service_principal, Chamber.env.password) }
458
+
459
+ role_assignment = WhileSpinning.('Find/Create Role Assignment') { FindRoleAssignment.(Chamber.env.uuid) || CreateRoleAssignment.(role_definition, service_principal, resource_group) }
460
+ public_ip_address = WhileSpinning.('Find/Create Public IP Address') { FindPublicIPv4.(resource_group) || CreatePublicIPv4.(resource_group) }
461
+ container_service = WhileSpinning.('Find/Create Container Service') { FindContainerService.(resource_group) || CreateContainerService.(service_principal, resource_group) }
462
+
463
+ network_security_group = WhileSpinning.('Find Network Security Group') { KubernetesMasterSecurityGroup.(resource_group) }
464
+ security_rule = WhileSpinning.('Find/Create Security Rule') { FindSecurityRule.(network_security_group) || CreateSecurityRule.(network_security_group, resource_group) }
465
+
466
+ network_interface = WhileSpinning.('Find Network Interface') { FindNetworkInterface.(resource_group) }
467
+ AssociatePublicIP.(resource_group, network_interface, public_ip_address)
468
+
469
+ WhileSpinning.('Configuring Swap Accounting') do
470
+ KubernetesAgents
471
+ .(resource_group)
472
+ .each(&EnableSwapAccounting.(resource_group))
473
+ .each(&RebootOnce.(resource_group))
474
+ end
475
+
476
+ def api(*args)
477
+ AzureAPI.call(*args)
478
+ end