armrest 0.1.0
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/.gitignore +18 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +4 -0
- data/Guardfile +19 -0
- data/LICENSE.txt +201 -0
- data/README.md +77 -0
- data/Rakefile +14 -0
- data/armrest.gemspec +35 -0
- data/exe/armrest +14 -0
- data/lib/armrest/api/auth/base.rb +8 -0
- data/lib/armrest/api/auth/cli.rb +36 -0
- data/lib/armrest/api/auth/login.rb +18 -0
- data/lib/armrest/api/auth/metadata.rb +51 -0
- data/lib/armrest/api/base.rb +133 -0
- data/lib/armrest/api/handle_response.rb +23 -0
- data/lib/armrest/api/main.rb +34 -0
- data/lib/armrest/api/mods.rb +8 -0
- data/lib/armrest/api/response.rb +5 -0
- data/lib/armrest/api/settings.rb +37 -0
- data/lib/armrest/api/version.rb +8 -0
- data/lib/armrest/auth.rb +48 -0
- data/lib/armrest/autoloader.rb +25 -0
- data/lib/armrest/cli/auth.rb +16 -0
- data/lib/armrest/cli/base.rb +7 -0
- data/lib/armrest/cli/blob_container.rb +29 -0
- data/lib/armrest/cli/blob_service.rb +25 -0
- data/lib/armrest/cli/help/blob_service/set_properties.md +4 -0
- data/lib/armrest/cli/help/completion.md +20 -0
- data/lib/armrest/cli/help/completion_script.md +3 -0
- data/lib/armrest/cli/help/connect.md +3 -0
- data/lib/armrest/cli/help.rb +11 -0
- data/lib/armrest/cli/resource_group.rb +29 -0
- data/lib/armrest/cli/secret.rb +11 -0
- data/lib/armrest/cli/storage_account.rb +32 -0
- data/lib/armrest/cli.rb +49 -0
- data/lib/armrest/command.rb +89 -0
- data/lib/armrest/completer/script.rb +8 -0
- data/lib/armrest/completer/script.sh +10 -0
- data/lib/armrest/completer.rb +159 -0
- data/lib/armrest/logger.rb +6 -0
- data/lib/armrest/logging.rb +22 -0
- data/lib/armrest/services/base.rb +16 -0
- data/lib/armrest/services/blob_container.rb +29 -0
- data/lib/armrest/services/blob_service.rb +25 -0
- data/lib/armrest/services/key_vault/base.rb +54 -0
- data/lib/armrest/services/key_vault/secret.rb +37 -0
- data/lib/armrest/services/resource_group.rb +23 -0
- data/lib/armrest/services/storage_account.rb +36 -0
- data/lib/armrest/version.rb +3 -0
- data/lib/armrest.rb +15 -0
- data/spec/cli_spec.rb +8 -0
- data/spec/spec_helper.rb +29 -0
- 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,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,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
|
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
data/spec/spec_helper.rb
ADDED
@@ -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
|