armrest 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +3 -0
  4. data/CHANGELOG.md +7 -0
  5. data/Gemfile +4 -0
  6. data/Guardfile +19 -0
  7. data/LICENSE.txt +201 -0
  8. data/README.md +77 -0
  9. data/Rakefile +14 -0
  10. data/armrest.gemspec +35 -0
  11. data/exe/armrest +14 -0
  12. data/lib/armrest/api/auth/base.rb +8 -0
  13. data/lib/armrest/api/auth/cli.rb +36 -0
  14. data/lib/armrest/api/auth/login.rb +18 -0
  15. data/lib/armrest/api/auth/metadata.rb +51 -0
  16. data/lib/armrest/api/base.rb +133 -0
  17. data/lib/armrest/api/handle_response.rb +23 -0
  18. data/lib/armrest/api/main.rb +34 -0
  19. data/lib/armrest/api/mods.rb +8 -0
  20. data/lib/armrest/api/response.rb +5 -0
  21. data/lib/armrest/api/settings.rb +37 -0
  22. data/lib/armrest/api/version.rb +8 -0
  23. data/lib/armrest/auth.rb +48 -0
  24. data/lib/armrest/autoloader.rb +25 -0
  25. data/lib/armrest/cli/auth.rb +16 -0
  26. data/lib/armrest/cli/base.rb +7 -0
  27. data/lib/armrest/cli/blob_container.rb +29 -0
  28. data/lib/armrest/cli/blob_service.rb +25 -0
  29. data/lib/armrest/cli/help/blob_service/set_properties.md +4 -0
  30. data/lib/armrest/cli/help/completion.md +20 -0
  31. data/lib/armrest/cli/help/completion_script.md +3 -0
  32. data/lib/armrest/cli/help/connect.md +3 -0
  33. data/lib/armrest/cli/help.rb +11 -0
  34. data/lib/armrest/cli/resource_group.rb +29 -0
  35. data/lib/armrest/cli/secret.rb +11 -0
  36. data/lib/armrest/cli/storage_account.rb +32 -0
  37. data/lib/armrest/cli.rb +49 -0
  38. data/lib/armrest/command.rb +89 -0
  39. data/lib/armrest/completer/script.rb +8 -0
  40. data/lib/armrest/completer/script.sh +10 -0
  41. data/lib/armrest/completer.rb +159 -0
  42. data/lib/armrest/logger.rb +6 -0
  43. data/lib/armrest/logging.rb +22 -0
  44. data/lib/armrest/services/base.rb +16 -0
  45. data/lib/armrest/services/blob_container.rb +29 -0
  46. data/lib/armrest/services/blob_service.rb +25 -0
  47. data/lib/armrest/services/key_vault/base.rb +54 -0
  48. data/lib/armrest/services/key_vault/secret.rb +37 -0
  49. data/lib/armrest/services/resource_group.rb +23 -0
  50. data/lib/armrest/services/storage_account.rb +36 -0
  51. data/lib/armrest/version.rb +3 -0
  52. data/lib/armrest.rb +15 -0
  53. data/spec/cli_spec.rb +8 -0
  54. data/spec/spec_helper.rb +29 -0
  55. metadata +281 -0
