kontena-plugin-cloud 1.0.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +9 -0
  5. data/LICENSE.txt +191 -0
  6. data/README.md +25 -0
  7. data/Rakefile +2 -0
  8. data/kontena-plugin-cloud.gemspec +27 -0
  9. data/lib/kontena/cli/master_code_exchanger.rb +80 -0
  10. data/lib/kontena/cli/models/platform.rb +33 -0
  11. data/lib/kontena/plugin/cloud.rb +21 -0
  12. data/lib/kontena/plugin/cloud/organization/list_command.rb +22 -0
  13. data/lib/kontena/plugin/cloud/organization/show_command.rb +16 -0
  14. data/lib/kontena/plugin/cloud/organization/user/add_command.rb +37 -0
  15. data/lib/kontena/plugin/cloud/organization/user/list_command.rb +22 -0
  16. data/lib/kontena/plugin/cloud/organization/user/remove_command.rb +33 -0
  17. data/lib/kontena/plugin/cloud/organization/user_command.rb +14 -0
  18. data/lib/kontena/plugin/cloud/organization_command.rb +14 -0
  19. data/lib/kontena/plugin/cloud/region/list_command.rb +20 -0
  20. data/lib/kontena/plugin/cloud/region_command.rb +10 -0
  21. data/lib/kontena/plugin/cloud_command.rb +8 -0
  22. data/lib/kontena/plugin/platform/audit_log_command.rb +33 -0
  23. data/lib/kontena/plugin/platform/common.rb +76 -0
  24. data/lib/kontena/plugin/platform/create_command.rb +85 -0
  25. data/lib/kontena/plugin/platform/health_command.rb +67 -0
  26. data/lib/kontena/plugin/platform/import_grid_command.rb +47 -0
  27. data/lib/kontena/plugin/platform/list_command.rb +46 -0
  28. data/lib/kontena/plugin/platform/remove_command.rb +29 -0
  29. data/lib/kontena/plugin/platform/show_command.rb +25 -0
  30. data/lib/kontena/plugin/platform/use_command.rb +41 -0
  31. data/lib/kontena/plugin/platform_command.rb +14 -0
  32. data/lib/kontena_cli_plugin.rb +51 -0
  33. metadata +117 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 59e599cd1a6b5a74ff9854efbffc34bc2faba4fc
