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.
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