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,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,8 @@
1
+ module Armrest::Api
2
+ module Mods
3
+ include Armrest::Logging
4
+ include HandleResponse
5
+ include Settings
6
+ include VERSION
7
+ end
8
+ end
@@ -0,0 +1,5 @@
1
+ module Armrest::Api
2
+ class Response < Hashie::Mash
3
+ disable_warnings
4
+ end
5
+ 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
@@ -0,0 +1,8 @@
1
+ module Armrest::Api
2
+ module VERSION
3
+ # vault uses 7.1
4
+ def api_version
5
+ @options[:api_version] || "2021-04-01"
6
+ end
7
+ end
8
+ end
@@ -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,7 @@
1
+ class Armrest::CLI
2
+ class Base
3
+ def initialize(options={})
4
+ @options = options
5
+ end
6
+ end
7
+ 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,4 @@
1
+ ## Examples
2
+
3
+ export STORAGE_ACCOUNT=mystorageaccount
4
+ armrest blob_service set_properties --storage-account $STORAGE_ACCOUNT --delete-retention-policy days:7 enabled:true --container-delete-retention-policy days:8 enabled:true
@@ -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,3 @@
1
+ To use, add the following to your `~/.bashrc` or `~/.profile`
2
+
3
+ eval $(armrest completion_script)
@@ -0,0 +1,3 @@
1
+ ## Examples
2
+
3
+ armrest connect
@@ -0,0 +1,11 @@
1
+ class Armrest::CLI
2
+ module Help
3
+ class << self
4
+ def text(namespaced_command)
5
+ path = namespaced_command.to_s.gsub(':','/')
6
+ path = File.expand_path("../help/#{path}.md", __FILE__)
7
+ IO.read(path) if File.exist?(path)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -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
@@ -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,8 @@
1
+ class Armrest::Completer
2
+ class Script
3
+ def self.generate
4
+ bash_script = File.expand_path("script.sh", File.dirname(__FILE__))
5
+ puts "source #{bash_script}"
6
+ end
7
+ end
8
+ 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