4
+ data.tar.gz: 114cbd20de9d08e733c9790e113473dfabbda920
5
+ SHA512:
6
+ metadata.gz: 6c59729a771e4d39e6a6a84a7e1db59803c7779e14dd907f4fa82e77413b971227c10a1f2dce3dec558c9b1a1940a986a26e16077618cbba652c2b6f0c0d1f6d
7
+ data.tar.gz: 41c156dad9f34cfd32990ccbde6655fc6c1708113350be06c7c4981859ad50a84c3aacb92f17c11344c86f3e5e0fa5799f09ba177887d16b42b35cf22117c64c
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in kontena-plugin-cloud.gemspec
4
+ gemspec
5
+
6
+ group :development do
7
+ gem 'rspec'
8
+ gem 'kontena-plugin-digitalocean'
9
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,191 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ Copyright 2015 Kontena, Inc.
180
+
181
+ Licensed under the Apache License, Version 2.0 (the "License");
182
+ you may not use this file except in compliance with the License.
183
+ You may obtain a copy of the License at
184
+
185
+ http://www.apache.org/licenses/LICENSE-2.0
186
+
187
+ Unless required by applicable law or agreed to in writing, software
188
+ distributed under the License is distributed on an "AS IS" BASIS,
189
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
190
+ See the License for the specific language governing permissions and
191
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # Kontena Cloud Plugin
2
+
3
+ This plugin adds [Kontena Cloud](https://kontena.io/cloud) management commands to Kontena CLI.
4
+
5
+ ## System Requirements
6
+
7
+ - [Kontena CLI](https://kontena.io/)
8
+
9
+ ## Installation
10
+
11
+ ```
12
+ $ kontena plugin install cloud
13
+ ```
14
+
15
+ ## Contributing
16
+
17
+ 1. Fork it ( https://github.com/kontena/kontena-plugin-cloud )
18
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
19
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
20
+ 4. Push to the branch (`git push origin my-new-feature`)
21
+ 5. Create a new Pull Request
22
+
23
+ ## License
24
+
25
+ Kontena is licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE.txt) for full license text.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'kontena/plugin/cloud'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "kontena-plugin-cloud"
8
+ spec.version = Kontena::Plugin::Cloud::VERSION
9
+ spec.authors = ["Kontena, Inc."]
10
+ spec.email = ["info@kontena.io"]
11
+
12
+ spec.summary = "Kontena Cloud management for Kontena CLI"
13
+ spec.description = "Kontena Cloud management for Kontena CLI"
14
+ spec.homepage = "https://kontena.io"
15
+ spec.license = "Apache-2.0"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_runtime_dependency 'kontena-cli', '>= 1.3.0'
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.14"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ end
@@ -0,0 +1,80 @@
1
+ require 'excon'
2
+
3
+ class Kontena::Cli::MasterCodeExchanger
4
+ class Error < StandardError
5
+ attr_accessor :code
6
+
7
+ def initialize(code, message)
8
+ self.code = code
9
+ super("#{code} #{message}")
10
+ end
11
+ end
12
+
13
+ REDIRECT_URI = 'http://localhost/'.freeze
14
+
15
+ # @param [String] url
16
+ def initialize(url, ssl_verify_peer: false)
17
+ @api_client = Excon.new(url, { ssl_verify_peer: ssl_verify_peer })
18
+ end
19
+
20
+ def exchange_code(authorization_code)
21
+ master_auth_response = request_auth
22
+ redirect_url = URI.parse(master_auth_response[:headers]['Location'])
23
+ state = CGI.parse(redirect_url.query)['state']
24
+ master_code_response = request_code(authorization_code, state.first)
25
+ redirect_url = URI.parse(master_code_response[:headers]['Location'])
26
+ CGI.parse(redirect_url.query)['code'].first
27
+ end
28
+
29
+ # @return [Hash]
30
+ def request_auth
31
+ params = {
32
+ redirect_uri: REDIRECT_URI
33
+ }
34
+ response = execute("/authenticate?#{URI.encode_www_form(params)}")
35
+ unless response[:status] == 302
36
+ raise Error.new(response[:status], response[:data])
37
+ end
38
+ response
39
+ end
40
+
41
+ # @param [String] code
42
+ # @param [String] state
43
+ # @return [Hash]
44
+ def request_code(code, state)
45
+ params = {
46
+ code: code,
47
+ state: state
48
+ }
49
+ response = execute("/cb?#{URI.encode_www_form(params)}")
50
+ unless response[:status] == 302
51
+ raise Error.new(response[:status], response[:data])
52
+ end
53
+ response
54
+ end
55
+
56
+ private
57
+
58
+ def execute(path, method = :get, payload = nil)
59
+ headers = {}
60
+ headers['Content-Type'] = 'application/json' unless payload.nil?
61
+
62
+ body = payload.to_json if payload
63
+
64
+ response = @api_client.request(method: method, body: body, path: path, headers: headers)
65
+ content_type = response.headers.dig('Content-Type') || ''
66
+ content_length = response.headers.dig('Content-Length').to_i
67
+
68
+ body = if content_type.include?('json') && content_length > 0
69
+ JSON.parse(response.body)
70
+ else
71
+ response.body
72
+ end
73
+
74
+ {
75
+ status: response.status,
76
+ headers: response.headers,
77
+ data: body
78
+ }
79
+ end
80
+ end
@@ -0,0 +1,33 @@
1
+ module Kontena::Cli::Models
2
+ class Platform
3
+
4
+ def initialize(api_data)
5
+ @api_data = api_data
6
+ end
7
+
8
+ def id
9
+ @api_data['id']
10
+ end
11
+
12
+ def region
13
+ @api_data.dig('relationships', 'datacenter', 'data', 'id')
14
+ end
15
+
16
+ def online?
17
+ state.to_s == 'online'.freeze
18
+ end
19
+
20
+ def organization
21
+ @api_data.dig('relationships', 'organization', 'data', 'id')
22
+ end
23
+
24
+ def method_missing(method, *args, &block)
25
+ key = method.to_s.gsub('_', '-')
26
+ if @api_data['attributes'].has_key?(key)
27
+ @api_data['attributes'][key]
28
+ else
29
+ raise ArgumentError.new("Method `#{m}` doesn't exist.")
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,21 @@
1
+ module Kontena
2
+ module Plugin
3
+ module Cloud
4
+ VERSION = "1.0.0.pre1"
5
+
6
+ module Organization
7
+ module User
8
+ end
9
+ end
10
+
11
+ module Region
12
+ end
13
+ end
14
+
15
+ module Platform
16
+ end
17
+
18
+ module Grid
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ class Kontena::Plugin::Cloud::Organization::ListCommand < Kontena::Command
2
+ include Kontena::Cli::Common
3
+ include Kontena::Cli::TableGenerator::Helper
4
+
5
+ requires_current_account_token
6
+
7
+ def execute
8
+ organizations = cloud_client.get('/organizations')['data']
9
+ print_table(organizations) do |row|
10
+ row.merge!(row['attributes'])
11
+ row['name'] = row['name']
12
+ row['role'] = row['owner'] ? pastel.cyan('owner') : 'member'
13
+ end
14
+ end
15
+
16
+ def fields
17
+ {
18
+ name: 'name',
19
+ 'your role': 'role'
20
+ }
21
+ end
22
+ end
@@ -0,0 +1,16 @@
1
+ class Kontena::Plugin::Cloud::Organization::ShowCommand < Kontena::Command
2
+ include Kontena::Cli::Common
3
+
4
+ parameter "NAME", "Organization name"
5
+
6
+ requires_current_account_token
7
+
8
+ def execute
9
+ org = cloud_client.get("/organizations/#{name}").dig('data', 'attributes')
10
+ puts "#{org['name']}:"
11
+ puts " email: #{org['email']}"
12
+ puts " your role: #{org['owner'] ? 'owner' : 'member'}"
13
+ puts " url: #{org['url'] || '-'}"
14
+ puts " location: #{org['location'] || '-'}"
15
+ end
16
+ end
@@ -0,0 +1,37 @@
1
+ class Kontena::Plugin::Cloud::Organization::User::AddCommand < Kontena::Command
2
+ include Kontena::Cli::Common
3
+
4
+ requires_current_account_token
5
+
6
+ parameter "NAME", "Organization name"
7
+ parameter "USERNAME ...", "List of usernames to add"
8
+
9
+ option ["--role", "-r"], "ROLE", "Role to grant for users", default: "member"
10
+
11
+ def execute
12
+ members = []
13
+ spinner "Resolving organization #{pastel.cyan(name)} current members" do
14
+ members = cloud_client.get("/organizations/#{name}/members")['data']
15
+ members = members.map { |m|
16
+ {
17
+ type: 'users',
18
+ id: m.dig('attributes', 'username'),
19
+ meta: {
20
+ role: m.dig('attributes', 'role')
21
+ }
22
+ }
23
+ }
24
+ end
25
+ username_list.each do |u|
26
+ members << {
27
+ type: 'users',
28
+ id: u,
29
+ meta: { role: role }
30
+ }
31
+ end
32
+ spinner "Adding #{pastel.cyan(username_list.join(', '))} to organization #{pastel.cyan(name)} with role #{pastel.cyan(role)}" do
33
+ data = {data: members}
34
+ cloud_client.put("/organizations/#{name}/relationships/members", data)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,22 @@
1
+ class Kontena::Plugin::Cloud::Organization::User::ListCommand < Kontena::Command
2
+ include Kontena::Cli::Common
3
+ include Kontena::Cli::TableGenerator::Helper
4
+
5
+ requires_current_account_token
6
+
7
+ parameter "NAME", "Organization name"
8
+
9
+ def execute
10
+ members = cloud_client.get("/organizations/#{name}/members")['data']
11
+ print_table(members) do |row|
12
+ row.merge!(row['attributes'])
13
+ end
14
+ end
15
+
16
+ def fields
17
+ {
18
+ username: 'username',
19
+ role: 'role'
20
+ }
21
+ end
22
+ end
@@ -0,0 +1,33 @@
1
+ class Kontena::Plugin::Cloud::Organization::User::RemoveCommand < Kontena::Command
2
+ include Kontena::Cli::Common
3
+
4
+ requires_current_account_token
5
+
6
+ parameter "NAME", "Organization name"
7
+ parameter "USERNAME ...", "List of usernames to remove"
8
+
9
+ option "--force", :flag, "Force remove", default: false, attribute_name: :forced
10
+
11
+ def execute
12
+ confirm_command(username_list.join(',')) unless forced?
13
+
14
+ members = []
15
+ spinner "Resolving organization #{pastel.cyan(name)} current members" do
16
+ members = cloud_client.get("/organizations/#{name}/members")['data']
17
+ members = members.map { |m|
18
+ {
19
+ type: 'users',
20
+ id: m.dig('attributes', 'username'),
21
+ meta: {
22
+ role: m.dig('attributes', 'role')
23
+ }
24
+ }
25
+ }
26
+ end
27
+ members.delete_if { |m| username_list.include?(m[:id]) }
28
+ spinner "Removing #{pastel.cyan(username_list.join(', '))} from organization #{pastel.cyan(name)}" do
29
+ data = {data: members}
30
+ cloud_client.put("/organizations/#{name}/relationships/members", data)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,14 @@
1
+ require_relative 'user/list_command'
2
+ require_relative 'user/add_command'
3
+ require_relative 'user/remove_command'
4
+
5
+ class Kontena::Plugin::Cloud::Organization::UserCommand < Kontena::Command
6
+ include Kontena::Plugin::Cloud::Organization
7
+
8
+ subcommand ['list', 'ls'], 'List organizations', User::ListCommand
9
+ subcommand 'add', 'Add users to organization', User::AddCommand
10
+ subcommand ['remove', 'rm'], 'Remove users from organization', User::RemoveCommand
11
+
12
+ def execute
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ require_relative 'organization/list_command'
2
+ require_relative 'organization/show_command'
3
+ require_relative 'organization/user_command'
4
+
5
+ class Kontena::Plugin::Cloud::OrganizationCommand < Kontena::Command
6
+ include Kontena::Plugin::Cloud
7
+
8
+ subcommand ['list', 'ls'], 'List organizations', Organization::ListCommand
9
+ subcommand 'show', 'Show organization details', Organization::ShowCommand
10
+
11
+ subcommand 'user', 'User management commands', Organization::UserCommand
12
+ def execute
13
+ end
14
+ end
@@ -0,0 +1,20 @@
1
+ class Kontena::Plugin::Cloud::Region::ListCommand < Kontena::Command
2
+ include Kontena::Cli::Common
3
+ include Kontena::Cli::TableGenerator::Helper
4
+
5
+ requires_current_account_token
6
+
7
+ def execute
8
+ datacenters = cloud_client.get('/datacenters')['data']
9
+ print_table(datacenters) do |row|
10
+ row.merge!(row['attributes'])
11
+ end
12
+ end
13
+
14
+ def fields
15
+ {
16
+ id: 'id',
17
+ name: 'name'
18
+ }
19
+ end
20
+ end
@@ -0,0 +1,10 @@
1
+ require_relative 'region/list_command'
2
+
3
+ class Kontena::Plugin::Cloud::RegionCommand < Kontena::Command
4
+ include Kontena::Plugin::Cloud
5
+
6
+ subcommand ['list', 'ls'], 'List regions', Region::ListCommand
7
+
8
+ def execute
9
+ end
10
+ end
@@ -0,0 +1,8 @@
1
+ class Kontena::Cli::CloudCommand < Kontena::Command
2
+
3
+ subcommand 'organization', 'Organization specific commands', load_subcommand('kontena/plugin/cloud/organization_command')
4
+ subcommand 'region', 'Region specific commands', load_subcommand('kontena/plugin/cloud/region_command')
5
+
6
+ def execute
7
+ end
8
+ end
@@ -0,0 +1,33 @@
1
+ require 'kontena/cli/grids/audit_log_command'
2
+ require_relative 'common'
3
+
4
+ class Kontena::Plugin::Platform::AuditLogCommand < Kontena::Command
5
+ include Kontena::Cli::Common
6
+ include Kontena::Plugin::Platform::Common
7
+ include Kontena::Cli::TableGenerator::Helper
8
+
9
+ parameter "NAME", "Platform name"
10
+
11
+ option ["-l", "--lines"], "LINES", "Number of lines"
12
+
13
+ def execute
14
+ require_platform(name)
15
+
16
+ audit_logs = client.get("grids/#{current_grid}/audit_log", {limit: lines})['logs']
17
+ print_table(audit_logs) do |a|
18
+ a['user'] = a['user_identity']['email']
19
+ a['resource'] = a['resource_name'] ? "#{a['resource_type']}:#{a['resource_name']}" : a['resource_type']
20
+ end
21
+ end
22
+
23
+ def fields
24
+ {
25
+ 'time' => 'time',
26
+ 'resource' => 'resource',
27
+ 'event' => 'event_name',
28
+ 'user' => 'user',
29
+ 'source ip' => 'source_ip',
30
+ 'user agent' => 'user_agent'
31
+ }
32
+ end
33
+ end
@@ -0,0 +1,76 @@
1
+ require_relative '../../cli/master_code_exchanger'
2
+ require_relative '../../cli/models/platform'
3
+
4
+ module Kontena::Plugin::Platform::Common
5
+ def fetch_platforms
6
+ all = []
7
+ organizations = cloud_client.get('/organizations')['data']
8
+ organizations.each do |org|
9
+ all = all + fetch_platforms_for_org(org['id'])
10
+ end
11
+ all
12
+ end
13
+
14
+ # @param [String] org_id
15
+ # @return [Array<Kontena::Cli::Models::Platform>]
16
+ def fetch_platforms_for_org(org_id)
17
+ platforms = cloud_client.get("/organizations/#{org_id}/platforms")['data']
18
+ platforms.map do |p|
19
+ Kontena::Cli::Models::Platform.new(p)
20
+ end
21
+ end
22
+
23
+ # @return [String, NilClass]
24
+ def current_organization
25
+ @current_organization || ENV['KONTENA_ORGANIZATION'] || (current_account && current_account.username)
26
+ end
27
+
28
+ def current_platform
29
+ self.current_grid
30
+ end
31
+
32
+ # @param [String] name
33
+ def require_platform(name)
34
+ unless name.include?('/')
35
+ name = "#{current_organization}/#{name}"
36
+ end
37
+ org, platform = name.split('/')
38
+
39
+ raise ArgumentError, "Organization missing" unless org
40
+
41
+ @current_organization = org
42
+
43
+ unless platform_config_exists?(name)
44
+ p = find_platform_by_name(platform, org)
45
+ exit_with_error("Platform not found") unless p
46
+
47
+ login_to_platform(name, p.url)
48
+ end
49
+ self.current_master = name
50
+ self.current_grid = platform
51
+ end
52
+
53
+ def platform_config_exists?(name)
54
+ !self.config.find_server_by(name: name).nil?
55
+ end
56
+
57
+ def find_platform_by_name(name, org)
58
+ data = cloud_client.get("/organizations/#{org}/platforms/#{name}")['data']
59
+ if data
60
+ Kontena::Cli::Models::Platform.new(data)
61
+ end
62
+ end
63
+
64
+ def login_to_platform(name, url)
65
+ organization, platform = name.split('/')
66
+ platform = cloud_client.get("/organizations/#{organization}/platforms/#{platform}")['data']
67
+ authorization = cloud_client.post("/organizations/#{organization}/masters/#{platform.dig('attributes', 'master-id')}/authorize", {})
68
+ exchanger = Kontena::Cli::MasterCodeExchanger.new(platform.dig('attributes', 'url'))
69
+ code = exchanger.exchange_code(authorization['code'])
70
+ cmd = [
71
+ 'master', 'login', '--silent', '--no-login-info', '--skip-grid-auto-select',
72
+ '--name', name, '--code', code, url
73
+ ]
74
+ Kontena.run!(cmd)
75
+ end
76
+ end
@@ -0,0 +1,85 @@
1
+ require_relative 'common'
2
+ class Kontena::Plugin::Platform::CreateCommand < Kontena::Command
3
+ include Kontena::Cli::Common
4
+ include Kontena::Plugin::Platform::Common
5
+
6
+ requires_current_account_token
7
+
8
+ parameter "[NAME]", "Platform name"
9
+
10
+ option ['--organization'], 'ORG', 'Organization name', environment_variable: 'KONTENA_ORGANIZATION'
11
+ option ['--region'], 'region', 'Region (us-east, eu-west)'
12
+ option ['--initial-size', '-i'], 'SIZE', 'Initial size (number of nodes) for platform'
13
+ option '--[no-]use', :flag, 'Switch to use created platform', default: true
14
+
15
+ def execute
16
+ confirm("This will create managed platform to Kontena Cloud, proceed?")
17
+
18
+ self.name = prompt.ask("Name:") unless self.name
19
+ self.organization = prompt_organization unless self.organization
20
+ self.region = prompt_region unless self.region
21
+ self.initial_size = prompt_initial_size unless self.initial_size
22
+
23
+ platform = nil
24
+ spinner "Creating platform #{pastel.cyan(name)} to region #{pastel.cyan(region)}" do
25
+ platform = create_platform(name, organization, initial_size, region)['data']
26
+ end
27
+ spinner "Waiting for platform #{pastel.cyan(name)} to come online" do
28
+ while !platform.online? do
29
+ sleep 5
30
+ platform = find_platform_by_name(platform.id, organization)
31
+ end
32
+ end
33
+ use_platform(platform) if use?
34
+ end
35
+
36
+ # @param [Kontena::Cli::Models::Platform] platform
37
+ def use_platform(platform)
38
+ platform_name = "#{organization}/#{name}"
39
+ login_to_platform(platform_name, platform.dig('attributes', 'url'))
40
+ spinner "Switching to use platform #{pastel.cyan(platform_name)}" do
41
+ config.current_grid = name
42
+ config.write
43
+ end
44
+ end
45
+
46
+ def prompt_organization
47
+ organizations = cloud_client.get('/organizations')['data']
48
+ prompt.select("Choose organization:") do |menu|
49
+ menu.choice "#{config.current_account.username} (you)", config.current_account.username
50
+ organizations.each do |o|
51
+ menu.choice o.dig('attributes', 'name')
52
+ end
53
+ end
54
+ end
55
+
56
+ def prompt_region
57
+ datacenters = cloud_client.get('/datacenters')['data']
58
+ prompt.select("Choose region:") do |menu|
59
+ datacenters.each do |d|
60
+ menu.choice d.dig('attributes', 'name'), d['id']
61
+ end
62
+ end
63
+ end
64
+
65
+ def prompt_initial_size
66
+ prompt.select("Initial platform size (number of nodes):") do |menu|
67
+ menu.choice "1 (dev/test)", 1
68
+ menu.choice "3 (tolerates 1 initial node failure)", 3
69
+ menu.choice "5 (tolerates 2 initial node failures)", 5
70
+ end
71
+ end
72
+
73
+ def create_platform(name, organization, initial_size, region)
74
+ data = {
75
+ attributes: { "name": name, "initial-size": initial_size },
76
+ relationships: {
77
+ datacenter: {
78
+ "data": { "type": "datacenters", "id": region }
79
+ }
80
+ }
81
+ }
82
+ data = cloud_client.post("/organizations/#{organization}/platforms", { data: data })['data']
83
+ Kontena::Cli::Models::Platform.new(data)
84
+ end
85
+ end
@@ -0,0 +1,67 @@
1
+ require 'kontena/cli/grids/health_command'
2
+ require_relative 'common'
3
+
4
+ class Kontena::Plugin::Platform::HealthCommand < Kontena::Command
5
+ include Kontena::Cli::Common
6
+ include Kontena::Plugin::Platform::Common
7
+ include Kontena::Cli::Helpers::HealthHelper
8
+
9
+ parameter "NAME", "Platform name"
10
+
11
+ def execute
12
+ require_platform(name)
13
+
14
+ grid = client.get("grids/#{current_grid}")
15
+ grid_nodes = client.get("grids/#{current_grid}/nodes")
16
+
17
+ show_platform_health(grid, grid_nodes['nodes'])
18
+ end
19
+
20
+ # Validate grid/nodes configuration for grid operation
21
+ #
22
+ # @return [Boolean] false if unhealthy
23
+ def show_platform_health(grid, nodes)
24
+ initial_size = grid['initial_size']
25
+ minimum_size = grid['initial_size'] / 2 + 1 # a majority is required for etcd quorum
26
+
27
+ grid_health = grid_health(grid, nodes)
28
+ initial_nodes = nodes.select{|node| node['initial_member']}
29
+ online_nodes = initial_nodes.select{|node| node['connected']}
30
+
31
+ # configuration and status
32
+ if initial_nodes.length == 0
33
+ puts "#{health_icon :error} #{name} does not have any initial nodes and requires at least #{minimum_size} of #{initial_size} initial nodes for operation"
34
+ elsif online_nodes.empty?
35
+ puts "#{health_icon :error} #{name} does not have any initial nodes online and requires at least #{minimum_size} of #{initial_size} initial nodes for operation"
36
+ elsif initial_nodes.length < minimum_size
37
+ puts "#{health_icon :error} #{name} only has #{initial_nodes.length} initial nodes and requires at least #{minimum_size} of #{initial_size} initial nodes for operation"
38
+ elsif online_nodes.length < minimum_size
39
+ puts "#{health_icon :error} #{name} only has #{online_nodes.length} initial nodes online and requires at least #{minimum_size} of #{initial_size} initial nodes for operation"
40
+ elsif initial_nodes.length < initial_size
41
+ puts "#{health_icon :warning} #{name} only has #{initial_nodes.length} initial nodes of #{initial_size} required for high-availability"
42
+ elsif online_nodes.length < initial_size
43
+ puts "#{health_icon :warning} #{name} only has #{online_nodes.length} initial nodes online of #{initial_size} required for high-availability"
44
+ elsif initial_nodes.length == 2
45
+ puts "#{health_icon :warning} #{name} only has #{initial_nodes.length} initial nodes and is not high-availability"
46
+ elsif initial_nodes.length == 1
47
+ puts "#{health_icon :warning} #{name} only has #{initial_nodes.length} initial node and is not high-availability"
48
+ else
49
+ puts "#{health_icon :ok} #{name} has all #{online_nodes.length} of #{initial_size} initial nodes online"
50
+ end
51
+
52
+ nodes.each do |node|
53
+ node_health = node_health(node, grid_health)
54
+
55
+ if node['connected']
56
+
57
+ elsif node['initial_member']
58
+ puts "#{health_icon grid_health} Initial node #{node['name']} is offline"
59
+ else
60
+ puts "#{health_icon node_health} Grid node #{node['name']} is offline"
61
+ end
62
+ end
63
+
64
+ # operational if we have etcd quorum
65
+ online_nodes.length >= minimum_size
66
+ end
67
+ end
@@ -0,0 +1,47 @@
1
+ class Kontena::Plugin::Platform::ImportGridCommand < Kontena::Command
2
+ include Kontena::Cli::Common
3
+
4
+ banner "Migrate grid to Kontena Cloud platform"
5
+
6
+ parameter "MASTER", "Kontena Master name in local config"
7
+ parameter "GRID", "Grid name"
8
+ parameter "PLATFORM", "Platform name"
9
+
10
+ option "--organization", "ORG", "Organization name"
11
+
12
+ requires_current_account_token
13
+
14
+ def execute
15
+ self.organization = prompt_organization unless self.organization
16
+
17
+ master = config.find_server(self.master)
18
+ if master.nil?
19
+ exit_with_error "Could not resolve master by name '#{self.master}'." +
20
+ "\nFor a list of known masters please run: kontena master list"
21
+ else
22
+ config.current_master = master['name']
23
+ end
24
+ config.current_master.grid = self.grid
25
+
26
+ grid = client.get("/v1/grids/#{self.grid}")
27
+ attributes = {
28
+ 'name' => self.platform,
29
+ 'grid' => self.grid,
30
+ 'url' => current_master.url,
31
+ 'initial-size' => grid['initial_size']
32
+ }
33
+ spinner "Migrating #{self.master}/#{self.grid} (#{current_master.url}) to Kontena Cloud organization #{organization}" do
34
+ cloud_client.post("/organizations/#{organization}/platforms/migrate", { data: { attributes: attributes } })
35
+ end
36
+ end
37
+
38
+ def prompt_organization
39
+ organizations = cloud_client.get('/organizations')['data']
40
+ prompt.select("Choose organization:") do |menu|
41
+ menu.choice "#{config.current_account.username} (you)", config.current_account.username
42
+ organizations.each do |o|
43
+ menu.choice o.dig('attributes', 'name')
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,46 @@
1
+ require_relative 'common'
2
+
3
+ class Kontena::Plugin::Platform::ListCommand < Kontena::Command
4
+ include Kontena::Cli::Common
5
+ include Kontena::Cli::TableGenerator::Helper
6
+ include Kontena::Plugin::Platform::Common
7
+
8
+ requires_current_account_token
9
+
10
+ parameter "[ORG]", "Organization name", environment_variable: "KONTENA_ORGANIZATION"
11
+
12
+ def execute
13
+ platforms = cloud_client.get("/organizations/#{org}/platforms")['data']
14
+
15
+ print_table(platforms) do |p|
16
+ platform = Kontena::Cli::Models::Platform.new(p)
17
+ p['name'] = "#{state_icon(platform.state)} #{org}/#{platform.name}"
18
+ p['organization'] = platform.organization
19
+ p['url'] = platform.url
20
+ p['region'] = platform.region
21
+ end
22
+ end
23
+
24
+ def default_org
25
+ current_account.username
26
+ end
27
+
28
+ def fields
29
+ {
30
+ name: 'name',
31
+ organization: 'organization',
32
+ region: 'region'
33
+ }
34
+ end
35
+
36
+ def state_icon(health)
37
+ case health
38
+ when nil
39
+ " ".freeze
40
+ when 'online'.freeze
41
+ pastel.green('⊛'.freeze)
42
+ else
43
+ pastel.dark('⊝'.freeze)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,29 @@
1
+ require_relative 'common'
2
+
3
+ class Kontena::Plugin::Platform::RemoveCommand < Kontena::Command
4
+ include Kontena::Cli::Common
5
+ include Kontena::Plugin::Platform::Common
6
+
7
+ requires_current_account_token
8
+
9
+ parameter "NAME", "Platform name"
10
+ option "--force", :flag, "Force remove", default: false, attribute_name: :forced
11
+
12
+ def execute
13
+ require_platform(name)
14
+ platform = find_platform_by_name(name)
15
+
16
+ confirm_command(name) unless forced?
17
+
18
+ spinner "Removing platform #{pastel.cyan(name)}" do
19
+ cloud_client.delete("/organizations/#{current_organization}/platforms/#{platform.id}")
20
+ remove_from_config(name)
21
+ end
22
+ end
23
+
24
+ def remove_from_config(name)
25
+ config.current_server = nil
26
+ config.servers.delete_if {|s| s.name == name }
27
+ config.write
28
+ end
29
+ end
@@ -0,0 +1,25 @@
1
+ require_relative 'common'
2
+
3
+ class Kontena::Plugin::Platform::ShowCommand < Kontena::Command
4
+ include Kontena::Cli::Common
5
+ include Kontena::Plugin::Platform::Common
6
+
7
+ requires_current_account_token
8
+
9
+ parameter "NAME", "Platform name"
10
+
11
+ def execute
12
+ require_platform(name)
13
+
14
+ platform = find_platform_by_name(current_platform, current_organization)
15
+
16
+ puts "#{name}:"
17
+ puts " id: #{platform.id}"
18
+ puts " name: #{platform.name}"
19
+ puts " organization: #{current_organization}"
20
+ puts " state: #{platform.state}"
21
+ puts " region: #{platform.region || '-'}"
22
+ puts " initial_size: #{platform.initial_size}"
23
+ puts " master: #{platform.url}"
24
+ end
25
+ end
@@ -0,0 +1,41 @@
1
+ require_relative 'common'
2
+
3
+ class Kontena::Plugin::Platform::UseCommand < Kontena::Command
4
+ include Kontena::Cli::Common
5
+ include Kontena::Plugin::Platform::Common
6
+
7
+ requires_current_account_token
8
+
9
+ parameter "[NAME]", "Platform name"
10
+
11
+ def execute
12
+ if name
13
+ require_platform(name)
14
+ platform = find_platform_by_name(current_grid, current_organization)
15
+ else
16
+ platform = prompt_platform
17
+ end
18
+
19
+ platform_name = "#{current_organization}/#{platform.name}"
20
+ unless platform_config_exists?(platform_name)
21
+ spinner "Generating platform token" do
22
+ login_to_platform(platform_name, platform.url)
23
+ end
24
+ else
25
+ config.current_master = platform_name
26
+ config.current_master.grid = platform.grid_id
27
+ config.write
28
+ end
29
+
30
+ puts "Using platform: #{pastel.cyan(platform_name)}"
31
+ end
32
+
33
+ def prompt_platform
34
+ platforms = fetch_platforms_for_org(current_organization)
35
+ prompt.select("Choose platform") do |menu|
36
+ platforms.each do |p|
37
+ menu.choice p.name, p
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,14 @@
1
+ class Kontena::Plugin::PlatformCommand < Kontena::Command
2
+
3
+ subcommand ['list', 'ls'], 'List platforms', load_subcommand('kontena/plugin/platform/list_command')
4
+ subcommand ['use', 'switch'], 'Use/switch local scope to platform', load_subcommand('kontena/plugin/platform/use_command')
5
+ subcommand 'show', 'Show platform details', load_subcommand('kontena/plugin/platform/show_command')
6
+ subcommand 'create', 'Create new platform', load_subcommand('kontena/plugin/platform/create_command')
7
+ subcommand ['remove', 'rm'], 'Remove platform', load_subcommand('kontena/plugin/platform/remove_command')
8
+ subcommand 'audit-log', 'Show platform audit logs', load_subcommand('kontena/plugin/platform/audit_log_command')
9
+ subcommand 'health', 'Show platform health', load_subcommand('kontena/plugin/platform/health_command')
10
+ subcommand 'import-grid', 'Import grid as Kontena Platform', load_subcommand('kontena/plugin/platform/import_grid_command')
11
+
12
+ def execute
13
+ end
14
+ end
@@ -0,0 +1,51 @@
1
+ require 'kontena_cli'
2
+ require 'kontena/command'
3
+ require_relative 'kontena/plugin/cloud'
4
+ require_relative 'kontena/plugin/cloud_command'
5
+ require_relative 'kontena/plugin/platform_command'
6
+
7
+ Kontena::MainCommand.register("platform", "Platform specific commands", Kontena::Plugin::PlatformCommand)
8
+
9
+ module Kontena
10
+ module Cli
11
+ module GridOptions
12
+ def self.included(base)
13
+ if base.respond_to?(:option)
14
+
15
+ base.option '--platform', 'PLATFORM', 'Specify platform to use' do |platform|
16
+ config.current_master = platform
17
+ config.current_grid = platform.split('/')[1]
18
+ end
19
+ base.option '--grid', 'GRID', 'Specify grid to use'
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ module CloudCommand
26
+
27
+ PLATFORM_NOT_SELECTED_ERROR = "Platform not selected, use 'kontena platform use' to select a platform"
28
+
29
+ def verify_current_master
30
+ super
31
+ rescue ArgumentError
32
+ exit_with_error PLATFORM_NOT_SELECTED_ERROR
33
+ end
34
+
35
+ def verify_current_master_token
36
+ super
37
+ rescue ArgumentError
38
+ exit_with_error PLATFORM_NOT_SELECTED_ERROR
39
+ end
40
+
41
+ def verify_current_grid
42
+ super
43
+ rescue ArgumentError
44
+ exit_with_error PLATFORM_NOT_SELECTED_ERROR
45
+ end
46
+ end
47
+
48
+ class Command < Clamp::Command
49
+ prepend CloudCommand
50
+ end
51
+ end
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kontena-plugin-cloud
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0.pre1
5
+ platform: ruby
6
+ authors:
7
+ - Kontena, Inc.
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-08-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: kontena-cli
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 1.3.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 1.3.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.14'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.14'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ description: Kontena Cloud management for Kontena CLI
56
+ email:
57
+ - info@kontena.io
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".rspec"
64
+ - Gemfile
65
+ - LICENSE.txt
66
+ - README.md
67
+ - Rakefile
68
+ - kontena-plugin-cloud.gemspec
69
+ - lib/kontena/cli/master_code_exchanger.rb
70
+ - lib/kontena/cli/models/platform.rb
71
+ - lib/kontena/plugin/cloud.rb
72
+ - lib/kontena/plugin/cloud/organization/list_command.rb
73
+ - lib/kontena/plugin/cloud/organization/show_command.rb
74
+ - lib/kontena/plugin/cloud/organization/user/add_command.rb
75
+ - lib/kontena/plugin/cloud/organization/user/list_command.rb
76
+ - lib/kontena/plugin/cloud/organization/user/remove_command.rb
77
+ - lib/kontena/plugin/cloud/organization/user_command.rb
78
+ - lib/kontena/plugin/cloud/organization_command.rb
79
+ - lib/kontena/plugin/cloud/region/list_command.rb
80
+ - lib/kontena/plugin/cloud/region_command.rb
81
+ - lib/kontena/plugin/cloud_command.rb
82
+ - lib/kontena/plugin/platform/audit_log_command.rb
83
+ - lib/kontena/plugin/platform/common.rb
84
+ - lib/kontena/plugin/platform/create_command.rb
85
+ - lib/kontena/plugin/platform/health_command.rb
86
+ - lib/kontena/plugin/platform/import_grid_command.rb
87
+ - lib/kontena/plugin/platform/list_command.rb
88
+ - lib/kontena/plugin/platform/remove_command.rb
89
+ - lib/kontena/plugin/platform/show_command.rb
90
+ - lib/kontena/plugin/platform/use_command.rb
91
+ - lib/kontena/plugin/platform_command.rb
92
+ - lib/kontena_cli_plugin.rb
93
+ homepage: https://kontena.io
94
+ licenses:
95
+ - Apache-2.0
96
+ metadata: {}
97
+ post_install_message:
98
+ rdoc_options: []
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">"
109
+ - !ruby/object:Gem::Version
110
+ version: 1.3.1
111
+ requirements: []
112
+ rubyforge_project:
113
+ rubygems_version: 2.5.2
114
+ signing_key:
115
+ specification_version: 4
116
+ summary: Kontena Cloud management for Kontena CLI
117
+ test_files: []