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.
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'gleis'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/exe/gleis ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'gleis/main'
4
+
5
+ puts "\n### Gleis CLI Copyright (c) 2018 towards GmbH - v#{Gleis::VERSION} - https://gleis.cloud ###\n\n"
6
+
7
+ Gleis::Main.start
@@ -0,0 +1,41 @@
1
+ module Gleis
2
+ # The class implements the methods required to manage the add-ons of a gleis app
3
+ class Addon
4
+ def self.add(app_name, name)
5
+ token = Token.check
6
+ body = API.request('post', 'addons', token, 'name': app_name, 'addon': name)
7
+ if body['success'] == 1
8
+ puts "Successfully added #{name} add-on to #{app_name}."
9
+ else
10
+ puts "Failed to add add-on: #{body['message']}"
11
+ end
12
+ end
13
+
14
+ def self.list(app_name)
15
+ token = Token.check
16
+ body = API.request('get', "addons/#{app_name}", token)
17
+ addons = body ['data']
18
+ if addons.any?
19
+ puts "List of available add-ons:\n\n"
20
+ printf("\t%-30s %-15s %s\n", 'ADD-ON NAME', 'VERSION', 'CATEGORY')
21
+ printf("\t%s\n\n", 'DESCRIPTION')
22
+ addons.each do |addon|
23
+ printf("\t%-30s %-15s %s\n", addon['name'], addon['version'], addon['category'])
24
+ puts "\t#{addon['description']}\n"
25
+ end
26
+ else
27
+ puts 'No add-ons avaialble.'
28
+ end
29
+ end
30
+
31
+ def self.remove(app_name, name)
32
+ token = Token.check
33
+ body = API.request('delete', "addons/#{app_name}/#{name}", token)
34
+ if body['success'] == 1
35
+ puts "Successfully removed #{name} add-on from #{app_name}."
36
+ else
37
+ puts "Failed to remove add-on: #{body['message']}"
38
+ end
39
+ end
40
+ end
41
+ end
data/lib/gleis/api.rb ADDED
@@ -0,0 +1,45 @@
1
+ require 'rest-client'
2
+
3
+ module Gleis
4
+ # This class uses REST over HTTPS to communicate with the API server
5
+ class API
6
+ def self.request(method, action, token = nil, body = {})
7
+ url = Config::API_URL + Config::API_VERSION + '/cli/' + action
8
+ # Support legacy rest-client (<2.0) which does not have RestClient::NotFound
9
+ notfound_exception = defined?(RestClient::ResourceNotFound) ? RestClient::ResourceNotFound : RestClient::NotFound
10
+ begin
11
+ case method
12
+ when 'get', 'delete'
13
+ resp = RestClient.send(method, url, 'X-Gleis-Token': token, content_type: :json, accept: :json)
14
+ when 'post', 'put'
15
+ if token
16
+ resp =
17
+ RestClient.send(method, url, body.to_json, 'X-Gleis-Token': token, content_type: :json, accept: :json)
18
+ else
19
+ resp = RestClient.send(method, url, body.to_json, content_type: :json, accept: :json)
20
+ end
21
+ end
22
+ rescue RestClient::BadRequest
23
+ abort('Authentication failed.')
24
+ rescue RestClient::Unauthorized
25
+ abort('Not authenticated, please login first.')
26
+ rescue RestClient::Forbidden
27
+ abort('You do not have the required permissions, please contact the app owner.')
28
+ rescue notfound_exception
29
+ abort('Application not found.')
30
+ rescue RestClient::InternalServerError
31
+ abort('An error occured on the platform. Please contact support if this problem persists.')
32
+ rescue StandardError # (e.g. SocketError, Errno::ECONNREFUSED, RestClient::BadGateway, ...)
33
+ abort('There was an issue connecting to the Gleis API server.')
34
+ else
35
+ if resp.body.empty?
36
+ # If there is no body return whole response object
37
+ resp
38
+ else
39
+ # Return Ruby data structure of the JSON parsed body
40
+ JSON.parse(resp.body)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,198 @@
1
+ module Gleis
2
+ # The class implements the methods required to manage gleis applications
3
+ class Application
4
+ def self.config(app_name, env_var)
5
+ token = Token.check
6
+ if env_var.end_with? '='
7
+ # Delete variable
8
+ var_name = env_var.split('=')[0]
9
+ response = API.request('delete', "config/#{app_name}/#{var_name}", token)
10
+ puts "Deleted environment variable #{var_name}" if response.code == 204
11
+ elsif env_var.include? '='
12
+ # Update or create variable
13
+ var_value = env_var.split('=')
14
+ response = API.request('post', 'config', token, 'name': app_name, 'var': var_value[0], 'value': var_value[1])
15
+ puts "Sucessfully created environment variable #{var_value[0]}" if response.code == 201
16
+ puts "Sucessfully updated environment variable #{var_value[0]}" if response.code == 200
17
+ else
18
+ config_body = Config.get_env_vars(app_name, token)
19
+ Utils.output_config_env_vars(config_body['env_vars'], app_name)
20
+ end
21
+ end
22
+
23
+ def self.create(app_name)
24
+ token = Token.check
25
+ print "Creating new app named #{app_name}, you should be ready to go in about 10 seconds ... "
26
+ body = API.request('post', 'create', token, 'name': app_name)
27
+ # Check status of project creation in Gitlab
28
+ if body['success'] == 1
29
+ puts "done!\n"
30
+ git_ssh_url_to_repo = body['ssh_url_to_repo']
31
+ # Add git remote if .git/config file is detected
32
+ if Utils.add_remote_to_git_config(git_ssh_url_to_repo) == false
33
+ puts "Now add the gleis git remote to your app using: git remote add gleis #{git_ssh_url_to_repo}"
34
+ else
35
+ puts 'Git remote gleis added to your git config'
36
+ end
37
+ puts 'When ready to deploy your app, push it to the gleis git remote with: git push gleis master'
38
+ puts "You can then access your app using the URL https://#{body['dns_name']}" if body.key? 'dns_name'
39
+ else
40
+ puts 'failed!'
41
+ end
42
+ end
43
+
44
+ def self.deployments(app_name)
45
+ token = Token.check
46
+ action = 'deployment/' + app_name
47
+ body = API.request('get', action, token)
48
+ if body['deployments'].any?
49
+ body['deployments'].reverse_each do |d|
50
+ puts "v#{d['version']} | #{d['commit'][0, 7]} | #{d['email']} | #{d['subject']}\n" \
51
+ "deployed by #{d['name']} on #{Time.parse(d['created_at']).strftime('%c')}\n\n"
52
+ end
53
+ else
54
+ puts 'No deployments found, please deploy your app first.'
55
+ end
56
+ end
57
+
58
+ def self.destroy(app_name)
59
+ token = Token.check
60
+ if Utils.prompt_confirmation("Are you sure you want to destroy the #{app_name} app?")
61
+ # Delete everything related to project/app
62
+ body = API.request('post', 'delete', token, 'name': app_name)
63
+ if body['success'] == 1
64
+ puts 'App destroyed successfully'
65
+ else
66
+ puts 'Failed to destroy app: ' + body['message']
67
+ end
68
+ else
69
+ puts 'Command cancelled'
70
+ end
71
+ end
72
+
73
+ def self.exec(app_name, command)
74
+ token = Token.check
75
+ config_body = Config.get_env_vars(app_name, token)
76
+ # Get storage and generate Docker mount parameter
77
+ mount_param = Utils.generate_docker_cmd_mount(API.request('get', "storage/#{app_name}", token), app_name)
78
+ # Get deployments and commit from last deployment
79
+ dp_body = API.request('get', "deployment/#{app_name}", token)
80
+ if dp_body['deployments'].any?
81
+ # Get CLI parameters from API server
82
+ p = Params.get_cli_parameters(token)
83
+ system("ssh -t -q -o 'StrictHostKeyChecking=no' \
84
+ -p #{p['run_port']} \
85
+ #{p['run_username']}@#{p['run_server']} \
86
+ '#{Utils.generate_docker_cmd_env_vars(config_body['env_vars'])} #{mount_param} \
87
+ #{p['registry_server']}/#{dp_body['namespace']}/#{app_name}:#{dp_body['deployments'].last['commit'][0..6]} \
88
+ #{command}'")
89
+ else
90
+ puts 'No deployments found, please deploy your app first.'
91
+ end
92
+ end
93
+
94
+ def self.git(app_name)
95
+ token = Token.check
96
+ action = 'git/' + app_name
97
+ body = API.request('get', action, token)
98
+ if body['success'] == 1
99
+ puts "The URL to the git repo of the #{app_name} app is: #{body['data']}"
100
+ else
101
+ puts 'Failed to fetch git URL for app.'
102
+ end
103
+ end
104
+
105
+ def self.logs(app_name)
106
+ token = Token.check
107
+ action = 'logs/' + app_name
108
+ body = API.request('get', action, token)
109
+ if body['log'].nil? || body['log'].empty?
110
+ puts 'No log entries found yet.'
111
+ else
112
+ puts "Last 100 lines of consolidated log output for #{app_name}:"
113
+ puts
114
+ puts body['log']
115
+ end
116
+ end
117
+
118
+ def self.restart(app_name)
119
+ token = Token.check
120
+ body = API.request('post', 'restart', token, 'name': app_name)
121
+ if body['success'] == 1
122
+ puts 'Successfully restarted app using rolling update'
123
+ else
124
+ puts 'Failed to restart app: ' + body['error_text']
125
+ end
126
+ end
127
+
128
+ def self.scale(app_name, replica)
129
+ token = Token.check
130
+ count = Utils.validate_scale_count(replica)
131
+ body = API.request('post', 'scale', token, 'name': app_name, 'count': count)
132
+ if body['success'] == 1
133
+ puts "Successfully scaled app to #{count} replica"
134
+ if body['message']
135
+ puts 'Note that your app is currently not running, hence scaling will take effect as soon as you start it'
136
+ end
137
+ else
138
+ puts "Failed to scale app: #{body['message']}"
139
+ end
140
+ end
141
+
142
+ def self.start(app_name)
143
+ token = Token.check
144
+ body = API.request('post', 'start', token, 'name': app_name)
145
+ if body['success'] == 1
146
+ puts 'Successfully started app'
147
+ else
148
+ puts 'Failed to start app: ' + body['message']
149
+ end
150
+ end
151
+
152
+ def self.ps(app_name)
153
+ token = Token.check
154
+ body = API.request('get', "ps/#{app_name}", token)
155
+ puts 'Failed to get processes.' unless body['success'] == 1
156
+ if body['data']&.size&.positive?
157
+ body['data'].each_with_index do |service, index|
158
+ puts "=== #{service[0]}: `#{service[1]['command']}`"
159
+ if service[1]['tasks'].empty?
160
+ puts 'No processes running'
161
+ else
162
+ Utils.output_tasks(service[1]['tasks'], service[1]['type'])
163
+ end
164
+ puts '' if index != body['data'].size - 1
165
+ end
166
+ else
167
+ puts 'No deployments found, please deploy your app first.'
168
+ end
169
+ end
170
+
171
+ def self.status(app_name)
172
+ token = Token.check
173
+ action = 'status/' + app_name
174
+ body = API.request('get', action, token)
175
+ puts "Status of app #{app_name}:\n\n"
176
+ if body['success'] == 1
177
+ status = body['status']
178
+ puts "\tStatus:\t\trunning\n" \
179
+ "\tStarted at:\t#{Time.parse(status['createdat']).localtime.strftime('%c')}\n" \
180
+ "\tUpdated at:\t#{Time.parse(status['updatedat']).localtime.strftime('%c')}\n" \
181
+ "\tUpdate count:\t#{status['forceupdate']}\n" \
182
+ "\tReplicas:\t#{status['replicas']}"
183
+ else
184
+ puts "\tStatus:\t\tnot running"
185
+ end
186
+ end
187
+
188
+ def self.stop(app_name)
189
+ token = Token.check
190
+ body = API.request('post', 'stop', token, 'name': app_name)
191
+ if body['success'] == 1
192
+ puts 'Succesfully stopped app'
193
+ else
194
+ puts 'Failed to stop app: ' + body['message']
195
+ end
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,47 @@
1
+ module Gleis
2
+ # This class implements all authentication related commands of the CLI
3
+ class Authentication
4
+ def self.login(username)
5
+ puts 'Login to Gleis'
6
+ username = username.downcase
7
+ password = Utils.prompt_password
8
+
9
+ body = API.request('post', 'login', nil, 'username': username, 'password': password)
10
+ token = body['token']
11
+
12
+ Token.save(token)
13
+ puts 'Authentication successful'
14
+ puts "\nNEWS: #{body['data']}\n\n" unless body['data'].nil?
15
+ # Generate SSH key pair if not found and upload pub key
16
+ ssh_key_filename = Config::SSH_KEY_FILE_BASE + '_' + Utils.convert_username_to_filename(username)
17
+ upload_ssh_public_key(ssh_key_filename, token) if SSH.generate_key(ssh_key_filename, username)
18
+ end
19
+
20
+ def self.whoami
21
+ token = Token.check
22
+ body = API.request('get', 'whoami', token)
23
+ puts "You are currently logged in as #{body['data']}"
24
+ end
25
+
26
+ def self.logout
27
+ token = Token.check
28
+ puts 'Logout of Gleis'
29
+ API.request('post', 'logout', token, {})
30
+ Token.delete
31
+ puts 'Logout successful'
32
+ end
33
+
34
+ def self.upload_ssh_public_key(filename, token)
35
+ ssh_public_key = SSH.load_public_key(filename)
36
+ body = API.request('post', 'upload_ssh_pub_key', token, 'ssh_public_key' => ssh_public_key)
37
+ if body['success'] == 1
38
+ puts "Successfully uploaded your SSH public key #{filename}.pub"
39
+ # Get run server name
40
+ params = Params.get_cli_parameters(token)
41
+ SSH.add_host_to_config(body['git_server'], params['run_server'], filename)
42
+ else
43
+ puts "Failed to upload SSH public key: #{body['message']}"
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,23 @@
1
+ module Gleis
2
+ module CLI
3
+ # Add-ons-related CLI commands
4
+ class Addon < Thor
5
+ class_option :app, aliases: :a, type: :string, default: Utils.app_name
6
+
7
+ desc 'add ADDON', 'Add add-on to app'
8
+ def add(name)
9
+ Gleis::Addon.add(options[:app], name)
10
+ end
11
+
12
+ desc 'list', 'List available add-ons on Gleis and add-ons used by app'
13
+ def list
14
+ Gleis::Addon.list(options[:app])
15
+ end
16
+
17
+ desc 'remove ADDON', 'Add add-on to app'
18
+ def remove(name)
19
+ Gleis::Addon.remove(options[:app], name)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,73 @@
1
+ module Gleis
2
+ module CLI
3
+ # Application-related CLI commands
4
+ class App < Thor
5
+ class_option :app, aliases: :a, type: :string, default: Utils.app_name
6
+
7
+ desc 'config [VAR=value]', 'Show, set or delete environment variables'
8
+ def config(env_var = '')
9
+ Application.config(options[:app], env_var)
10
+ end
11
+
12
+ desc 'create', 'Create new application'
13
+ def create
14
+ Application.create(options[:app])
15
+ end
16
+
17
+ desc 'destroy', 'Destroy application'
18
+ def destroy
19
+ Application.destroy(options[:app])
20
+ end
21
+
22
+ desc 'deployments', 'List all deployments'
23
+ def deployments
24
+ Application.deployments(options[:app])
25
+ end
26
+
27
+ desc 'exec COMMAND', 'Execute a one-time-command (e.g. rails db:migrate)'
28
+ def exec(command)
29
+ Application.exec(options[:app], command)
30
+ end
31
+
32
+ desc 'git', 'Show URL of git repository'
33
+ def git
34
+ Application.git(options[:app])
35
+ end
36
+
37
+ desc 'logs', 'View last log entries'
38
+ def logs
39
+ Application.logs(options[:app])
40
+ end
41
+
42
+ desc 'ps', 'Show running processes'
43
+ def ps
44
+ Application.ps(options[:app])
45
+ end
46
+
47
+ desc 'restart', 'Restart application (rolling update)'
48
+ def restart
49
+ Application.restart(options[:app])
50
+ end
51
+
52
+ desc 'scale #REPLICA', 'Scale up or down (horizontal scaling)'
53
+ def scale(replica)
54
+ Application.scale(options[:app], replica)
55
+ end
56
+
57
+ desc 'start', 'Start application'
58
+ def start
59
+ Application.start(options[:app])
60
+ end
61
+
62
+ desc 'status', 'Status of application'
63
+ def status
64
+ Application.status(options[:app])
65
+ end
66
+
67
+ desc 'stop', 'Stop application'
68
+ def stop
69
+ Application.stop(options[:app])
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,21 @@
1
+ module Gleis
2
+ module CLI
3
+ # Authentication-related CLI commands
4
+ class Auth < Thor
5
+ desc 'login USERNAME', 'Login into Gleis'
6
+ def login(username)
7
+ Authentication.login(username)
8
+ end
9
+
10
+ desc 'logout', 'Logout of Gleis'
11
+ def logout
12
+ Authentication.logout
13
+ end
14
+
15
+ desc 'whoami', 'Info on current login'
16
+ def whoami
17
+ Authentication.whoami
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,48 @@
1
+ module Gleis
2
+ module CLI
3
+ # Database-related CLI subcommands
4
+ class Db < Thor
5
+ class_option :app, aliases: :a, type: :string, default: Utils.app_name
6
+
7
+ desc 'backup', 'Backup locally database DATABASE_URL'
8
+ def backup
9
+ Gleis::Database.backup(options[:app])
10
+ end
11
+
12
+ desc 'delete DB_ENV_VAR', 'Delete database configured at variable name'
13
+ def delete(env_var_name)
14
+ Gleis::Database.delete(options[:app], env_var_name)
15
+ end
16
+
17
+ desc 'info', 'Information about database DATABASE_URL'
18
+ def info
19
+ Gleis::Database.info(options[:app])
20
+ end
21
+
22
+ desc 'new', 'Create new database'
23
+ def new
24
+ Gleis::Database.new(options[:app])
25
+ end
26
+
27
+ desc 'promote DB_ENV_VAR', 'Promote database to app using variable name (e.g. DATABASE1_URL)'
28
+ def promote(env_var_name)
29
+ Gleis::Database.promote(options[:app], env_var_name)
30
+ end
31
+
32
+ desc 'psql', 'Connect to PostgreSQL database DATABASE_URL'
33
+ def psql
34
+ Gleis::Database.psql(options[:app])
35
+ end
36
+
37
+ desc 'push LOCAL_DB_NAME', 'Copy local database to remote database DATABASE_URL'
38
+ def push(local_name)
39
+ Gleis::Database.push(options[:app], local_name)
40
+ end
41
+
42
+ desc 'reset DB_ENV_VAR', 'Reset database configured at variable name'
43
+ def reset(env_var_name)
44
+ Gleis::Database.reset(options[:app], env_var_name)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,23 @@
1
+ module Gleis
2
+ module CLI
3
+ # Domain-related CLI commands
4
+ class Domain < Thor
5
+ class_option :app, aliases: :a, type: :string, default: Utils.app_name
6
+
7
+ desc 'add DOMAIN', 'Add domain name to app'
8
+ def add(name)
9
+ Gleis::Domain.add(options[:app], name)
10
+ end
11
+
12
+ desc 'list', 'List existing domain names for app'
13
+ def list
14
+ Gleis::Domain.list(options[:app])
15
+ end
16
+
17
+ desc 'remove DOMAIN', 'Remove domain name from app'
18
+ def remove(name)
19
+ Gleis::Domain.remove(options[:app], name)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,16 @@
1
+ module Gleis
2
+ module CLI
3
+ # Management-related CLI commands
4
+ class Management < Thor
5
+ desc 'apps', 'List all apps for organisation'
6
+ def apps
7
+ Gleis::Management.apps
8
+ end
9
+
10
+ desc 'license', 'Display short license info'
11
+ def license
12
+ Gleis::Management.license
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,23 @@
1
+ module Gleis
2
+ module CLI
3
+ # Sharing-related CLI commands
4
+ class Sharing < Thor
5
+ class_option :app, aliases: :a, type: :string, default: Utils.app_name
6
+
7
+ desc 'add EMAIL ROLE', 'Allow user to access app (roles: collaborator, owner)'
8
+ def add(email, role)
9
+ Gleis::Sharing.add(options[:app], email, role)
10
+ end
11
+
12
+ desc 'list', 'Show access list of application'
13
+ def list
14
+ Gleis::Sharing.list(options[:app])
15
+ end
16
+
17
+ desc 'remove EMAIL', 'Remove user access to app'
18
+ def remove(email)
19
+ Gleis::Sharing.remove(options[:app], email)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,28 @@
1
+ module Gleis
2
+ module CLI
3
+ # Storage-related CLI commands
4
+ class Storage < Thor
5
+ class_option :app, aliases: :a, type: :string, default: Utils.app_name
6
+
7
+ desc 'add TYPE', 'Add storage to app'
8
+ def add(type)
9
+ Gleis::Storage.add(options[:app], type)
10
+ end
11
+
12
+ desc 'attach DIRECTORY', 'Attach storage to directory (e.g. /usr/src/app/public)'
13
+ def attach(dir)
14
+ Gleis::Storage.attach(options[:app], dir)
15
+ end
16
+
17
+ desc 'list', 'List available storage types'
18
+ def list
19
+ Gleis::Storage.list(options[:app])
20
+ end
21
+
22
+ desc 'sync DIRECTORY', 'Synchronise local directory with remote storage'
23
+ def sync(dir)
24
+ Gleis::Storage.sync(options[:app], dir)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,21 @@
1
+ module Gleis
2
+ # Global parameters of gleis gem and app config env variables
3
+ class Config
4
+ # Override default API URL if GLEIS_API_URL env variable is set
5
+ API_URL = ENV['GLEIS_API_URL'].nil? ? 'https://api.basel.gleis.one/' : ENV['GLEIS_API_URL']
6
+ API_VERSION = 'v0'.freeze
7
+ SSH_KEY_FILE_BASE = Dir.home + '/.ssh/gleis'
8
+ TOKEN_FILE = Dir.home + '/.gleis-token'
9
+
10
+ def self.get_env_vars(app_name, token)
11
+ body = API.request('get', "config/#{app_name}", token)
12
+ abort("Failed to get app environment variables: #{body['error_text']}") if body['success'] != 1
13
+ body
14
+ end
15
+
16
+ def self.get_env_var(app_name, token, var_name)
17
+ body = API.request('get', "config/#{app_name}/#{var_name}", token)
18
+ return body['data'] if body['success'] == 1
19
+ end
20
+ end
21
+ end