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,23 @@
|
|
1
|
+
require "hashie"
|
2
|
+
|
3
|
+
module Armrest::Api
|
4
|
+
module HandleResponse
|
5
|
+
def load_json(resp)
|
6
|
+
if ok?(resp.code)
|
7
|
+
data = JSON.load(resp.body).deep_transform_keys(&:underscore)
|
8
|
+
Response.new(data)
|
9
|
+
else
|
10
|
+
logger.info "Error: Non-successful http response status code: #{resp.code}"
|
11
|
+
logger.info "headers: #{resp.each_header.to_h.inspect}"
|
12
|
+
raise "Azure API called failed"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Note: 422 is Unprocessable Entity. This means an invalid data payload was sent.
|
17
|
+
# We want that to error and raise
|
18
|
+
def ok?(http_code)
|
19
|
+
http_code =~ /^20/ ||
|
20
|
+
http_code =~ /^40/
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Armrest::Api
|
2
|
+
class Main < Base
|
3
|
+
def headers
|
4
|
+
{
|
5
|
+
"Authorization" => authorization,
|
6
|
+
"Content-Type" => "application/json",
|
7
|
+
}
|
8
|
+
end
|
9
|
+
|
10
|
+
# cache key is the resource/audience. IE:
|
11
|
+
# {
|
12
|
+
# "https://management.azure.com" => {...},
|
13
|
+
# "https://vault.azure.com" => {...},
|
14
|
+
# }
|
15
|
+
@@creds_cache = {}
|
16
|
+
def authorization
|
17
|
+
creds = @@creds_cache[resource]
|
18
|
+
if creds && Time.now < Time.at(creds['expires_on'].to_i)
|
19
|
+
return bearer_token(creds)
|
20
|
+
end
|
21
|
+
|
22
|
+
provider = Armrest::Auth.new(@options).provider
|
23
|
+
if provider
|
24
|
+
creds = provider.creds
|
25
|
+
@@creds_cache[resource] = creds
|
26
|
+
bearer_token(creds)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def bearer_token(creds)
|
31
|
+
"#{creds['token_type']} #{creds['access_token']}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "azure_info"
|
2
|
+
|
3
|
+
module Armrest::Api
|
4
|
+
module Settings
|
5
|
+
def client_id
|
6
|
+
@options[:client_id] || ENV['ARM_CLIENT_ID'] || ENV['AZURE_CLIENT_ID']
|
7
|
+
end
|
8
|
+
|
9
|
+
def client_secret
|
10
|
+
@options[:client_secret] || ENV['ARM_CLIENT_SECRET'] || ENV['AZURE_CLIENT_SECRET']
|
11
|
+
end
|
12
|
+
|
13
|
+
def tenant_id
|
14
|
+
@options[:tenant_id] || ENV['ARM_TENANT_ID'] || ENV['AZURE_TENANT_ID'] || AzureInfo.tenant_id
|
15
|
+
end
|
16
|
+
|
17
|
+
def resource
|
18
|
+
@options[:resource] || "https://management.azure.com"
|
19
|
+
end
|
20
|
+
|
21
|
+
def subscription_id
|
22
|
+
@options[:subscription_id] || ENV['ARM_SUBSCRIPTION_ID'] || ENV['AZURE_SUBSCRIPTION_ID'] || AzureInfo.subscription_id
|
23
|
+
end
|
24
|
+
|
25
|
+
def location
|
26
|
+
@options[:location] || ENV['ARM_LOCATION'] || ENV['AZURE_LOCATION'] || AzureInfo.location
|
27
|
+
end
|
28
|
+
|
29
|
+
def group
|
30
|
+
@options[:group] || ENV['ARM_GROUP'] || AzureInfo.group
|
31
|
+
end
|
32
|
+
|
33
|
+
def endpoint
|
34
|
+
@options[:endpoint] || "https://management.azure.com"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/armrest/auth.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
module Armrest
|
2
|
+
class Auth
|
3
|
+
include Armrest::Logging
|
4
|
+
|
5
|
+
def initialize(options={})
|
6
|
+
@options = options
|
7
|
+
end
|
8
|
+
|
9
|
+
def provider
|
10
|
+
providers.each do |meth|
|
11
|
+
provider = send(meth)
|
12
|
+
if provider
|
13
|
+
logger.debug "Resolved auth provider: #{provider}"
|
14
|
+
return provider
|
15
|
+
end
|
16
|
+
end
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
def providers
|
22
|
+
if @options[:type]
|
23
|
+
["#{@options[:type]}_credentials"]
|
24
|
+
else # full chain
|
25
|
+
[
|
26
|
+
:app_credentials,
|
27
|
+
:msi_credentials,
|
28
|
+
:cli_credentials,
|
29
|
+
]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def app_credentials
|
34
|
+
return unless ENV['ARM_CLIENT_ID'] || ENV['AZURE_CLIENT_ID']
|
35
|
+
Armrest::Api::Auth::Login.new(@options)
|
36
|
+
end
|
37
|
+
|
38
|
+
def msi_credentials
|
39
|
+
api = Armrest::Api::Auth::Metadata.new(@options)
|
40
|
+
api if api.available?
|
41
|
+
end
|
42
|
+
|
43
|
+
def cli_credentials
|
44
|
+
return unless File.exist?("#{ENV['HOME']}/.azure/accessTokens.json")
|
45
|
+
Armrest::Api::Auth::CLI.new(@options)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require "zeitwerk"
|
2
|
+
|
3
|
+
module Armrest
|
4
|
+
class Autoloader
|
5
|
+
class Inflector < Zeitwerk::Inflector
|
6
|
+
def camelize(basename, _abspath)
|
7
|
+
map = self.class.camelize_map
|
8
|
+
map[basename.to_sym] || super
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.camelize_map
|
12
|
+
{ cli: "CLI", msi: "MSI", version: "VERSION" }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class << self
|
17
|
+
def setup
|
18
|
+
loader = Zeitwerk::Loader.new
|
19
|
+
loader.inflector = Inflector.new
|
20
|
+
loader.push_dir(File.dirname(__dir__)) # lib
|
21
|
+
loader.setup
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class Armrest::CLI
|
2
|
+
class Auth
|
3
|
+
def initialize(options={})
|
4
|
+
@options = options
|
5
|
+
end
|
6
|
+
|
7
|
+
def run
|
8
|
+
provider = Armrest::Auth.new(@options).provider
|
9
|
+
if provider
|
10
|
+
puts JSON.pretty_generate(provider.creds)
|
11
|
+
else
|
12
|
+
puts "Unable to authenticate"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class Armrest::CLI
|
2
|
+
class BlobContainer < Armrest::Command
|
3
|
+
class_option :storage_account, desc: "Storage account name"
|
4
|
+
|
5
|
+
desc "get", "Check existence of blob container"
|
6
|
+
long_desc Help.text("blob_container/get")
|
7
|
+
def get(name)
|
8
|
+
exist = Armrest::Services::BlobContainer.new(options).get(name: name)
|
9
|
+
if exist
|
10
|
+
puts "Blob container exists: #{name}"
|
11
|
+
else
|
12
|
+
puts "Blob container does not exist: #{name}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
desc "create", "Create or update Blob container"
|
17
|
+
long_desc Help.text("blob_container/create")
|
18
|
+
def create(name)
|
19
|
+
resp = Armrest::Services::BlobContainer.new(options).create(name: name)
|
20
|
+
if resp.code == "201"
|
21
|
+
puts "Blob container created: #{name}"
|
22
|
+
else
|
23
|
+
puts "Blob container unable to create: #{name}"
|
24
|
+
puts "resp:"
|
25
|
+
pp resp
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Armrest::CLI
|
2
|
+
class BlobService < Armrest::Command
|
3
|
+
class_option :storage_account, desc: "Storage account name"
|
4
|
+
|
5
|
+
desc "get_properties", "Check existence of blob container"
|
6
|
+
long_desc Help.text("blob_container/get_properties")
|
7
|
+
def get_properties
|
8
|
+
result = Armrest::Services::BlobService.new(options).get_properties
|
9
|
+
puts JSON.pretty_generate(result)
|
10
|
+
end
|
11
|
+
|
12
|
+
desc "create", "Create or update Blob container"
|
13
|
+
long_desc Help.text("blob_container/create")
|
14
|
+
option :delete_retention_policy, type: :hash, desc: "days:7 enabled:true"
|
15
|
+
option :container_delete_retention_policy, type: :hash, desc: "days:7 enabled:true"
|
16
|
+
option :is_versioning_enabled, type: :boolean, desc: "Enables versioning"
|
17
|
+
def set_properties
|
18
|
+
props = options.dup
|
19
|
+
props.delete(:storage_account)
|
20
|
+
resp = Armrest::Services::BlobService.new(options).set_properties(props)
|
21
|
+
puts "resp:"
|
22
|
+
pp resp
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
## Examples
|
2
|
+
|
3
|
+
armrest completion
|
4
|
+
|
5
|
+
Prints words for TAB auto-completion.
|
6
|
+
|
7
|
+
armrest completion
|
8
|
+
armrest completion hello
|
9
|
+
armrest completion hello name
|
10
|
+
|
11
|
+
To enable, TAB auto-completion add the following to your profile:
|
12
|
+
|
13
|
+
eval $(armrest completion_script)
|
14
|
+
|
15
|
+
Auto-completion example usage:
|
16
|
+
|
17
|
+
armrest [TAB]
|
18
|
+
armrest hello [TAB]
|
19
|
+
armrest hello name [TAB]
|
20
|
+
armrest hello name --[TAB]
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class Armrest::CLI
|
2
|
+
class ResourceGroup < Armrest::Command
|
3
|
+
desc "check_existence", "Check existence of resource group"
|
4
|
+
long_desc Help.text("resource_group/check_existence")
|
5
|
+
def check_existence(name)
|
6
|
+
exist = Armrest::Services::ResourceGroup.new(options).check_existence(name: name)
|
7
|
+
if exist
|
8
|
+
puts "Resource group exists: #{name}"
|
9
|
+
else
|
10
|
+
puts "Resource group does not exist: #{name}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
desc "create_or_update", "Create or update resource group"
|
15
|
+
long_desc Help.text("resource_group/create_or_update")
|
16
|
+
option :location, description: "location"
|
17
|
+
option :tags, type: :hash, description: "--tags=name:bob age:8"
|
18
|
+
def create_or_update(name)
|
19
|
+
resp = Armrest::Services::ResourceGroup.new(options).create_or_update(options.merge(name: name))
|
20
|
+
if resp.code == "201"
|
21
|
+
puts "Resource group created: #{name}"
|
22
|
+
else
|
23
|
+
puts "Resource group unable to create: #{name}"
|
24
|
+
puts "resp:"
|
25
|
+
pp resp
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class Armrest::CLI
|
2
|
+
class Secret < Armrest::Command
|
3
|
+
desc "show", "Runs show operations."
|
4
|
+
long_desc Help.text("secret/show")
|
5
|
+
option :version, aliases: %w[v], description: "secret version"
|
6
|
+
option :vault, description: "vault name"
|
7
|
+
def show(name)
|
8
|
+
puts Armrest::Services::KeyVault::Secret.new(options).show(name: name)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class Armrest::CLI
|
2
|
+
class StorageAccount < Armrest::Command
|
3
|
+
desc "check_name_availability", "Check availability of storage account"
|
4
|
+
long_desc Help.text("storage_account/check_name_availability")
|
5
|
+
def check_name_availability(name)
|
6
|
+
result = Armrest::Services::StorageAccount.new(options).check_name_availability(name: name)
|
7
|
+
if result.name_available
|
8
|
+
puts "Storage account is available: #{name}"
|
9
|
+
else
|
10
|
+
puts "Storage account is not available: #{name}"
|
11
|
+
pp result
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
desc "create", "Create storage account"
|
16
|
+
long_desc Help.text("storage_account/create")
|
17
|
+
option :location, description: "location"
|
18
|
+
option :tags, type: :hash, description: "--tags=name:bob age:8"
|
19
|
+
def create(name)
|
20
|
+
resp = Armrest::Services::StorageAccount.new(options).create(options.merge(name: name))
|
21
|
+
puts "resp:"
|
22
|
+
pp resp
|
23
|
+
if resp.code == "202"
|
24
|
+
puts "Storage account created: #{name}"
|
25
|
+
elsif resp.code =~ /^20/
|
26
|
+
puts "Storage account updated: #{name}"
|
27
|
+
else
|
28
|
+
puts "Storage account unable to create: #{name}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/armrest/cli.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
module Armrest
|
2
|
+
class CLI < Command
|
3
|
+
class_option :verbose, type: :boolean
|
4
|
+
class_option :noop, type: :boolean
|
5
|
+
|
6
|
+
desc "blob_container SUBCOMMAND", "blob_container subcommands"
|
7
|
+
long_desc Help.text(:blob_container)
|
8
|
+
subcommand "blob_container", BlobContainer
|
9
|
+
|
10
|
+
desc "blob_service SUBCOMMAND", "blob_service subcommands"
|
11
|
+
long_desc Help.text(:blob_service)
|
12
|
+
subcommand "blob_service", BlobService
|
13
|
+
|
14
|
+
desc "resource_group SUBCOMMAND", "resource_group subcommands"
|
15
|
+
long_desc Help.text(:resource_group)
|
16
|
+
subcommand "resource_group", ResourceGroup
|
17
|
+
|
18
|
+
desc "secret SUBCOMMAND", "secret subcommands"
|
19
|
+
long_desc Help.text(:secret)
|
20
|
+
subcommand "secret", Secret
|
21
|
+
|
22
|
+
desc "storage_account SUBCOMMAND", "storage_account subcommands"
|
23
|
+
long_desc Help.text(:storage_account)
|
24
|
+
subcommand "storage_account", StorageAccount
|
25
|
+
|
26
|
+
desc "auth [TYPE]", "Auth to Azure API. When type is not provided the full credentials chain is checked"
|
27
|
+
long_desc Help.text(:auth)
|
28
|
+
def auth(type=nil)
|
29
|
+
Auth.new(options.merge(type: type)).run
|
30
|
+
end
|
31
|
+
|
32
|
+
desc "completion *PARAMS", "Prints words for auto-completion."
|
33
|
+
long_desc Help.text(:completion)
|
34
|
+
def completion(*params)
|
35
|
+
Completer.new(CLI, *params).run
|
36
|
+
end
|
37
|
+
|
38
|
+
desc "completion_script", "Generates a script that can be eval to setup auto-completion."
|
39
|
+
long_desc Help.text(:completion_script)
|
40
|
+
def completion_script
|
41
|
+
Completer::Script.generate
|
42
|
+
end
|
43
|
+
|
44
|
+
desc "version", "prints version"
|
45
|
+
def version
|
46
|
+
puts VERSION
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require "thor"
|
2
|
+
|
3
|
+
# Override thor's long_desc identation behavior
|
4
|
+
# https://github.com/erikhuda/thor/issues/398
|
5
|
+
class Thor
|
6
|
+
module Shell
|
7
|
+
class Basic
|
8
|
+
def print_wrapped(message, options = {})
|
9
|
+
message = "\n#{message}" unless message[0] == "\n"
|
10
|
+
stdout.puts message
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module Armrest
|
17
|
+
class Command < Thor
|
18
|
+
class << self
|
19
|
+
def dispatch(m, args, options, config)
|
20
|
+
# Allow calling for help via:
|
21
|
+
# armrest command help
|
22
|
+
# armrest command -h
|
23
|
+
# armrest command --help
|
24
|
+
# armrest command -D
|
25
|
+
#
|
26
|
+
# as well thor's normal way:
|
27
|
+
#
|
28
|
+
# armrest help command
|
29
|
+
help_flags = Thor::HELP_MAPPINGS + ["help"]
|
30
|
+
if args.length > 1 && !(args & help_flags).empty?
|
31
|
+
args -= help_flags
|
32
|
+
args.insert(-2, "help")
|
33
|
+
end
|
34
|
+
|
35
|
+
# armrest version
|
36
|
+
# armrest --version
|
37
|
+
# armrest -v
|
38
|
+
version_flags = ["--version", "-v"]
|
39
|
+
if args.length == 1 && !(args & version_flags).empty?
|
40
|
+
args = ["version"]
|
41
|
+
end
|
42
|
+
|
43
|
+
super
|
44
|
+
end
|
45
|
+
|
46
|
+
# Override command_help to include the description at the top of the
|
47
|
+
# long_description.
|
48
|
+
def command_help(shell, command_name)
|
49
|
+
meth = normalize_command_name(command_name)
|
50
|
+
command = all_commands[meth]
|
51
|
+
alter_command_description(command)
|
52
|
+
super
|
53
|
+
end
|
54
|
+
|
55
|
+
def alter_command_description(command)
|
56
|
+
return unless command
|
57
|
+
|
58
|
+
# Add description to beginning of long_description
|
59
|
+
long_desc = if command.long_description
|
60
|
+
"#{command.description}\n\n#{command.long_description}"
|
61
|
+
else
|
62
|
+
command.description
|
63
|
+
end
|
64
|
+
|
65
|
+
# add reference url to end of the long_description
|
66
|
+
unless website.empty?
|
67
|
+
full_command = [command.ancestor_name, command.name].compact.join('-')
|
68
|
+
url = "#{website}/reference/armrest-#{full_command}"
|
69
|
+
long_desc += "\n\nHelp also available at: #{url}"
|
70
|
+
end
|
71
|
+
|
72
|
+
command.long_description = long_desc
|
73
|
+
end
|
74
|
+
private :alter_command_description
|
75
|
+
|
76
|
+
# meant to be overriden
|
77
|
+
def website
|
78
|
+
""
|
79
|
+
end
|
80
|
+
|
81
|
+
# https://github.com/erikhuda/thor/issues/244
|
82
|
+
# Deprecation warning: Thor exit with status 0 on errors. To keep this behavior, you must define `exit_on_failure?` in `Lono::CLI`
|
83
|
+
# You can silence deprecations warning by setting the environment variable THOR_SILENCE_DEPRECATION.
|
84
|
+
def exit_on_failure?
|
85
|
+
true
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
_armrest() {
|
2
|
+
COMPREPLY=()
|
3
|
+
local word="${COMP_WORDS[COMP_CWORD]}"
|
4
|
+
local words=("${COMP_WORDS[@]}")
|
5
|
+
unset words[0]
|
6
|
+
local completion=$(armrest completion ${words[@]})
|
7
|
+
COMPREPLY=( $(compgen -W "$completion" -- "$word") )
|
8
|
+
}
|
9
|
+
|
10
|
+
complete -F _armrest armrest
|