uffizzi-cli 0.1.4.3 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 22ba7aad922d83a3168cba4c93f476c334359ca83663f6fd9e10a9bbface0ae9
4
- data.tar.gz: e32af9850d0dca8b4ba93675cd5f19d4c4fcdf8b86df5e4231125aa56ef63341
3
+ metadata.gz: 2edd95fc4165e75f533b8925b92031b83baa0db249e78234df4afff63ac1f509
4
+ data.tar.gz: 856ce13ac1dbf34a84f8304456c3226286825a2626661e69ded2953e12fa6438
5
5
  SHA512:
6
- metadata.gz: 013051664b8bdf93ed677b3ceff71053f283800dc4493f98c1326de0a85db99ba2e58217d8c6a16ec3502c7cf4898a3612208f2343ec177d7f44eae32ddb8d63
7
- data.tar.gz: a1054b99e188dbbb6e13c5d19dec8134996386a42730dd5f2d9af857fc1b05b02cca00543e0c9158942a61d1b5e51a1d483f60c16ff74a4adca3f924d6819ed9
6
+ metadata.gz: 8ac95ad20cef50b6a72c0cc67c7faaf92135f054e32dd5f5aaa7436b4545459d057a7a93e12d720cec2b455ceac5c9603414034f496dc3bd4fc9e9b5a190ab4e
7
+ data.tar.gz: 457220d0efead268142c44dcb4a9b6fe92b4f26428191618212ae133a46aad57087649061077341befcdf71d8025dd9ef99b48f228b7e4c0032eba2ba4a048e6
data/.rubocop.yml CHANGED
@@ -23,6 +23,9 @@ Style/AsciiComments:
23
23
  StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-comments'
24
24
  Enabled: false
25
25
 
26
+ Style/OptionalBooleanParameter:
27
+ Enabled: false
28
+
26
29
  Naming/AsciiIdentifiers:
27
30
  Description: 'Use only ascii symbols in identifiers.'
28
31
  StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-identifiers'
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- uffizzi-cli (0.1.4.3)
4
+ uffizzi-cli (0.2.2)
5
5
  thor
6
6
 
7
7
  GEM
@@ -78,6 +78,9 @@ GEM
78
78
  rubocop (~> 1.0)
79
79
  ruby-progressbar (1.11.0)
80
80
  thor (1.2.1)
81
+ tty-cursor (0.7.1)
82
+ tty-spinner (0.9.3)
83
+ tty-cursor (~> 0.7)
81
84
  tzinfo (2.0.4)
82
85
  concurrent-ruby (~> 1.0)
83
86
  unicode (0.4.4.4)
@@ -106,6 +109,7 @@ DEPENDENCIES
106
109
  rubocop
107
110
  rubocop-minitest
108
111
  rubocop-rake
112
+ tty-spinner
109
113
  uffizzi-cli!
110
114
  webmock
111
115
 
data/README.md CHANGED
@@ -1,31 +1,31 @@
1
- # Uffizzi CLI
1
+ # Uffizzi CLI
2
2
 
