gleis 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.
@@ -0,0 +1,126 @@
1
+ module Gleis
2
+ # The class implements the methods required for the databases of a gleis app
3
+ class Database
4
+ def self.backup(app_name)
5
+ token = Token.check
6
+ Utils.check_for_local_pg_command('pg_dump')
7
+ url = Config.get_env_var(app_name, token, 'DATABASE_URL')
8
+ abort('There is no database configured under the DATABASE_URL variable.') unless url
9
+ db_name = url.split('/').last
10
+ timestamp = Time.now.strftime('%Y%m%d%H%M%S')
11
+ backup_file = "/tmp/#{app_name}_#{db_name}_#{timestamp}.pgdump"
12
+ if system("pg_dump -f #{backup_file} #{url}")
13
+ puts "Database configured at DATABASE_URL succesfully backed up locally in #{backup_file}"
14
+ else
15
+ puts 'Failed to backup database configured under DATABASE_URL'
16
+ end
17
+ end
18
+
19
+ def self.delete(app_name, env_var_name)
20
+ token = Token.check
21
+ if Utils.prompt_confirmation("Are you sure you want to delete the database at #{env_var_name}?")
22
+ body = API.request('delete', "db/#{app_name}/#{env_var_name}", token)
23
+ if body['success'] == 1
24
+ puts "Successfully deleted database configured at #{env_var_name}."
25
+ else
26
+ puts "Failed to delete database: #{body['message']}"
27
+ end
28
+ else
29
+ puts 'Command cancelled'
30
+ end
31
+ end
32
+
33
+ def self.info(app_name)
34
+ token = Token.check
35
+ url = Config.get_env_var(app_name, token, 'DATABASE_URL')
36
+ abort_message = 'You do not have a database or you did not promote it yet. '\
37
+ 'You can create one with the db:new command and promote it with db:promote.'
38
+ abort(abort_message) unless url
39
+ # Get database info
40
+ db_name = url.split('/').last
41
+ body = API.request('get', "database/#{db_name}", token)
42
+ return unless body['success'] == 1
43
+
44
+ db = body['database']
45
+ puts "Info about database at DATABASE_URL:\n\n"
46
+ puts "\tName:\t\t#{db['name']}\n" \
47
+ "\tCreated on:\t#{Time.parse(db['created_at']).strftime('%c')}"
48
+ if body['available']
49
+ puts "\tStatus:\t\tavailable\n" \
50
+ "\tDatabase:\t#{body['version']}\n" \
51
+ "\tConnections:\t#{body['connections']}"
52
+ else
53
+ puts "\tStatus:\t\tnot available"
54
+ end
55
+ end
56
+
57
+ def self.new(app_name)
58
+ token = Token.check
59
+ body = API.request('post', 'db', token, 'name': app_name)
60
+ if body['success'] == 1
61
+ puts "Successfully created new database for #{app_name} available as config variable #{body['message']}"
62
+ else
63
+ puts 'Failed to create new database'
64
+ end
65
+ end
66
+
67
+ def self.promote(app_name, env_var_name)
68
+ token = Token.check
69
+ body = API.request('post', 'db/promote', token, 'name': app_name, 'var': env_var_name)
70
+ if body['success'] == 1
71
+ puts "Succesfully promoted database environment variable #{env_var_name} to DATABASE_URL"
72
+ else
73
+ puts 'Failed to promote database environment variable'
74
+ end
75
+ end
76
+
77
+ def self.push(app_name, local_name)
78
+ token = Token.check
79
+ Utils.check_for_local_pg_command('pg_dump')
80
+ Utils.check_for_local_pg_command('pg_restore')
81
+ url = Config.get_env_var(app_name, token, 'DATABASE_URL')
82
+ abort('There is no database configured under the DATABASE_URL variable.') unless url
83
+ ENV['PGCONNECT_TIMEOUT'] = '5'
84
+ # Check if database is empty
85
+ sql_statement = "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='public'"
86
+ table_count = `psql #{url} -t -A -c "#{sql_statement}"`.chomp
87
+ if table_count.to_i.zero?
88
+ # Check connection to local database
89
+ unless system("psql -c 'SELECT 1' #{local_name} >/dev/null 2>&1")
90
+ abort("Failed to connect to local database #{local_name}, please check " \
91
+ 'that the database name is correct and that you have access to it.')
92
+ end
93
+ if system("pg_dump -Fc -x #{local_name} | pg_restore -O -n public -d #{url}")
94
+ puts "Successfully pushed local database #{local_name} to database configured at DATABASE_URL."
95
+ else
96
+ puts "Failed to push local database #{local_name} to DATABASE_URL."
97
+ end
98
+ else
99
+ puts 'The database configured at DATABASE_URL already contains data, please empty it first.'
100
+ end
101
+ end
102
+
103
+ def self.psql(app_name)
104
+ token = Token.check
105
+ abort('The PostgreSQL client psql is not installed on this system.') unless system('which psql >/dev/null')
106
+ url = Config.get_env_var(app_name, token, 'DATABASE_URL')
107
+ abort('There is no database configured under the DATABASE_URL variable.') unless url
108
+ ENV['PGCONNECT_TIMEOUT'] = '5'
109
+ exec("psql #{url}")
110
+ end
111
+
112
+ def self.reset(app_name, env_var_name)
113
+ token = Token.check
114
+ if Utils.prompt_confirmation("Are you sure you want to reset the database at #{env_var_name}?")
115
+ body = API.request('put', 'db', token, 'name': app_name, 'var': env_var_name)
116
+ if body['success'] == 1
117
+ puts "Successfully reset database configured at #{env_var_name}."
118
+ else
119
+ puts "Failed to delete database: #{body['message']}"
120
+ end
121
+ else
122
+ puts 'Command cancelled'
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,39 @@
1
+ module Gleis
2
+ # The class implements the methods required to manage the domain names of a gleis app
3
+ class Domain
4
+ def self.add(app_name, name)
5
+ token = Token.check
6
+ body = API.request('post', 'domains', token, 'name': app_name, 'domain': name)
7
+ if body['success'] == 1
8
+ puts "Successfully added domain to #{app_name}."
9
+ else
10
+ puts "Failed to add domain: #{body['message']}"
11
+ end
12
+ end
13
+
14
+ def self.list(app_name)
15
+ token = Token.check
16
+ body = API.request('get', "domains/#{app_name}", token)
17
+ puts "Your app is available under the URL: https://#{body['fqdn']}\n\n"
18
+ domains = body ['data']
19
+ if domains.any?
20
+ puts "Domain list for #{app_name}:\n\n"
21
+ domains.each do |domain|
22
+ puts "\t#{domain['name']}"
23
+ end
24
+ else
25
+ puts 'No domains defined yet.'
26
+ end
27
+ end
28
+
29
+ def self.remove(app_name, name)
30
+ token = Token.check
31
+ body = API.request('delete', "domains/#{app_name}/#{name}", token)
32
+ if body['success'] == 1
33
+ puts "Successfully removed domain name from #{app_name}."
34
+ else
35
+ puts "Failed to remove domain name: #{body['message']}"
36
+ end
37
+ end
38
+ end
39
+ end
data/lib/gleis/main.rb ADDED
@@ -0,0 +1,36 @@
1
+ require 'thor'
2
+ require 'gleis'
3
+
4
+ module Gleis
5
+ # This class defines all the main command-line interface commands for gleis
6
+ class Main < Thor
7
+ desc 'login USERNAME', 'Login into Gleis'
8
+ def login(username)
9
+ Authentication.login(username)
10
+ end
11
+
12
+ desc 'addon COMMAND', 'Manage add-ons: add, list, remove'
13
+ subcommand 'addon', CLI::Addon
14
+
15
+ desc 'app COMMAND', 'Manage applications: create, start, stop, logs, etc.'
16
+ subcommand 'app', CLI::App
17
+
18
+ desc 'auth COMMAND', 'Authentication commands: login, logout, whoami'
19
+ subcommand 'auth', CLI::Auth
20
+
21
+ desc 'db COMMAND', 'Manage databases: info, new, promote, psql, etc.'
22
+ subcommand 'db', CLI::Db
23
+
24
+ desc 'domain COMMAND', 'Manage domains: add, list, remove'
25
+ subcommand 'domain', CLI::Domain
26
+
27
+ desc 'mgmt COMMAND', 'Administration and management related commands'
28
+ subcommand 'mgmt', CLI::Management
29
+
30
+ desc 'sharing COMMAND', 'Manage collaboration: add, list, remove'
31
+ subcommand 'sharing', CLI::Sharing
32
+
33
+ desc 'storage COMMAND', 'Manage persistent storage: add, list, sync, etc.'
34
+ subcommand 'storage', CLI::Storage
35
+ end
36
+ end
@@ -0,0 +1,42 @@
1
+ module Gleis
2
+ # The class implements management and administrative methods around the Gleis platform
3
+ class Management
4
+ def self.apps
5
+ token = Token.check
6
+ body = API.request('get', 'mgmt/apps', token)
7
+ apps = body ['data']
8
+ if apps.any?
9
+ puts "Apps list for your organisation:\n\n"
10
+ printf("\t%-39s %-25s %s\n", 'APP NAME', 'CREATED ON', 'PLAN')
11
+ printf("\t%-39s %-25s %s\n\n", '--------', '----------', '----')
12
+ apps.each do |app|
13
+ printf("\t%-39s %-25s %s\n", app['name'], Time.parse(app['created_at']).localtime.strftime('%c'),
14
+ app['plan_name'].capitalize)
15
+ end
16
+ else
17
+ puts 'No apps found.'
18
+ end
19
+ end
20
+
21
+ def self.license
22
+ puts <<LICENSE
23
+ Gleis Command Line Interface for the Gleis Platform as a Service
24
+ Copyright (C) 2018 towards GmbH
25
+
26
+ This program is free software: you can redistribute it and/or modify
27
+ it under the terms of the GNU General Public License as published by
28
+ the Free Software Foundation, either version 3 of the License, or
29
+ (at your option) any later version.
30
+
31
+ This program is distributed in the hope that it will be useful,
32
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
33
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
34
+ GNU General Public License for more details.
35
+
36
+ You should have received a copy of the GNU General Public License
37
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
38
+
39
+ LICENSE
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,9 @@
1
+ module Gleis
2
+ # This class deals with the CLI config parameters stored on the API server
3
+ class Params
4
+ def self.get_cli_parameters(token)
5
+ body = API.request('get', 'params', token)
6
+ body['data']
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,38 @@
1
+ module Gleis
2
+ # The class implements the methods required for collaborating on a gleis app
3
+ class Sharing
4
+ def self.add(app_name, email, role)
5
+ token = Token.check
6
+ body = API.request('post', 'sharing', token, 'name': app_name, 'email': email, 'role': role)
7
+ if body['success'] == 1
8
+ puts "Successfully added user to access list for #{app_name}."
9
+ else
10
+ puts "Failed to add user to access list: #{body['message']}"
11
+ end
12
+ end
13
+
14
+ def self.list(app_name)
15
+ token = Token.check
16
+ body = API.request('get', "sharing/#{app_name}", token)
17
+ acls = body ['acl']
18
+ if acls.any?
19
+ puts "Access list for #{app_name}:\n\n"
20
+ acls.each do |acl|
21
+ printf("\t%-45s %s\n", acl['user_name'], acl['role_name'])
22
+ end
23
+ else
24
+ puts 'No access list defined yet.'
25
+ end
26
+ end
27
+
28
+ def self.remove(app_name, email)
29
+ token = Token.check
30
+ body = API.request('delete', "sharing/#{app_name}/#{email}", token)
31
+ if body['success'] == 1
32
+ puts "Successfully removed user from access list for #{app_name}."
33
+ else
34
+ puts "Failed to remove user from access list: #{body['message']}"
35
+ end
36
+ end
37
+ end
38
+ end
data/lib/gleis/ssh.rb ADDED
@@ -0,0 +1,36 @@
1
+ require 'socket'
2
+
3
+ module Gleis
4
+ # This class manages the SSH config and keys on the client side
5
+ class SSH
6
+ def self.generate_key(key_file, username)
7
+ return if File.exist?(key_file)
8
+
9
+ puts 'Could not find an existing public/private key pair'
10
+ return unless Utils.prompt_yes_no('Would you like to generate one now?')
11
+
12
+ hostname = Socket.gethostname
13
+ datetime = Time.now.strftime('%Y-%m-%d %H:%M')
14
+ # returns true on success
15
+ system('ssh-keygen', '-f', key_file, '-b 4096',
16
+ "-C generated by Gleis CLI for #{username} by #{ENV['USER']}@#{hostname} on #{datetime}")
17
+ end
18
+
19
+ def self.load_public_key(key_file)
20
+ public_key_file = key_file + '.pub'
21
+ return File.read(public_key_file).chomp if File.exist?(public_key_file)
22
+ end
23
+
24
+ def self.add_host_to_config(git_host, run_host, key_file)
25
+ config_file = Dir.home + '/.ssh/config'
26
+ ssh_host_line = "Host #{git_host} #{run_host}"
27
+ # Check if SSH config for hosts already exists
28
+ return if Utils.line_exists_in_file(key_file, ssh_host_line)
29
+
30
+ f = File.open(config_file, 'a')
31
+ f.puts ssh_host_line
32
+ f.puts "\tIdentityFile #{key_file}"
33
+ f.close
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,69 @@
1
+ module Gleis
2
+ # The class implements the methods required to the storage of a gleis app
3
+ class Storage
4
+ def self.add(app_name, type)
5
+ token = Token.check
6
+ body = API.request('post', 'storage', token, 'name': app_name, 'type': type)
7
+ if body['success'] == 1
8
+ puts "Successfully added #{type} storage to #{app_name}. As soon as you restart " \
9
+ "your app the storage will be available under the #{body['mount_target']} directory."
10
+ else
11
+ puts "Failed to add storage: #{body['message']}"
12
+ end
13
+ end
14
+
15
+ def self.attach(app_name, dir)
16
+ token = Token.check
17
+ body = API.request('post', 'storage/attach', token, 'name': app_name, 'dir': dir)
18
+ if body['success'] == 1
19
+ puts "Successfully attached storage to #{dir} directory. As soon as you restart your app " \
20
+ 'the storage will be available under this directory.'
21
+ else
22
+ puts "Failed to attach storage: #{body['message']}"
23
+ end
24
+ end
25
+
26
+ def self.list(app_name)
27
+ token = Token.check
28
+ action = 'storage/' + app_name
29
+ body = API.request('get', action, token)
30
+ puts "Available storage types:\n\n"
31
+ printf("\t%-8s %s\n", 'TYPE', 'DESCRIPTION')
32
+ printf("\t%-8s %s\n\n", '----', '-----------')
33
+ body['data']['storage_types'].each do |st|
34
+ printf("\t%-8s %s\n", st['name'], st['description'])
35
+ end
36
+ if body['data']['storage'].any?
37
+ puts "\nStorage in use by app:\n\n"
38
+ printf("\t%-8s %-30s %-25s %s\n", 'TYPE', 'MOUNT TARGET', 'CREATED ON', 'USAGE')
39
+ printf("\t%-8s %-30s %-25s %s\n\n", '----', '------------', '-----------', '-----')
40
+ body['data']['storage'].each do |s|
41
+ printf("\t%-8s %-30s %-25s %s\n", s['storage_type_name'], s['mount_target'],
42
+ Time.parse(s['created_at']).localtime.strftime('%c'), s['usage'])
43
+ end
44
+ else
45
+ puts "\nYour app is not currently using any storage."
46
+ end
47
+ end
48
+
49
+ def self.sync(app_name, dir)
50
+ token = Token.check
51
+ abort("Directory #{dir} does not exist or is not a directory.") unless Dir.exist?(dir)
52
+ # Get CLI parameters from API server
53
+ params = Params.get_cli_parameters(token)
54
+ abort('The rsync tool is not installed on this system.') unless system('which rsync >/dev/null')
55
+ body = API.request('get', "storage/#{app_name}", token)
56
+ if body['success'] == 1
57
+ if body['storage']
58
+ ns_app_name = "#{body['namespace']}_#{app_name}"
59
+ ENV['RSYNC_PASSWORD'] = body['storage']['password']
60
+ exec("rsync -rth --progress #{dir}/ rsync://#{ns_app_name}@#{params['sync_server']}/#{ns_app_name}")
61
+ else
62
+ puts 'No storage configured yet.'
63
+ end
64
+ else
65
+ puts 'Failed to get storage info.'
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,21 @@
1
+ module Gleis
2
+ # This class manages the authentication token granted from the API server
3
+ class Token
4
+ def self.save(token)
5
+ abort('No tokens received.') unless token
6
+ f = File.new(Config::TOKEN_FILE, 'w', 0o0640)
7
+ f.write(token)
8
+ f.close
9
+ end
10
+
11
+ def self.check
12
+ return File.read(Config::TOKEN_FILE) if File.exist?(Config::TOKEN_FILE)
13
+
14
+ abort('Not authenticated, please login first.')
15
+ end
16
+
17
+ def self.delete
18
+ File.delete(Config::TOKEN_FILE) if File.exist?(Config::TOKEN_FILE)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,105 @@
1
+ module Gleis
2
+ # This class gathers various utilities as small methods
3
+ class Utils
4
+ def self.convert_username_to_filename(username)
5
+ username.sub('@', '_at_').tr('.', '_')
6
+ end
7
+
8
+ def self.prompt_password
9
+ print 'Password: '
10
+ system 'stty -echo'
11
+ password = $stdin.gets.chomp
12
+ system 'stty echo'
13
+ puts
14
+ password
15
+ end
16
+
17
+ def self.prompt_yes_no(text)
18
+ loop do
19
+ print text + ' [y/n]: '
20
+ case STDIN.gets.strip
21
+ when 'Y', 'y', 'yes'
22
+ return true
23
+ when /\A[nN]o?\Z/
24
+ return false
25
+ end
26
+ end
27
+ end
28
+
29
+ def self.app_name
30
+ Dir.pwd.split('/').last
31
+ end
32
+
33
+ def self.check_for_local_pg_command(command)
34
+ abort("The PostgreSQL client required to run the command #{command}) is not installed on this system.") unless
35
+ system("which #{command} >/dev/null")
36
+ end
37
+
38
+ def self.validate_scale_count(count)
39
+ count_i = count.to_i
40
+ abort('Please specifiy a number between 1 and 10 as parameter in order to scale your app.') unless
41
+ count_i&.between?(1, 10)
42
+ count_i
43
+ end
44
+
45
+ def self.add_remote_to_git_config(remote_url)
46
+ config_file = Dir.pwd + '/.git/config'
47
+ return false unless File.exist?(config_file)
48
+
49
+ # Check if gleis remote already exists
50
+ git_remote_line = '[remote "gleis"]'
51
+ return if line_exists_in_file(config_file, git_remote_line)
52
+
53
+ f = File.open(config_file, 'a')
54
+ f.puts "\n#{git_remote_line}"
55
+ f.puts "\turl = #{remote_url}"
56
+ f.puts "\tfetch = +refs/heads/*:refs/remotes/origin/*"
57
+ f.close
58
+ end
59
+
60
+ def self.line_exists_in_file(file, line)
61
+ File.readlines(file).grep(/#{Regexp.escape(line)}/).size.positive?
62
+ end
63
+
64
+ def self.prompt_confirmation(text)
65
+ print 'WARNING: ' + text + ' (write YES in uppercase to confirm): '
66
+ answer = STDIN.gets.chomp
67
+ answer == 'YES'
68
+ end
69
+
70
+ def self.output_tasks(tasks, type)
71
+ tasks.each do |task|
72
+ timestamp_formatted = Time.parse(task['timestamp']).localtime.strftime('%c')
73
+ puts "#{type}.#{task['slot']}: #{task['status']} #{timestamp_formatted}"
74
+ end
75
+ end
76
+
77
+ def self.output_config_env_vars(env_vars, app_name)
78
+ puts "Configuration (environment variables) for #{app_name}:\n\n"
79
+ env_vars.each do |key, value|
80
+ puts "\t#{key}=#{value}"
81
+ end
82
+ end
83
+
84
+ def self.generate_docker_cmd_env_vars(env_vars)
85
+ env_vars_param = ''
86
+ env_vars.each do |key, value|
87
+ env_vars_param << " -e #{key}=#{value}"
88
+ end
89
+ env_vars_param
90
+ end
91
+
92
+ def self.generate_docker_cmd_mount(body, app_name)
93
+ mount_param = ''
94
+ if body['success'] == 1
95
+ storage_type = body['storage_type']
96
+ if storage_type
97
+ storage = body['storage']
98
+ mount_src = "#{storage_type['mount_source']}/#{body['namespace']}/#{app_name}"
99
+ mount_param = "--mount type=#{storage_type['mount_type']},src=#{mount_src},dst=#{storage['mount_target']}"
100
+ end
101
+ end
102
+ mount_param
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,3 @@
1
+ module Gleis
2
+ VERSION = '0.1.0'
3
+ end
data/lib/gleis.rb ADDED
@@ -0,0 +1,29 @@
1
+ require 'gleis/utils'
2
+ require 'gleis/authentication'
3
+ require 'gleis/addon'
4
+ require 'gleis/api'
5
+ require 'gleis/application'
6
+ require 'gleis/cli/addon'
7
+ require 'gleis/cli/app'
8
+ require 'gleis/cli/auth'
9
+ require 'gleis/cli/db'
10
+ require 'gleis/cli/domain'
11
+ require 'gleis/cli/management'
12
+ require 'gleis/cli/sharing'
13
+ require 'gleis/cli/storage'
14
+ require 'gleis/config'
15
+ require 'gleis/database'
16
+ require 'gleis/domain'
17
+ require 'gleis/management'
18
+ require 'gleis/params'
19
+ require 'gleis/sharing'
20
+ require 'gleis/ssh'
21
+ require 'gleis/storage'
22
+ require 'gleis/token'
23
+ require 'gleis/version'
24
+
25
+ require 'json'
26
+
27
+ # Top-level gleis module namespace
28
+ module Gleis
29
+ end