cloudstrap-azure 0.4.6.pre

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