armrest 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|