3
- A command-line interace (CLI) for [Uffizzi App](https://github.com/UffizziCloud/uffizzi_app)
3
+ A command-line interace (CLI) for [Uffizzi App](https://github.com/UffizziCloud/uffizzi_app)
4
4
 
5
5
  ## Uffizzi Overview
6
6
 
7
- Uffizzi is the Full-stack Previews Engine that makes it easy for your team to preview code changes before merging—whether frontend, backend or microserivce. Define your full-stack apps with a familiar syntax based on Docker Compose, and Uffizzi will create on-demand test environments when you open pull requests or build new images. Preview URLs are updated when there’s a new commit, so your team can catch issues early, iterate quickly, and accelerate your release cycles.
7
+ Uffizzi is the Full-stack Previews Engine that makes it easy for your team to preview code changes before merging—whether frontend, backend or microserivce. Define your full-stack apps with a familiar syntax based on Docker Compose, and Uffizzi will create on-demand test environments when you open pull requests or build new images. Preview URLs are updated when there’s a new commit, so your team can catch issues early, iterate quickly, and accelerate your release cycles.
8
8
 
9
- ## Getting started with Uffizzi
9
+ ## Getting started with Uffizzi
10
10
 
11
- The fastest and easiest way to get started with Uffizzi is via the fully hosted version available at https://uffizzi.com, which includes free plans for small teams and qualifying open-source projects.
11
+ The fastest and easiest way to get started with Uffizzi is via the fully hosted version available at https://uffizzi.com, which includes free plans for small teams and qualifying open-source projects.
12
12
 
13
13
  Alternatively, you can self-host Uffizzi via the open-source repositories available here on GitHub. The remainder of this README is intended for users interested in self-hosting Uffizzi or for those who are just curious about how Uffizzi works.
14
14
 
15
- ## Uffizzi Architecture
15
+ ## Uffizzi Architecture
16
16
 
17
- Uffizzi consists of the following components:
17
+ Uffizzi consists of the following components:
18
18
 
19
- * [Uffizzi App](https://github.com/UffizziCloud/uffizzi_app) - The primary REST API for creating and managing Previews
20
- * [Uffizzi Controller](https://github.com/UffizziCloud/uffizzi_controller) - A smart proxy service that handles requests from Uffizzi App to the Kubernetes API
21
- * Uffizzi CLI (this repository) - A command-line interface for Uffizzi App
22
- * [Uffizzi Dashboard](https://app.uffizzi.com) - A graphical user interface for Uffizzi App, available as a paid service at https://uffizzi.com
19
+ - [Uffizzi App](https://github.com/UffizziCloud/uffizzi_app) - The primary REST API for creating and managing Previews
20
+ - [Uffizzi Controller](https://github.com/UffizziCloud/uffizzi_controller) - A smart proxy service that handles requests from Uffizzi App to the Kubernetes API
21
+ - Uffizzi CLI (this repository) - A command-line interface for Uffizzi App
22
+ - [Uffizzi Dashboard](https://app.uffizzi.com) - A graphical user interface for Uffizzi App, available as a paid service at https://uffizzi.com
23
23
 
24
- To host Uffizzi yourself, you will also need the following external dependencies:
24
+ To host Uffizzi yourself, you will also need the following external dependencies:
25
25
 
26
- * Kubernetes (k8s) cluster
27
- * Postgres database
28
- * Redis cache
26
+ - Kubernetes (k8s) cluster
27
+ - Postgres database
28
+ - Redis cache
29
29
 
30
30
  ## Installation
31
31
 
@@ -54,40 +54,81 @@ Run rubocop:
54
54
 
55
55
  ## Commands
56
56
 
57
- ### login ###
57
+ ### login
58
58
 
59
59
  ```
60
- $ uffizzi login -u your@email.com -h localhost:8080
60
+ $ uffizzi login -u your@email.com --hostname localhost:8080
61
61
  ```
62
- Logging you into the app which you set in the hostname option.
63
62
 
63
+ Logging you into the app which you set in the hostname option.
64
64
 
65
- ### login options ###
65
+ ### login options
66
66
 
67
- Option | Aliase | Description
68
- ------- | ------- | -----------
69
- `--user` | `-u` | Your email for logging in
70
- `--hostname`| `-h` | Adress of your app
67
+ | Option | Aliase | Description |
68
+ | ------------ | ------ | ------------------------- |
69
+ | `--user` | `-u` | Your email for logging in |
70
+ | `--hostname` | | Adress of your app |
71
71
 
72
72
  If hostname uses basic authentication you can specify options for it by setting `basic_auth_user` and `basic_auth_password` via `config set` command.
73
73
 
74
- ### projects ###
74
+ ### project
75
75
 
76
76
  ```
77
- $ uffizzi projects
77
+ $ uffizzi project
78
+ ```
79
+
80
+ Use this command to configure your projects. This command has 2 subcommands `list` and `compose`.
81
+
82
+ ```
83
+ $ uffizzi project list
78
84
  ```
79
85
 
80
86
  Shows all your projects' slugs
81
87
 
82
88
  If you have only one project it will be added to your config file automatically, if there's more than one project you need to set up your project manually with the command `uffizzi config set YOUR_PROJECT_SLUG`
83
89
 
84
- ### config ###
90
+ ### compose
91
+
92
+ ```
93
+ $ uffizzi project compose
94
+ ```
95
+
96
+ That's the subcommand for project command. Use it to configure your compose file. This command has 3 subcommands `set`, `describe` and `unset`.
97
+
98
+ ```
99
+ $ uffizzi project compose set -f path_to_your_compose_file.yml
100
+ ```
101
+
102
+ Creates a new or updates existed compose file in uffizzi app for project specified in config file
103
+
104
+ ```
105
+ $ uffizzi project compose describe
106
+ ```
107
+
108
+ Shows the content of compose file related to project specified in config file if it's valid or validation errors if it's not
109
+
110
+ ```
111
+ $ uffizzi project compose unset
112
+ ```
113
+
114
+ Removes compose file related to project specified in config file
115
+
116
+ You need to set project before use any of these commands via `uffizzi config set project YOUR_PROJECT_SLUG` command
117
+
118
+ ### compose options
119
+
120
+ | Option | Aliase | Description |
121
+ | -------- | ------ | ------------------------- |
122
+ | `--file` | `-f` | Path to your compose file |
123
+
124
+ ### config
85
125
 
86
126
  Use this command to configure your cli app. This command has 4 subcommands `list`, `get`, `set`, and `delete`.
87
127
 
88
128
  ```
89
129
  $ uffizzi config list
90
130
  ```
131
+
91
132
  Shows all options and their values from the config file.
92
133
 
93
134
  ```
data/config/config.rb ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ostruct'
4
+
5
+ module Uffizzi
6
+ def self.configuration
7
+ @configuration ||= OpenStruct.new
8
+ end
9
+
10
+ def self.configure
11
+ yield(configuration)
12
+ end
13
+
14
+ configure do |config|
15
+ config.hostname = 'http://web:7000'
16
+ end
17
+ end
@@ -9,6 +9,11 @@ module Uffizzi
9
9
  ConfigFile.option_exists?(:cookie) &&
10
10
  ConfigFile.option_exists?(:hostname)
11
11
  end
12
+
13
+ def project_set?
14
+ ConfigFile.exists? &&
15
+ ConfigFile.option_exists?(:project)
16
+ end
12
17
  end
13
18
  end
14
19
  end
@@ -2,12 +2,35 @@
2
2
 
3
3
  require 'io/console'
4
4
  require 'uffizzi'
5
+ require 'uffizzi/clients/api/api_client'
5
6
 
6
7
  module Uffizzi
7
- class Config
8
+ class CLI::Config < Thor
8
9
  include ApiClient
9
10
 
10
- def run(command, property, value)
11
+ desc 'list', 'list'
12
+ def list
13
+ run('list')
14
+ end
15
+
16
+ desc 'get', 'get'
17
+ def get(property)
18
+ run('get', property)
19
+ end
20
+
21
+ desc 'set', 'set'
22
+ def set(property, value)
23
+ run('set', property, value)
24
+ end
25
+
26
+ desc 'delete', 'delete'
27
+ def delete(property)
28
+ run('delete', property)
29
+ end
30
+
31
+ private
32
+
33
+ def run(command, property = nil, value = nil)
11
34
  case command
12
35
  when 'list'
13
36
  handle_list_command
@@ -17,40 +40,25 @@ module Uffizzi
17
40
  handle_set_command(property, value)
18
41
  when 'delete'
19
42
  handle_delete_command(property)
20
- else
21
- puts "#{command} is not a uffizzi config command"
22
43
  end
23
44
  end
24
45
 
25
- private
26
-
27
46
  def handle_list_command
28
47
  ConfigFile.list
29
48
  end
30
49
 
31
50
  def handle_get_command(property)
32
- if property.nil?
33
- puts 'No property provided'
34
- return
35
- end
36
51
  option = ConfigFile.read_option(property.to_sym)
37
- puts option unless option.nil?
52
+ message = option.nil? ? "The option #{property} doesn't exist in config file" : option
53
+
54
+ Uffizzi.ui.say(message)
38
55
  end
39
56
 
40
57
  def handle_set_command(property, value)
41
- if property.nil? || value.nil?
42
- puts 'No property provided' if property.nil?
43
- puts 'No value provided' if value.nil?
44
- return
45
- end
46
58
  ConfigFile.write_option(property.to_sym, value)
47
59
  end
48
60
 
49
61
  def handle_delete_command(property)
50
- if property.nil?
51
- puts 'No property provided'
52
- return
53
- end
54
62
  ConfigFile.delete_option(property.to_sym)
55
63
  end
56
64
  end
@@ -3,6 +3,7 @@
3
3
  require 'io/console'
4
4
  require 'uffizzi'
5
5
  require 'uffizzi/response_helper'
6
+ require 'uffizzi/clients/api/api_client'
6
7
 
7
8
  module Uffizzi
8
9
  class CLI::Login
@@ -17,10 +18,10 @@ module Uffizzi
17
18
  params = prepare_request_params(password)
18
19
  response = create_session(@options[:hostname], params)
19
20
 
20
- if Uffizzi::ResponseHelper.created?(response)
21
+ if ResponseHelper.created?(response)
21
22
  handle_succeed_response(response)
22
23
  else
23
- handle_failed_response(response)
24
+ ResponseHelper.handle_failed_response(response)
24
25
  end
25
26
  end
26
27
 
@@ -35,10 +36,6 @@ module Uffizzi
35
36
  }
36
37
  end
37
38
 
38
- def handle_failed_response(response)
39
- print_errors(response[:body][:errors])
40
- end
41
-
42
39
  def handle_succeed_response(response)
43
40
  account = response[:body][:user][:accounts].first
44
41
  return Uffizzi.ui.say('No account related to this email') unless account_valid?(account)
@@ -0,0 +1,267 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uffizzi'
4
+ require 'tty-spinner'
5
+ require 'uffizzi/auth_helper'
6
+
7
+ module Uffizzi
8
+ class CLI::Preview < Thor
9
+ include ApiClient
10
+
11
+ @spinner
12
+
13
+ class << self
14
+ def help(_shell, _subcommand)
15
+ Cli::Common.show_manual(:preview)
16
+ end
17
+ end
18
+
19
+ desc 'list', 'list'
20
+ def list
21
+ return Cli::Common.show_manual(:list) if options[:help]
22
+
23
+ run(options, 'list', nil, nil)
24
+ end
25
+
26
+ desc 'create', 'create'
27
+ def create(file_path = nil)
28
+ return Cli::Common.show_manual(:create) if options[:help]
29
+
30
+ run(options, 'create', file_path, nil)
31
+ end
32
+
33
+ desc 'delete', 'delete'
34
+ def delete(deployment)
35
+ return Cli::Common.show_manual(:delete) if options[:help]
36
+
37
+ run(options, 'delete', nil, deployment)
38
+ end
39
+
40
+ desc 'describe', 'describe'
41
+ def describe(deployment)
42
+ return Cli::Common.show_manual(:describe) if options[:help]
43
+
44
+ run(options, 'describe', nil, deployment)
45
+ end
46
+
47
+ private
48
+
49
+ def run(options, command, file_path, deployment)
50
+ return Uffizzi.ui.say('You are not logged in.') unless Uffizzi::AuthHelper.signed_in?
51
+ return Uffizzi.ui.say('This command needs project to be set in config file') unless Uffizzi::AuthHelper.project_set?
52
+
53
+ project_slug = options[:project].nil? ? ConfigFile.read_option(:project) : options[:project]
54
+
55
+ case command
56
+ when 'list'
57
+ handle_list_command(project_slug)
58
+ when 'create'
59
+ handle_create_command(file_path, project_slug)
60
+ when 'delete'
61
+ handle_delete_command(deployment, project_slug)
62
+ when 'describe'
63
+ handle_describe_command(deployment, project_slug)
64
+ end
65
+ end
66
+
67
+ def handle_list_command(project_slug)
68
+ hostname = ConfigFile.read_option(:hostname)
69
+ response = fetch_deployments(hostname, project_slug)
70
+
71
+ if ResponseHelper.ok?(response)
72
+ handle_succeed_list_response(response)
73
+ else
74
+ ResponseHelper.handle_failed_response(response)
75
+ end
76
+ end
77
+
78
+ def handle_create_command(file_path, project_slug)
79
+ hostname = ConfigFile.read_option(:hostname)
80
+ params = file_path.nil? ? {} : prepare_params(file_path)
81
+ response = create_deployment(hostname, project_slug, params)
82
+
83
+ if ResponseHelper.created?(response)
84
+ handle_succeed_create_response(hostname, project_slug, response)
85
+ else
86
+ ResponseHelper.handle_failed_response(response)
87
+ end
88
+ end
89
+
90
+ def handle_succeed_create_response(hostname, project_slug, response)
91
+ deployment = response[:body][:deployment]
92
+ deployment_id = deployment[:id]
93
+ params = { id: deployment_id }
94
+
95
+ response = deploy_containers(hostname, project_slug, deployment_id, params)
96
+
97
+ if ResponseHelper.no_content?(response)
98
+ Uffizzi.ui.say("Preview created with name deployment-#{deployment_id}")
99
+ print_deployment_progress(hostname, deployment, project_slug)
100
+ else
101
+ ResponseHelper.handle_failed_response(response)
102
+ end
103
+ end
104
+
105
+ def print_deployment_progress(hostname, deployment, project_slug)
106
+ deployment_id = deployment[:id]
107
+
108
+ @spinner = TTY::Spinner.new('[:spinner] Creating containers...', format: :dots)
109
+ @spinner.auto_spin
110
+
111
+ activity_items = []
112
+
113
+ loop do
114
+ response = get_activity_items(hostname, project_slug, deployment_id)
115
+ handle_activity_items_response(response)
116
+ return unless @spinner.spinning?
117
+
118
+ activity_items = response[:body][:activity_items]
119
+ break unless activity_items.empty?
120
+
121
+ sleep(5)
122
+ end
123
+
124
+ @spinner.success
125
+
126
+ Uffizzi.ui.say('Done')
127
+
128
+ @spinner = TTY::Spinner::Multi.new('[:spinner] Deploying preview...', format: :dots, style: {
129
+ middle: ' ',
130
+ bottom: ' ',
131
+ })
132
+
133
+ containers_spinners = create_containers_spinners(activity_items)
134
+
135
+ loop do
136
+ response = get_activity_items(hostname, project_slug, deployment_id)
137
+ handle_activity_items_response(response)
138
+ return if @spinner.done?
139
+
140
+ activity_items = response[:body][:activity_items]
141
+ check_activity_items_state(activity_items, containers_spinners)
142
+ break if activity_items.all? { |activity_item| activity_item[:state] == 'deployed' || activity_item[:state] == 'failed' }
143
+
144
+ sleep(5)
145
+ end
146
+
147
+ Uffizzi.ui.say('Done')
148
+ preview_url = "http://#{deployment[:preview_url]}"
149
+ Uffizzi.ui.say(preview_url) if @spinner.success?
150
+ end
151
+
152
+ def create_containers_spinners(activity_items)
153
+ activity_items.map do |activity_item|
154
+ container_spinner = @spinner.register("[:spinner] #{activity_item[:name]}")
155
+ container_spinner.auto_spin
156
+ {
157
+ name: activity_item[:name],
158
+ spinner: container_spinner,
159
+ }
160
+ end
161
+ end
162
+
163
+ def check_activity_items_state(activity_items, containers_spinners)
164
+ finished_activity_items = activity_items.filter do |activity_item|
165
+ activity_item[:state] == 'deployed' || activity_item[:state] == 'failed'
166
+ end
167
+ finished_activity_items.each do |activity_item|
168
+ container_spinner = containers_spinners.detect { |spinner| spinner[:name] == activity_item[:name] }
169
+ spinner = container_spinner[:spinner]
170
+ case activity_item[:state]
171
+ when 'deployed'
172
+ spinner.success
173
+ when 'failed'
174
+ spinner.error
175
+ end
176
+ end
177
+ end
178
+
179
+ def handle_delete_command(deployment, project_slug)
180
+ return Uffizzi.ui.say("Preview should be specified in 'deployment-PREVIEW_ID' format") unless deployment_name_valid?(deployment)
181
+
182
+ hostname = ConfigFile.read_option(:hostname)
183
+ deployment_id = deployment.split('-').last
184
+
185
+ response = delete_deployment(hostname, project_slug, deployment_id)
186
+
187
+ if ResponseHelper.no_content?(response)
188
+ handle_succeed_delete_response(deployment_id)
189
+ else
190
+ ResponseHelper.handle_failed_response(response)
191
+ end
192
+ end
193
+
194
+ def handle_describe_command(deployment, project_slug)
195
+ return Uffizzi.ui.say("Preview should be specified in 'deployment-PREVIEW_ID' format") unless deployment_name_valid?(deployment)
196
+
197
+ hostname = ConfigFile.read_option(:hostname)
198
+ deployment_id = deployment.split('-').last
199
+
200
+ response = describe_deployment(hostname, project_slug, deployment_id)
201
+
202
+ if ResponseHelper.ok?(response)
203
+ handle_succeed_describe_response(response)
204
+ else
205
+ ResponseHelper.handle_failed_response(response)
206
+ end
207
+ end
208
+
209
+ def handle_activity_items_response(response)
210
+ unless ResponseHelper.ok?(response)
211
+ @spinner.error
212
+ ResponseHelper.handle_failed_response(response)
213
+ end
214
+ end
215
+
216
+ def handle_succeed_list_response(response)
217
+ deployments = response[:body][:deployments] || []
218
+ return Uffizzi.ui.say('The project has no active deployments') if deployments.empty?
219
+
220
+ deployments.each do |deployment|
221
+ Uffizzi.ui.say("deployment-#{deployment[:id]}")
222
+ end
223
+ end
224
+
225
+ def handle_succeed_delete_response(deployment_id)
226
+ Uffizzi.ui.say("Preview deployment-#{deployment_id} deleted")
227
+ end
228
+
229
+ def handle_succeed_describe_response(response)
230
+ deployment = response[:body][:deployment]
231
+ deployment.each_key do |key|
232
+ Uffizzi.ui.say("#{key}: #{deployment[key]}")
233
+ end
234
+ end
235
+
236
+ def prepare_params(file_path)
237
+ begin
238
+ compose_file_data = File.read(file_path)
239
+ rescue Errno::ENOENT => e
240
+ Uffizzi.ui.say(e)
241
+ return
242
+ end
243
+
244
+ compose_file_dir = File.dirname(file_path)
245
+ dependencies = ComposeFileService.parse(compose_file_data, compose_file_dir)
246
+ absolute_path = File.absolute_path(file_path)
247
+ compose_file_params = {
248
+ path: absolute_path,
249
+ content: Base64.encode64(compose_file_data),
250
+ source: absolute_path,
251
+ }
252
+
253
+ {
254
+ compose_file: compose_file_params,
255
+ dependencies: dependencies,
256
+ }
257
+ end
258
+
259
+ def deployment_name_valid?(deployment)
260
+ return false unless deployment.start_with?('deployment-')
261
+ return false unless deployment.split('-').size == 2
262
+
263
+ deployment_id = deployment.split('-').last
264
+ deployment_id.to_i.to_s == deployment_id
265
+ end
266
+ end
267
+ end