@@ -0,0 +1,159 @@
1
+ =begin
2
+ Code Explanation:
3
+
4
+ There are 3 types of things to auto-complete:
5
+
6
+ 1. command: the command itself
7
+ 2. parameters: command parameters.
8
+ 3. options: command options
9
+
10
+ Here's an example:
11
+
12
+ mycli hello name --from me
13
+
14
+ * command: hello
15
+ * parameters: name
16
+ * option: --from
17
+
18
+ When command parameters are done processing, the remaining completion words will be options. We can tell that the command params are completed based on the method arity.
19
+
20
+ ## Arity
21
+
22
+ For example, say you had a method for a CLI command with the following form:
23
+
24
+ ufo scale service count --cluster development
25
+
26
+ It's equivalent ruby method:
27
+
28
+ scale(service, count) = has an arity of 2
29
+
30
+ So typing:
31
+
32
+ ufo scale service count [TAB] # there are 3 parameters including the "scale" command according to Thor's CLI processing.
33
+
34
+ So the completion should only show options, something like this:
35
+
36
+ --noop --verbose --cluster
37
+
38
+ ## Splat Arguments
39
+
40
+ When the ruby method has a splat argument, it's arity is negative. Here are some example methods and their arities.
41
+
42
+ ship(service) = 1
43
+ scale(service, count) = 2
44
+ ships(*services) = -1
45
+ foo(example, *rest) = -2
46
+
47
+ Fortunately, negative and positive arity values are processed the same way. So we take simply take the absolute value of the arity and process it the same.
48
+
49
+ Here are some test cases, hit TAB after typing the command:
50
+
51
+ armrest completion
52
+ armrest completion hello
53
+ armrest completion hello name
54
+ armrest completion hello name --
55
+ armrest completion hello name --noop
56
+
57
+ armrest completion
58
+ armrest completion sub:goodbye
59
+ armrest completion sub:goodbye name
60
+
61
+ ## Subcommands and Thor::Group Registered Commands
62
+
63
+ Sometimes the commands are not simple thor commands but are subcommands or Thor::Group commands. A good specific example is the ufo tool.
64
+
65
+ * regular command: ufo ship
66
+ * subcommand: ufo docker
67
+ * Thor::Group command: ufo init
68
+
69
+ Auto-completion accounts for each of these type of commands.
70
+ =end
71
+ module Armrest
72
+ class Completer
73
+ def initialize(command_class, *params)
74
+ @params = params
75
+ @current_command = @params[0]
76
+ @command_class = command_class # CLI initiall
77
+ end
78
+
79
+ def run
80
+ if subcommand?(@current_command)
81
+ subcommand_class = @command_class.subcommand_classes[@current_command]
82
+ @params.shift # destructive
83
+ Completer.new(subcommand_class, *@params).run # recursively use subcommand
84
+ return
85
+ end
86
+
87
+ # full command has been found!
88
+ unless found?(@current_command)
89
+ puts all_commands
90
+ return
91
+ end
92
+
93
+ # will only get to here if command aws found (above)
94
+ arity = @command_class.instance_method(@current_command).arity.abs
95
+ if @params.size > arity or thor_group_command?
96
+ puts options_completion
97
+ else
98
+ puts params_completion
99
+ end
100
+ end
101
+
102
+ def subcommand?(command)
103
+ @command_class.subcommands.include?(command)
104
+ end
105
+
106
+ # hacky way to detect that command is a registered Thor::Group command
107
+ def thor_group_command?
108
+ command_params(raw=true) == [[:rest, :args]]
109
+ end
110
+
111
+ def found?(command)
112
+ public_methods = @command_class.public_instance_methods(false)
113
+ command && public_methods.include?(command.to_sym)
114
+ end
115
+
116
+ # all top-level commands
117
+ def all_commands
118
+ commands = @command_class.all_commands.reject do |k,v|
119
+ v.is_a?(Thor::HiddenCommand)
120
+ end
121
+ commands.keys
122
+ end
123
+
124
+ def command_params(raw=false)
125
+ params = @command_class.instance_method(@current_command).parameters
126
+ # Example:
127
+ # >> Sub.instance_method(:goodbye).parameters
128
+ # => [[:req, :name]]
129
+ # >>
130
+ raw ? params : params.map!(&:last)
131
+ end
132
+
133
+ def params_completion
134
+ offset = @params.size - 1
135
+ offset_params = command_params[offset..-1]
136
+ command_params[offset..-1].first
137
+ end
138
+
139
+ def options_completion
140
+ used = ARGV.select { |a| a.include?('--') } # so we can remove used options
141
+
142
+ method_options = @command_class.all_commands[@current_command].options.keys
143
+ class_options = @command_class.class_options.keys
144
+
145
+ all_options = method_options + class_options + ['help']
146
+
147
+ all_options.map! { |o| "--#{o.to_s.gsub('_','-')}" }
148
+ filtered_options = all_options - used
149
+ filtered_options.uniq
150
+ end
151
+
152
+ # Useful for debugging. Using puts messes up completion.
153
+ def log(msg)
154
+ File.open("/tmp/complete.log", "a") do |file|
155
+ file.puts(msg)
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,6 @@
1
+ require 'logger'
2
+
3
+ module Armrest
4
+ class Logger < ::Logger
5
+ end
6
+ end
@@ -0,0 +1,22 @@
1
+ module Armrest
2
+ module Logging
3
+ @@logger = nil
4
+ def logger
5
+ @@logger ||= default_logger
6
+ end
7
+
8
+ def logger=(v)
9
+ @@logger = v
10
+ end
11
+
12
+ # Note the Armrest logger on debug is pretty verbose so think it may be better
13
+ # to not assign the Armrest::Logging.logger = Terraspace.logger
14
+ def default_logger
15
+ logger = Armrest::Logger.new($stderr)
16
+ logger.level = ENV['ARMREST_LOG_LEVEL'] ? ENV['ARMREST_LOG_LEVEL'] : :info
17
+ logger
18
+ end
19
+
20
+ extend self
21
+ end
22
+ end
@@ -0,0 +1,16 @@
1
+ module Armrest::Services
2
+ class Base
3
+ include Armrest::Api::Mods
4
+ extend Memoist
5
+
6
+ def initialize(options={})
7
+ @options = options
8
+ end
9
+
10
+ private
11
+ def api
12
+ Armrest::Api::Main.new
13
+ end
14
+ memoize :api
15
+ end
16
+ end
@@ -0,0 +1,29 @@
1
+ module Armrest::Services
2
+ class BlobContainer < Base
3
+ def initialize(options={})
4
+ super
5
+ @storage_account = options[:storage_account]
6
+ end
7
+
8
+ # https://docs.microsoft.com/en-us/rest/api/storagerp/blob-containers/get
9
+ # GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{accountName}/blobServices/default/containers/{containerName}?api-version=2021-04-01
10
+ def get(attrs={})
11
+ name = attrs[:name]
12
+ path = "subscriptions/#{subscription_id}/resourceGroups/#{group}/providers/Microsoft.Storage/storageAccounts/#{@storage_account}/blobServices/default/containers/#{name}"
13
+ api.get(path)
14
+ end
15
+
16
+ def exist?(attrs={})
17
+ resp = get(attrs)
18
+ resp.code =~ /^20/
19
+ end
20
+
21
+ # https://docs.microsoft.com/en-us/rest/api/storagerp/blob-containers/create
22
+ # PUT https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{accountName}/blobServices/default/containers/{containerName}?api-version=2021-04-01
23
+ def create(attrs={})
24
+ name = attrs.delete(:name)
25
+ path = "subscriptions/#{subscription_id}/resourceGroups/#{group}/providers/Microsoft.Storage/storageAccounts/#{@storage_account}/blobServices/default/containers/#{name}"
26
+ api.put(path)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,25 @@
1
+ module Armrest::Services
2
+ class BlobService < Base
3
+ def initialize(options={})
4
+ super
5
+ @storage_account = options[:storage_account]
6
+ end
7
+
8
+ # https://docs.microsoft.com/en-us/rest/api/storagerp/blob-services/get-service-properties
9
+ # GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{accountName}/blobServices/default?api-version=2021-04-01
10
+ def get_properties
11
+ path = "subscriptions/#{subscription_id}/resourceGroups/#{group}/providers/Microsoft.Storage/storageAccounts/#{@storage_account}/blobServices/default"
12
+ resp = api.get(path)
13
+ load_json(resp)
14
+ end
15
+
16
+ # https://docs.microsoft.com/en-us/rest/api/storagerp/blob-services/set-service-properties
17
+ # PUT https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{accountName}/blobServices/default?api-version=2021-04-01
18
+ def set_properties(props)
19
+ props = props.to_h.deep_symbolize_keys
20
+ data = { properties: props }
21
+ path = "subscriptions/#{subscription_id}/resourceGroups/#{group}/providers/Microsoft.Storage/storageAccounts/#{@storage_account}/blobServices/default"
22
+ api.put(path, data)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,54 @@
1
+ module Armrest::Services::KeyVault
2
+ class Base < Armrest::Services::Base
3
+ class Error < StandardError; end
4
+ class VaultNotFoundError < Error; end
5
+ class VaultNotConfiguredError < Error; end
6
+
7
+ extend Memoist
8
+ cattr_accessor :vault
9
+
10
+ private
11
+ def api
12
+ check_vault_configured!
13
+ vault_subdomain = @vault.downcase
14
+ endpoint = "https://#{vault_subdomain}.vault.azure.net"
15
+ logger.debug "Azure vault endpoint #{endpoint}"
16
+ Armrest::Api::Main.new(
17
+ api_version: "7.1",
18
+ endpoint: endpoint,
19
+ resource: "https://vault.azure.net",
20
+ )
21
+ end
22
+ memoize :api
23
+
24
+ def check_vault_configured!
25
+ return if @vault
26
+ logger.error "ERROR: Vault has not been configured.".color(:red)
27
+ logger.error <<~EOL
28
+ Please configure the Azure KeyVault you want to use. Examples:
29
+
30
+ 1. env var
31
+
32
+ ARMREST_VAULT=demo-vault
33
+
34
+ 2. class var
35
+
36
+ Armrest::KeyVault::Secret.vault = "demo-vault"
37
+ EOL
38
+ raise VaultNotConfiguredError.new
39
+ end
40
+
41
+ # Secret error handling: 1. network 2. json parse 3. missing secret
42
+ #
43
+ # Azure API responses with decent error message when
44
+ # 403 Forbidden - KeyVault Access Policy needs to be set up
45
+ # 404 Not Found - Secret name is incorrect
46
+ #
47
+ def standard_error_message(resp)
48
+ data = JSON.load(resp.body)
49
+ data['error']['message']
50
+ rescue JSON::ParserError
51
+ resp.body
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,37 @@
1
+ require "json"
2
+ require "net/http"
3
+ require "uri"
4
+
5
+ # az keyvault secret show --vault-name $VAULT --name "demo-dev-pass" | jq -r '.value'
6
+ module Armrest::Services::KeyVault
7
+ class Secret < Base
8
+ # Using Azure REST API since the old gem doesnt support secrets https://github.com/Azure/azure-sdk-for-ruby
9
+ # https://docs.microsoft.com/en-us/rest/api/keyvault/get-secret/get-secret
10
+ def show(options={})
11
+ name = options[:name]
12
+ @vault = options[:vault] || @options[:vault] || ENV['ARMREST_VAULT'] || self.class.vault
13
+ version = "/#{version}" if @options[:version]
14
+ begin
15
+ resp = api.get("/secrets/#{name}#{version}")
16
+ rescue SocketError => e
17
+ if e.message.include?("vault.azure.net")
18
+ message = "WARN: Vault not found. Vault: #{@vault}"
19
+ logger.info message.color(:yellow)
20
+ return message
21
+ else
22
+ raise
23
+ end
24
+ end
25
+
26
+ case resp.code.to_s
27
+ when /^2/
28
+ data = JSON.load(resp.body)
29
+ data['value']
30
+ else
31
+ message = standard_error_message(resp)
32
+ logger.info "WARN: #{message}".color(:yellow)
33
+ return message
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,23 @@
1
+ module Armrest::Services
2
+ class ResourceGroup < Base
3
+ # https://docs.microsoft.com/en-us/rest/api/resources/resource-groups/check-existence
4
+ # HEAD https://management.azure.com/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}?api-version=2021-04-01
5
+ def check_existence(attrs={})
6
+ name = attrs[:name]
7
+ path = "subscriptions/#{subscription_id}/resourcegroups/#{name}"
8
+ resp = api.head(path)
9
+ resp.code == "204" # means it exists
10
+ end
11
+
12
+ # https://docs.microsoft.com/en-us/rest/api/resources/resource-groups/create-or-update
13
+ # PUT https://management.azure.com/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}?api-version=2021-04-01
14
+ def create_or_update(attrs={})
15
+ name = attrs.delete(:name)
16
+ # https://docs.microsoft.com/en-us/rest/api/resources/resource-groups/create-or-update#request-body
17
+ attrs[:location] ||= location
18
+ attrs[:tags] = attrs[:tags] if attrs[:tags]
19
+ path = "subscriptions/#{subscription_id}/resourcegroups/#{name}"
20
+ api.put(path, attrs)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,36 @@
1
+ module Armrest::Services
2
+ class StorageAccount < Base
3
+ # https://docs.microsoft.com/en-us/rest/api/storagerp/storage-accounts/check-name-availability
4
+ # POST https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Storage/checkNameAvailability?api-version=2021-04-01
5
+ def check_name_availability(attrs={})
6
+ name = attrs[:name]
7
+ path = "subscriptions/#{subscription_id}/providers/Microsoft.Storage/checkNameAvailability"
8
+ attrs = {
9
+ name: name,
10
+ type: "Microsoft.Storage/storageAccounts",
11
+ }
12
+ res = api.post(path, attrs)
13
+ load_json(res)
14
+ end
15
+
16
+ # https://docs.microsoft.com/en-us/rest/api/storagerp/storage-accounts/create
17
+ # PUT https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{accountName}?api-version=2021-04-01
18
+ # Note there's an update api also but PUT to create will also update. So just implementing create.
19
+ def create(attrs={})
20
+ name = attrs.delete(:name)
21
+ # https://docs.microsoft.com/en-us/rest/api/storagerp/storage-accounts/create#request-body
22
+ attrs[:kind] ||= "StorageV2"
23
+ attrs[:location] ||= location
24
+ attrs[:sku] ||= {
25
+ name: "Standard_RAGRS", # default according to az storage account create --help
26
+ tier: "Standard",
27
+ }
28
+ attrs[:properties] ||= {
29
+ allow_blob_public_access: false
30
+ }
31
+ path = "subscriptions/#{subscription_id}/resourceGroups/#{group}/providers/Microsoft.Storage/storageAccounts/#{name}"
32
+ puts "StorageAccount#create attrs #{attrs}".color(:purple)
33
+ resp = api.put(path, attrs)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,3 @@
1
+ module Armrest
2
+ VERSION = "0.1.0"
3
+ end
data/lib/armrest.rb ADDED
@@ -0,0 +1,15 @@
1
+ $stdout.sync = true unless ENV["ARMREST_STDOUT_SYNC"] == "0"
2
+
3
+ $:.unshift(File.expand_path("../", __FILE__))
4
+
5
+ require "armrest/autoloader"
6
+ Armrest::Autoloader.setup
7
+
8
+ require "active_support"
9
+ require "active_support/core_ext/string"
10
+ require "memoist"
11
+ require "rainbow/ext/string"
12
+
13
+ module Armrest
14
+ class Error < StandardError; end
15
+ end
data/spec/cli_spec.rb ADDED
@@ -0,0 +1,8 @@
1
+ describe Armrest::CLI do
2
+ describe "armrest" do
3
+ it "version" do
4
+ out = execute("exe/armrest version")
5
+ expect(out).to be_a(String)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,29 @@
1
+ ENV["ARMREST_TEST"] = "1"
2
+
3
+ # CodeClimate test coverage: https://docs.codeclimate.com/docs/configuring-test-coverage
4
+ # require 'simplecov'
5
+ # SimpleCov.start
6
+
7
+ require "pp"
8
+ require "byebug"
9
+ root = File.expand_path("../", File.dirname(__FILE__))
10
+ require "#{root}/lib/armrest"
11
+
12
+ module Helper
13
+ def execute(cmd)
14
+ puts "Running: #{cmd}" if show_command?
15
+ out = `#{cmd}`
16
+ puts out if show_command?
17
+ out
18
+ end
19
+
20
+ # Added SHOW_COMMAND because DEBUG is also used by other libraries like
21
+ # bundler and it shows its internal debugging logging also.
22
+ def show_command?
23
+ ENV['DEBUG'] || ENV['SHOW_COMMAND']
24
+ end
25
+ end
26
+
27
+ RSpec.configure do |c|
28
+ c.include Helper
29
+ end