cyclid-client 0.3.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.
@@ -0,0 +1,129 @@
1
+ # Copyright 2016 Liqwyd Ltd.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'colorize'
16
+ require 'uri'
17
+
18
+ require_rel 'organization/*.rb'
19
+
20
+ module Cyclid
21
+ module Cli
22
+ # 'organization' sub-command
23
+ class Organization < Thor
24
+ desc 'show', 'Show details of the organization'
25
+ def show
26
+ org = client.org_get(client.config.organization)
27
+
28
+ # Convert the public key to PEM
29
+ der_key = Base64.decode64(org['public_key'])
30
+ public_key = OpenSSL::PKey::RSA.new(der_key)
31
+
32
+ # Pretty print the organization details
33
+ puts 'Name: '.colorize(:cyan) + org['name']
34
+ puts 'Owner Email: '.colorize(:cyan) + org['owner_email']
35
+ puts 'Public Key: '.colorize(:cyan) + public_key.to_pem
36
+ puts 'Members:'.colorize(:cyan)
37
+ if org['users'].any?
38
+ org['users'].each do |user|
39
+ puts "\t#{user}"
40
+ end
41
+ else
42
+ puts "\tNone"
43
+ end
44
+ rescue StandardError => ex
45
+ abort "Failed to get organization: #{ex}"
46
+ end
47
+
48
+ desc 'modify', 'Modify the organization'
49
+ long_desc <<-LONGDESC
50
+ Modify the organization.
51
+
52
+ The --email option sets the owners email address.
53
+ LONGDESC
54
+ option :email, aliases: '-e'
55
+ def modify
56
+ client.org_modify(client.config.organization,
57
+ owner_email: options[:email])
58
+ rescue StandardError => ex
59
+ abort "Failed to modify organization: #{ex}"
60
+ end
61
+
62
+ desc 'list', 'List your available organizations'
63
+ def list
64
+ Dir.glob("#{CYCLID_CONFIG_DIR}/*").each do |fname|
65
+ next if File.symlink?(fname)
66
+ next unless File.file?(fname)
67
+
68
+ begin
69
+ # Create a Config from this file and display the details
70
+ config = Cyclid::Client::Config.new(path: fname)
71
+
72
+ puts File.basename(fname).colorize(:cyan)
73
+ uri = URI::HTTP.build(host: config.server, port: config.port)
74
+ puts "\tServer: ".colorize(:cyan) + uri.to_s
75
+ puts "\tOrganization: ".colorize(:cyan) + config.organization
76
+ puts "\tUsername: ".colorize(:cyan) + config.username
77
+ rescue StandardError => ex
78
+ $stderr.puts "Failed to load config file #{fname}: #{ex}"
79
+ end
80
+ end
81
+ end
82
+
83
+ desc 'use NAME', 'Select the organization NAME to use by default'
84
+ def use(name = nil)
85
+ # If 'use' was called without an argument, print the name of the
86
+ # current configuration
87
+ if name.nil?
88
+ fname = if File.symlink?(options[:config])
89
+ File.readlink(options[:config])
90
+ else
91
+ options[:config]
92
+ end
93
+ puts File.basename(fname)
94
+ else
95
+ # List the avialble configurations
96
+ fname = File.join(CYCLID_CONFIG_DIR, name)
97
+
98
+ # Sanity check that the configuration file exists and is valid
99
+ abort 'No such organization' unless File.exist?(fname)
100
+ abort 'Not a valid organization' unless File.file?(fname)
101
+
102
+ begin
103
+ config = Cyclid::Client::Config.new(path: fname)
104
+
105
+ raise if config.server.nil? or \
106
+ config.organization.nil? or \
107
+ config.username.nil? or \
108
+ config.secret.nil?
109
+ rescue StandardError
110
+ abort 'Invalid configuration file'
111
+ end
112
+
113
+ # The configuration file exists and appears to be sane, so switch the
114
+ # 'config' symlink to point to it.
115
+ Dir.chdir(CYCLID_CONFIG_DIR) do
116
+ File.delete('config')
117
+ File.symlink(name, 'config')
118
+ end
119
+ end
120
+ end
121
+
122
+ desc 'member', 'Manage organization members'
123
+ subcommand 'member', Member
124
+
125
+ desc 'config', 'Manage organization configuration'
126
+ subcommand 'config', Config
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,90 @@
1
+ # Copyright 2016 Liqwyd Ltd.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module Cyclid
16
+ module Cli
17
+ # Commands for managing per. organization configuration
18
+ class Config < Thor
19
+ desc 'show TYPE PLUGIN', 'Show a plugin configuration'
20
+ def show(type, plugin)
21
+ plugin_data = client.org_config_get(client.config.organization, type, plugin)
22
+
23
+ config = plugin_data['config']
24
+ schema = plugin_data['schema']
25
+ schema.each do |setting|
26
+ name = setting['name']
27
+ type = setting['type']
28
+
29
+ case type
30
+ when 'string', 'integer'
31
+ data = config[name] || 'Not set'
32
+ puts "#{setting['description']}: ".colorize(:cyan) + data
33
+ when 'boolean'
34
+ data = config[name] || 'Not set'
35
+ puts "#{setting['description']}: ".colorize(:cyan) + (data ? 'true' : 'false')
36
+ when 'list'
37
+ puts setting['description'].colorize(:cyan)
38
+ data = config[name]
39
+ if data.empty?
40
+ puts "\tNone"
41
+ else
42
+ data.each do |item|
43
+ puts "\t#{item}"
44
+ end
45
+ end
46
+ when 'hash-list'
47
+ puts setting['description'].colorize(:cyan)
48
+ data = config[name]
49
+ if data.empty?
50
+ puts "\tNone"
51
+ else
52
+ data.each do |item|
53
+ item.each do |k, v|
54
+ puts "\t#{k}: #{v}"
55
+ end
56
+ end
57
+ end
58
+ else
59
+ raise "unknown schema type #{type}"
60
+ end
61
+ end
62
+ rescue StandardError => ex
63
+ abort "Failed to get plugin configuration: #{ex}"
64
+ end
65
+
66
+ desc 'edit TYPE PLUGIN', 'Edit a plugin configuration'
67
+ def edit(type, plugin)
68
+ plugin_data = client.org_config_get(client.config.organization, type, plugin)
69
+
70
+ # Inject the schema description into each config item
71
+ schema = plugin_data['schema']
72
+ config = plugin_data['config'].each do |k, v|
73
+ description = ''
74
+ schema.each do |item|
75
+ description = item['description'] if item['name'] == k
76
+ end
77
+ { k => v, 'description' => description }
78
+ end
79
+
80
+ # Open a text editor on the configuration
81
+ config = invoke_editor(config)
82
+
83
+ # Submit it to the server
84
+ client.org_config_set(client.config.organization, type, plugin, config)
85
+ rescue StandardError => ex
86
+ abort "Failed to update plugin configuration: #{ex}"
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,126 @@
1
+ # Copyright 2016 Liqwyd Ltd.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module Cyclid
16
+ module Cli
17
+ # Commands for managing organization members
18
+ class Member < Thor
19
+ desc 'add USERS', 'Add users to the organization'
20
+ def add(*users)
21
+ org = client.org_get(client.config.organization)
22
+
23
+ # Concat the new list with the existing list and remove any
24
+ # duplicates.
25
+ user_list = org['users']
26
+ user_list.concat users
27
+ user_list.uniq!
28
+
29
+ client.org_modify(client.config.organization,
30
+ members: user_list)
31
+ rescue StandardError => ex
32
+ abort "Failed to add users to the organization: #{ex}"
33
+ end
34
+
35
+ desc 'permission USER PERMISSION', 'Modify a members organization permissions'
36
+ long_desc <<-LONGDESC
37
+ Modify the organization permissions for USER.
38
+
39
+ PERMISSION must be one of:
40
+ * admin - Give the user organization administrator permissions.
41
+ * write - Give the user organization create/modify permissions.
42
+ * read - Give the user organization read permissions.
43
+ * none - Remove all organization permissions.
44
+
45
+ With 'none' the user remains an organization member but can not
46
+ interact with it. See the 'member remove' command if you want to
47
+ actually remove a user from your organization.
48
+ LONGDESC
49
+ def permission(user, permission)
50
+ perms = case permission.downcase
51
+ when 'admin'
52
+ { 'admin' => true, 'write' => true, 'read' => true }
53
+ when 'write'
54
+ { 'admin' => false, 'write' => true, 'read' => true }
55
+ when 'read'
56
+ { 'admin' => false, 'write' => false, 'read' => true }
57
+ when 'none'
58
+ { 'admin' => false, 'write' => false, 'read' => false }
59
+ else
60
+ raise "invalid permission #{permission}"
61
+ end
62
+
63
+ client.org_user_permissions(client.config.organization, user, perms)
64
+ rescue StandardError => ex
65
+ abort "Failed to modify user permissions: #{ex}"
66
+ end
67
+ map 'perms' => :permission
68
+
69
+ desc 'list', 'List organization members'
70
+ def list
71
+ org = client.org_get(client.config.organization)
72
+ org['users'].each do |user|
73
+ puts user
74
+ end
75
+ rescue StandardError => ex
76
+ abort "Failed to get organization members: #{ex}"
77
+ end
78
+
79
+ desc 'show USER', 'Show details of an organization member'
80
+ def show(user)
81
+ user = client.org_user_get(client.config.organization, user)
82
+
83
+ # Pretty print the user details
84
+ puts 'Username: '.colorize(:cyan) + user['username']
85
+ puts 'Email: '.colorize(:cyan) + user['email']
86
+ puts 'Permissions'.colorize(:cyan)
87
+ user['permissions'].each do |k, v|
88
+ puts "\t#{k.capitalize}: ".colorize(:cyan) + v.to_s
89
+ end
90
+ rescue StandardError => ex
91
+ abort "Failed to get user: #{ex}"
92
+ end
93
+
94
+ desc 'remove USERS', 'Remove users from the organization'
95
+ long_desc <<-LONGDESC
96
+ Remove the list of USERS from the organization.
97
+
98
+ The --force option will remove the users without asking for confirmation.
99
+ LONGDESC
100
+ option :force, aliases: '-f', type: :boolean
101
+ def remove(*users)
102
+ org = client.org_get(client.config.organization)
103
+
104
+ # Remove any users that exist as members of this organization;
105
+ # ask for confirmation on a per-user basis (unless '-f' was passed)
106
+ user_list = org['users']
107
+ user_list.delete_if do |user|
108
+ if users.include? user
109
+ if options[:force]
110
+ true
111
+ else
112
+ print "Remove user #{user}: are you sure? (Y/n): ".colorize(:red)
113
+ STDIN.getc.chr.casecmp('y') == 0
114
+ end
115
+ end
116
+ end
117
+ user_list.uniq!
118
+
119
+ client.org_modify(client.config.organization,
120
+ members: user_list)
121
+ rescue StandardError => ex
122
+ abort "Failed to remove users from the organization: #{ex}"
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,42 @@
1
+ # Copyright 2016 Liqwyd Ltd.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'colorize'
16
+
17
+ module Cyclid
18
+ module Cli
19
+ # 'secret' sub-command
20
+ class Secret < Thor
21
+ desc 'encrypt', 'Encrypt a secret with the organizations public key'
22
+ def encrypt
23
+ # Get the organizations public key in a form we can use
24
+ org = client.org_get(client.config.organization)
25
+ der_key = Base64.decode64(org['public_key'])
26
+ public_key = OpenSSL::PKey::RSA.new(der_key)
27
+
28
+ # Get the secret in a safe manner
29
+ print 'Secret: '
30
+ secret = STDIN.noecho(&:gets).chomp
31
+ print "\r"
32
+
33
+ # Encrypt with the public key
34
+ encrypted = public_key.public_encrypt(secret)
35
+
36
+ puts 'Secret: '.colorize(:cyan) + Base64.strict_encode64(encrypted)
37
+ rescue StandardError => ex
38
+ abort "Failed to encrypt secret: #{ex}"
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,121 @@
1
+ # Copyright 2016 Liqwyd Ltd.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module Cyclid
16
+ module Cli
17
+ # 'stage' sub-command
18
+ class Stage < Thor
19
+ desc 'list', 'List the defined stages'
20
+ def list
21
+ stages = client.stage_list(client.config.organization)
22
+ stages.each do |stage|
23
+ puts "#{stage[:name]} v#{stage[:version]}"
24
+ end
25
+ rescue StandardError => ex
26
+ abort "Failed to get stages: #{ex}"
27
+ end
28
+
29
+ desc 'show NAME', 'Show details of a stage'
30
+ def show(name)
31
+ stages = client.stage_get(client.config.organization, name)
32
+
33
+ # Pretty print the stage details
34
+ stages.each do |stage|
35
+ puts 'Name: '.colorize(:cyan) + stage['name']
36
+ puts 'Version: '.colorize(:cyan) + stage['version']
37
+ puts 'Steps'.colorize(:cyan)
38
+ stage['steps'].each do |step|
39
+ puts "\t\tAction: ".colorize(:cyan) + step['action']
40
+ step.delete('action')
41
+ step.each do |k, v|
42
+ puts "\t\t#{k.capitalize}: ".colorize(:cyan) + v.to_s
43
+ end
44
+ end
45
+ end
46
+ rescue StandardError => ex
47
+ abort "Failed to get stage: #{ex}"
48
+ end
49
+
50
+ desc 'create FILENAME', 'Create a new stage from a file'
51
+ long_desc <<-LONGDESC
52
+ Create a new stage. FILENAME should be the path to a valid Cyclid stage definition, in
53
+ either YAML or JSON format.
54
+
55
+ You can create multiple versions of a stage with the same name. You can either set the
56
+ version in the stage definition itself or use the --version option.
57
+
58
+ Cyclid will attempt to detect the format of the file automatically. You can force the
59
+ parsing format using either the --yaml or --json options.
60
+
61
+ The --yaml option causes the file to be parsed as YAML.
62
+
63
+ The --json option causes the file to be parsed as JSON.
64
+ LONGDESC
65
+ option :yaml, aliases: '-y'
66
+ option :json, aliases: '-j'
67
+ option :version, aliases: '-v'
68
+ def create(filename)
69
+ stage_file = File.expand_path(filename)
70
+ raise 'Cannot open file' unless File.exist?(stage_file)
71
+
72
+ stage_type = if options[:yaml]
73
+ 'yaml'
74
+ elsif options[:json]
75
+ 'json'
76
+ else
77
+ # Detect format
78
+ match = stage_file.match(/\A.*\.(json|yml|yaml)\z/)
79
+ match[1]
80
+ end
81
+ stage_type = 'yaml' if stage_type == 'yml'
82
+
83
+ # Do a client-side sanity check by attempting to parse the file; it
84
+ # will fail-fast if the file has a syntax error
85
+ stage = File.read(stage_file)
86
+ stage_data = if stage_type == 'yaml'
87
+ YAML.load(stage)
88
+ elsif stage_type == 'json'
89
+ JSON.parse(stage)
90
+ else
91
+ raise 'Unknown or unsupported file type'
92
+ end
93
+
94
+ # Inject the version if it was passed on the command line
95
+ stage_data['version'] = options[:version] if options[:version]
96
+
97
+ client.stage_create(client.config.organization, stage_data)
98
+ rescue StandardError => ex
99
+ abort "Failed to create stage: #{ex}"
100
+ end
101
+
102
+ desc 'edit NAME', 'Edit a stage definition'
103
+ long_desc <<-LONGDESC
104
+ Edit a stage. Individual stages are immutable, but you may create a new
105
+ version of an existing stage using this command.
106
+ LONGDESC
107
+ def edit(name)
108
+ stages = client.stage_get(client.config.organization, name)
109
+
110
+ # XXX This is a hack. The API returns all stages from this endpoint;
111
+ # we might need to add or extend the API to return "latest" only.
112
+ stage = stages.last
113
+
114
+ stage = invoke_editor(stage)
115
+ client.stage_modify(client.config.organization, stage)
116
+ rescue StandardError => ex
117
+ abort "Failed to edit stage: #{ex}"
118
+ end
119
+ end
120
+ end
121
+ end