uffizzi-cli 1.0.5 → 2.0.27

Sign up to get free protection for your applications and to get access to all the features.
Files changed (156) hide show
  1. checksums.yaml +4 -4
  2. data/config/uffizzi.rb +1 -0
  3. data/lib/uffizzi/auth_helper.rb +13 -5
  4. data/lib/uffizzi/cli/account.rb +122 -0
  5. data/lib/uffizzi/cli/cluster.rb +363 -0
  6. data/lib/uffizzi/cli/common.rb +2 -4
  7. data/lib/uffizzi/cli/config.rb +5 -4
  8. data/lib/uffizzi/cli/connect.rb +14 -8
  9. data/lib/uffizzi/cli/disconnect.rb +1 -1
  10. data/lib/uffizzi/cli/login.rb +130 -66
  11. data/lib/uffizzi/cli/login_by_identity_token.rb +11 -7
  12. data/lib/uffizzi/cli/preview/service.rb +1 -1
  13. data/lib/uffizzi/cli/preview.rb +33 -32
  14. data/lib/uffizzi/cli/project.rb +17 -9
  15. data/lib/uffizzi/cli.rb +46 -7
  16. data/lib/uffizzi/clients/api/api_client.rb +92 -2
  17. data/lib/uffizzi/clients/api/api_routes.rb +44 -0
  18. data/lib/uffizzi/clients/api/http_client.rb +19 -7
  19. data/lib/uffizzi/config_file.rb +15 -40
  20. data/lib/uffizzi/error.rb +15 -0
  21. data/lib/uffizzi/helpers/config_helper.rb +49 -0
  22. data/lib/uffizzi/helpers/file_helper.rb +25 -0
  23. data/lib/uffizzi/helpers/login_helper.rb +33 -0
  24. data/lib/uffizzi/helpers/project_helper.rb +19 -0
  25. data/lib/uffizzi/promt.rb +4 -0
  26. data/lib/uffizzi/response_helper.rb +33 -14
  27. data/lib/uffizzi/services/cluster_service.rb +59 -0
  28. data/lib/uffizzi/services/compose_file_service.rb +5 -4
  29. data/lib/uffizzi/services/env_variables_service.rb +1 -1
  30. data/lib/uffizzi/services/github_service.rb +21 -0
  31. data/lib/uffizzi/services/kubeconfig_service.rb +132 -0
  32. data/lib/uffizzi/services/preview_service.rb +52 -18
  33. data/lib/uffizzi/shell.rb +19 -16
  34. data/lib/uffizzi/token.rb +37 -0
  35. data/lib/uffizzi/version.rb +1 -1
  36. data/lib/uffizzi.rb +10 -0
  37. data/man/uffizzi +7 -5
  38. data/man/uffizzi-account +29 -0
  39. data/man/uffizzi-account-list +26 -0
  40. data/man/uffizzi-account-list.html +103 -0
  41. data/man/uffizzi-account-list.ronn +19 -0
  42. data/man/uffizzi-account-set-default +29 -0
  43. data/man/uffizzi-account-set-default.html +105 -0
  44. data/man/uffizzi-account-set-default.ronn +22 -0
  45. data/man/uffizzi-account.html +106 -0
  46. data/man/uffizzi-account.ronn +23 -0
  47. data/man/uffizzi-cluster +37 -0
  48. data/man/uffizzi-cluster-create +49 -0
  49. data/man/uffizzi-cluster-create.ronn +41 -0
  50. data/man/uffizzi-cluster-delete +32 -0
  51. data/man/uffizzi-cluster-delete.ronn +24 -0
  52. data/man/uffizzi-cluster-describe +39 -0
  53. data/man/uffizzi-cluster-describe.ronn +30 -0
  54. data/man/uffizzi-cluster-list +34 -0
  55. data/man/uffizzi-cluster-list.ronn +26 -0
  56. data/man/uffizzi-cluster-update-kubeconfig +37 -0
  57. data/man/uffizzi-cluster-update-kubeconfig.ronn +29 -0
  58. data/man/uffizzi-cluster.ronn +32 -0
  59. data/man/uffizzi-compose +39 -0
  60. data/man/uffizzi-compose-create +67 -0
  61. data/man/uffizzi-compose-create.ronn +57 -0
  62. data/man/uffizzi-compose-delete +38 -0
  63. data/man/uffizzi-compose-delete.ronn +29 -0
  64. data/man/uffizzi-compose-describe +38 -0
  65. data/man/uffizzi-compose-describe.ronn +29 -0
  66. data/man/uffizzi-compose-events +38 -0
  67. data/man/uffizzi-compose-events.ronn +29 -0
  68. data/man/uffizzi-compose-list +58 -0
  69. data/man/uffizzi-compose-list.ronn +49 -0
  70. data/man/uffizzi-compose-service-list +42 -0
  71. data/man/uffizzi-compose-service-list.ronn +32 -0
  72. data/man/uffizzi-compose-service-logs +59 -0
  73. data/man/uffizzi-compose-service-logs.ronn +48 -0
  74. data/man/uffizzi-compose-update +61 -0
  75. data/man/uffizzi-compose-update.ronn +51 -0
  76. data/man/uffizzi-compose.ronn +33 -0
  77. data/man/uffizzi-compose_service_logs +59 -0
  78. data/man/uffizzi-compose_service_logs.ronn +50 -0
  79. data/man/uffizzi-config +2 -2
  80. data/man/uffizzi-config.ronn +1 -1
  81. data/man/uffizzi-connect +2 -4
  82. data/man/uffizzi-connect-acr +2 -2
  83. data/man/uffizzi-connect-acr.ronn +1 -1
  84. data/man/uffizzi-connect-docker-hub +2 -2
  85. data/man/uffizzi-connect-docker-hub.ronn +1 -1
  86. data/man/uffizzi-connect-docker-registry +2 -2
  87. data/man/uffizzi-connect-docker-registry.ronn +1 -1
  88. data/man/uffizzi-connect-ecr +2 -2
  89. data/man/uffizzi-connect-ecr.ronn +1 -1
  90. data/man/uffizzi-connect-gcr +2 -2
  91. data/man/uffizzi-connect-gcr.ronn +1 -1
  92. data/man/uffizzi-connect-ghcr +2 -2
  93. data/man/uffizzi-connect-ghcr.ronn +1 -1
  94. data/man/uffizzi-connect.ronn +2 -2
  95. data/man/uffizzi-disconnect +2 -2
  96. data/man/uffizzi-disconnect.ronn +1 -1
  97. data/man/uffizzi-login +7 -3
  98. data/man/uffizzi-login-by-identity-token +3 -3
  99. data/man/uffizzi-login-by-identity-token.ronn +2 -2
  100. data/man/uffizzi-login.html +113 -0
  101. data/man/uffizzi-login.ronn +6 -2
  102. data/man/uffizzi-logout +2 -2
  103. data/man/uffizzi-logout.ronn +1 -1
  104. data/man/uffizzi-preview +9 -9
  105. data/man/uffizzi-preview-create +22 -20
  106. data/man/uffizzi-preview-create.ronn +28 -26
  107. data/man/uffizzi-preview-delete +11 -10
  108. data/man/uffizzi-preview-delete.ronn +12 -11
  109. data/man/uffizzi-preview-describe +10 -10
  110. data/man/uffizzi-preview-describe.ronn +11 -11
  111. data/man/uffizzi-preview-events +12 -11
  112. data/man/uffizzi-preview-events.ronn +13 -12
  113. data/man/uffizzi-preview-list +19 -18
  114. data/man/uffizzi-preview-list.ronn +21 -20
  115. data/man/uffizzi-preview-service-list +16 -12
  116. data/man/uffizzi-preview-service-list.ronn +16 -13
  117. data/man/uffizzi-preview-service-logs +14 -12
  118. data/man/uffizzi-preview-service-logs.ronn +18 -17
  119. data/man/uffizzi-preview-update +19 -18
  120. data/man/uffizzi-preview-update.ronn +21 -20
  121. data/man/uffizzi-preview.ronn +10 -10
  122. data/man/uffizzi-preview_service_logs +14 -12
  123. data/man/uffizzi-preview_service_logs.ronn +18 -17
  124. data/man/uffizzi-project +3 -3
  125. data/man/uffizzi-project-compose +2 -2
  126. data/man/uffizzi-project-compose-describe +2 -2
  127. data/man/uffizzi-project-compose-describe.ronn +1 -1
  128. data/man/uffizzi-project-compose-set +2 -2
  129. data/man/uffizzi-project-compose-set.ronn +1 -1
  130. data/man/uffizzi-project-compose-unset +2 -2
  131. data/man/uffizzi-project-compose-unset.ronn +1 -1
  132. data/man/uffizzi-project-compose.ronn +1 -1
  133. data/man/uffizzi-project-create +2 -2
  134. data/man/uffizzi-project-create.ronn +1 -1
  135. data/man/uffizzi-project-delete +2 -2
  136. data/man/uffizzi-project-delete.ronn +1 -1
  137. data/man/uffizzi-project-describe +3 -3
  138. data/man/uffizzi-project-describe.ronn +1 -1
  139. data/man/uffizzi-project-preview-describe +37 -0
  140. data/man/uffizzi-project-preview-describe.ronn +29 -0
  141. data/man/uffizzi-project-preview-set +66 -0
  142. data/man/uffizzi-project-preview-set.ronn +57 -0
  143. data/man/uffizzi-project-secret +2 -2
  144. data/man/uffizzi-project-secret-create +2 -2
  145. data/man/uffizzi-project-secret-create.ronn +1 -1
  146. data/man/uffizzi-project-secret-delete +2 -2
  147. data/man/uffizzi-project-secret-delete.ronn +1 -1
  148. data/man/uffizzi-project-secret-list +2 -2
  149. data/man/uffizzi-project-secret-list.ronn +1 -1
  150. data/man/uffizzi-project-secret.ronn +1 -1
  151. data/man/uffizzi-project-set-default +2 -2
  152. data/man/uffizzi-project-set-default.ronn +1 -1
  153. data/man/uffizzi-project.html +124 -0
  154. data/man/uffizzi-project.ronn +2 -2
  155. data/man/uffizzi.ronn +12 -10
  156. metadata +134 -22
@@ -27,28 +27,47 @@ module Uffizzi
27
27
  response[:code] == Net::HTTPOK
28
28
  end
29
29
 
30
- def handle_failed_response(response)
31
- prepared_errors = prepare_errors(response[:body][:errors])
32
- raise Uffizzi::Error.new(prepared_errors)
30
+ def handle_failed_response(response, addtional_errors = [])
31
+ errors = get_response_errors(response)
32
+ common_msg = errors_to_string(errors)
33
+ addtional_msg = errors_to_string(addtional_errors)
34
+ message = [common_msg, addtional_msg]
35
+ .reject { |m| m == '' }
36
+ .join("\n")
37
+ .concat("\n")
38
+
39
+ raise Uffizzi::ServerResponseError.new(message)
33
40
  end
34
41
 
35
42
  def handle_invalid_compose_response(response)
36
- prepared_errors = prepare_errors(response[:body][:compose_file][:payload][:errors])
37
- raise Uffizzi::Error.new(prepared_errors)
43
+ message = errors_to_string(response[:body][:compose_file][:payload][:errors])
44
+ raise Uffizzi::ServerResponseError.new(message)
45
+ end
46
+
47
+ def get_response_errors(response)
48
+ return response if response.is_a?(Array)
49
+ return [response.to_s] unless response.is_a?(Hash)
50
+
51
+ body = response.fetch(:body, response.to_s)
52
+ return [body.to_s] unless body.is_a?(Hash)
53
+
54
+ errors = body.fetch(:errors, body.to_s)
55
+ return [errors.to_s] unless [Hash, Array].include?(errors.class)
56
+
57
+ errors
38
58
  end
39
59
 
40
60
  private
41
61
 
42
- def prepare_errors(errors)
43
- errors.values.reduce('') do |acc, error_messages|
44
- if error_messages.is_a?(Array)
45
- error_messages.each { |error_message| acc = "#{acc}#{prepare_error_message(error_message)}\n" }
46
- else
47
- acc = "#{acc}#{prepare_error_message(error_message)}\n"
48
- end
62
+ def errors_to_string(errors)
63
+ return errors.join("\n") if errors.is_a?(Array)
64
+ return errors.to_s unless errors.is_a?(Hash)
49
65
 
50
- acc
51
- end
66
+ errors
67
+ .values
68
+ .flatten
69
+ .map { |msg| prepare_error_message(msg.to_s) }
70
+ .join("\n")
52
71
  end
53
72
 
54
73
  def prepare_error_message(error_message)
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uffizzi/clients/api/api_client'
4
+
5
+ class ClusterService
6
+ CLUSTER_STATE_DEPLOYING_NAMESPACE = 'deploying_namespace'
7
+ CLUSTER_STATE_DEPLOYING = 'deploying'
8
+ CLUSTER_STATE_DEPLOYED = 'deployed'
9
+ CLUSTER_STATE_FAILED_DEPLOY_NAMESPACE = 'failed_deploy_namespace'
10
+ CLUSTER_STATE_FAILED = 'failed'
11
+
12
+ class << self
13
+ include ApiClient
14
+
15
+ def deployed?(cluster_state)
16
+ cluster_state == CLUSTER_STATE_DEPLOYED
17
+ end
18
+
19
+ def deploying?(cluster_state)
20
+ [CLUSTER_STATE_DEPLOYING_NAMESPACE, CLUSTER_STATE_DEPLOYING].include?(cluster_state)
21
+ end
22
+
23
+ def failed?(cluster_state)
24
+ [CLUSTER_STATE_FAILED_DEPLOY_NAMESPACE, CLUSTER_STATE_FAILED].include?(cluster_state)
25
+ end
26
+
27
+ def wait_cluster_deploy(project_slug, cluster_name, oidc_token)
28
+ loop do
29
+ params = {
30
+ cluster_name: cluster_name,
31
+ oidc_token: oidc_token,
32
+ }
33
+ response = get_cluster(Uffizzi::ConfigFile.read_option(:server), project_slug, params)
34
+ return Uffizzi::ResponseHelper.handle_failed_response(response) unless Uffizzi::ResponseHelper.ok?(response)
35
+
36
+ cluster_data = response.dig(:body, :cluster)
37
+
38
+ return cluster_data unless deploying?(cluster_data[:state])
39
+
40
+ sleep(5)
41
+ end
42
+ end
43
+
44
+ def generate_name
45
+ name = [Faker::Name.first_name, Faker::Name.last_name].map(&:downcase).join('-')
46
+
47
+ return name if valid_name?(name)
48
+
49
+ generate_name
50
+ end
51
+
52
+ def valid_name?(name)
53
+ return false if name.nil?
54
+
55
+ regex = /\A[a-zA-Z0-9-]*\z/
56
+ regex.match?(name)
57
+ end
58
+ end
59
+ end
@@ -84,7 +84,7 @@ class ComposeFileService
84
84
  gzipped_file_size = Pathname.new(tmp_tar_path).size
85
85
 
86
86
  if gzipped_file_size > MAX_HOST_VOLUME_GZIP_FILE_SIZE
87
- Uffizzi.ui.say("File/Directory too big by path: #{path}. Gzipped tar archive size is #{gzipped_file_size}")
87
+ raise Uffizzi::Error.new("File or directory is too large:: #{path}. Gzipped tar archive size is #{gzipped_file_size}")
88
88
  end
89
89
 
90
90
  Base64.encode64(File.binread(tmp_tar_path))
@@ -139,11 +139,12 @@ class ComposeFileService
139
139
  def parse_compose_content_to_object(compose_content)
140
140
  begin
141
141
  compose_data = Psych.safe_load(compose_content, aliases: true)
142
- rescue Psych::SyntaxError
143
- Uffizzi.ui.say('Invalid compose file')
142
+ rescue Psych::SyntaxError => e
143
+ err = [e.problem, e.context].compact.join(' ')
144
+ raise Uffizzi::Error.new("Syntax error: #{err} at line #{e.line} column #{e.column}")
144
145
  end
145
146
 
146
- Uffizzi.ui.say('Unsupported compose file') if compose_data.nil?
147
+ raise Uffizzi::Error.new('Unsupported compose file') if compose_data.nil?
147
148
 
148
149
  compose_data
149
150
  end
@@ -28,7 +28,7 @@ class EnvVariablesService
28
28
  else
29
29
  "Environment variable #{variable_name} doesn't exist"
30
30
  end
31
- raise StandardError.new(error_message)
31
+ raise Uffizzi::Error.new(error_message)
32
32
  end
33
33
 
34
34
  def variable_has_default_value?(variable_content)
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GithubService
4
+ class << self
5
+ GITHUB_OUTPUT = 'GITHUB_OUTPUT'
6
+
7
+ def write_to_github_env(data)
8
+ return unless data.is_a?(Hash)
9
+
10
+ github_output = ENV.fetch(GITHUB_OUTPUT) { raise "#{GITHUB_OUTPUT} is not defined" }
11
+
12
+ File.open(github_output, 'a') do |f|
13
+ data.each { |(key, value)| f.puts("#{key}=#{value}") }
14
+ end
15
+ end
16
+
17
+ def github_actions_exists?
18
+ ENV['GITHUB_ACTIONS'].present?
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uffizzi/clients/api/api_client'
4
+ require 'psych'
5
+
6
+ class KubeconfigService
7
+ class InvalidKubeconfigError < StandardError
8
+ def initialize(file_path)
9
+ msg = "Invalid kubeconfig at path '#{file_path}'"
10
+
11
+ super(msg)
12
+ end
13
+ end
14
+
15
+ KUBECONFIG_GENERAL_KEYS = ['apiVersion', 'clusters', 'contexts', 'current-context', 'kind', 'users'].freeze
16
+
17
+ class << self
18
+ include ApiClient
19
+
20
+ def merge(target_kubeconfig, source_kubeconfig)
21
+ return source_kubeconfig.deep_dup if target_kubeconfig.nil?
22
+
23
+ new_cluster_name = get_current_cluster_name(source_kubeconfig)
24
+
25
+ if cluster_exists_in_kubeconfig?(target_kubeconfig, new_cluster_name)
26
+ replace_by_cluster_name(target_kubeconfig, source_kubeconfig, new_cluster_name)
27
+ else
28
+ add(target_kubeconfig, source_kubeconfig)
29
+ end
30
+ end
31
+
32
+ def exclude(target_kubeconfig, source_kubeconfig)
33
+ return if target_kubeconfig.nil?
34
+
35
+ excludable_cluster_name = get_current_cluster_name(source_kubeconfig)
36
+ exclude_by_cluster_name(target_kubeconfig, excludable_cluster_name)
37
+ end
38
+
39
+ def get_current_context(kubeconfig)
40
+ kubeconfig['current-context']
41
+ end
42
+
43
+ def get_first_context(kubeconfig)
44
+ kubeconfig.fetch('contexts', [])[0]
45
+ end
46
+
47
+ def get_current_cluster_name(kubeconfig)
48
+ kubeconfig['contexts']
49
+ .detect { |c| c['name'] == get_current_context(kubeconfig) }
50
+ .dig('context', 'cluster')
51
+ end
52
+
53
+ def update_current_context(kubeconfig, current_context)
54
+ new_kubeconfig = kubeconfig.deep_dup
55
+ new_kubeconfig['current-context'] = current_context
56
+
57
+ new_kubeconfig
58
+ end
59
+
60
+ def save_to_filepath(filepath, kubeconfig)
61
+ real_file_path = File.expand_path(filepath)
62
+ target_kubeconfig = File.exist?(real_file_path) ? Psych.safe_load(File.read(real_file_path)) : nil
63
+
64
+ if target_kubeconfig.present? && !valid_kubeconfig?(target_kubeconfig)
65
+ raise InvalidKubeconfigError.new(filepath)
66
+ end
67
+
68
+ new_kubeconfig = block_given? ? yield(target_kubeconfig) : kubeconfig
69
+ return if new_kubeconfig.nil?
70
+
71
+ dir_path = File.dirname(real_file_path)
72
+ FileUtils.mkdir_p(dir_path) unless File.directory?(dir_path)
73
+ File.write(real_file_path, new_kubeconfig.to_yaml)
74
+ end
75
+
76
+ def default_path
77
+ kubeconfig_env_path || Uffizzi.configuration.default_kubeconfig_path
78
+ end
79
+
80
+ private
81
+
82
+ def cluster_exists_in_kubeconfig?(kubeconfig, cluster_name)
83
+ clusters = kubeconfig['clusters'] || []
84
+ clusters.detect { |c| c['name'] == cluster_name }.present?
85
+ end
86
+
87
+ def add(target_kubeconfig, source_kubeconfig)
88
+ new_kubeconfig = target_kubeconfig.deep_dup
89
+ new_kubeconfig['clusters'] << source_kubeconfig['clusters'][0]
90
+ new_kubeconfig['contexts'] << source_kubeconfig['contexts'][0]
91
+ new_kubeconfig['users'] << source_kubeconfig['users'][0]
92
+
93
+ new_kubeconfig
94
+ end
95
+
96
+ def replace_by_cluster_name(target_kubeconfig, source_kubeconfig, cluster_name)
97
+ new_kubeconfig = exclude_by_cluster_name(target_kubeconfig, cluster_name)
98
+
99
+ add(new_kubeconfig, source_kubeconfig)
100
+ end
101
+
102
+ def exclude_by_cluster_name(kubeconfig, cluster_name)
103
+ clusters = kubeconfig['clusters']
104
+ contexts = kubeconfig['contexts']
105
+ users = kubeconfig['users']
106
+
107
+ return kubeconfig if clusters.empty? || contexts.empty? || users.empty?
108
+
109
+ target_user = contexts.detect { |c| c.dig('context', 'cluster') == cluster_name }.dig('context', 'user')
110
+ new_clusters = clusters.reject { |c| c['name'] == cluster_name }
111
+ new_contexts = contexts.reject { |c| c.dig('context', 'cluster') == cluster_name }
112
+ new_users = users.reject { |c| c['name'] == target_user }
113
+
114
+ kubeconfig.merge({ 'clusters' => new_clusters, 'contexts' => new_contexts, 'users' => new_users })
115
+ end
116
+
117
+ def kubeconfig_env_path
118
+ file_paths = ENV['KUBECONFIG']
119
+ return if file_paths.blank?
120
+
121
+ file_paths.split(':').first
122
+ end
123
+
124
+ def valid_kubeconfig?(data)
125
+ return false unless data.is_a?(Hash)
126
+
127
+ data_keys = data.keys.map(&:to_s)
128
+
129
+ KUBECONFIG_GENERAL_KEYS.all? { |k| data_keys.include?(k) }
130
+ end
131
+ end
132
+ end
@@ -3,6 +3,9 @@
3
3
  require 'uffizzi/clients/api/api_client'
4
4
 
5
5
  class PreviewService
6
+ ACTIVITY_ITEM_STATE_FAILED = 'failed'
7
+ ACTIVITY_ITEM_STATE_DEPLOYED = 'deployed'
8
+
6
9
  class << self
7
10
  include ApiClient
8
11
 
@@ -18,7 +21,8 @@ class PreviewService
18
21
 
19
22
  def run_containers_deploy(project_slug, deployment)
20
23
  deployment_id = deployment[:id]
21
- params = { id: deployment_id }
24
+ oidc_token = Uffizzi::ConfigFile.read_option(:oidc_token)
25
+ params = { id: deployment_id, token: oidc_token }
22
26
 
23
27
  response = deploy_containers(server_url, project_slug, deployment_id, params)
24
28
 
@@ -42,9 +46,7 @@ class PreviewService
42
46
 
43
47
  activity_items = []
44
48
  loop do
45
- response = get_activity_items(server_url, project_slug, deployment[:id])
46
- handle_activity_items_response(response, spinner)
47
- activity_items = response[:body][:activity_items]
49
+ activity_items = fetch_activity_items(project_slug, deployment[:id])
48
50
  break if activity_items.count == deployment[:containers].count
49
51
 
50
52
  sleep(5)
@@ -52,9 +54,13 @@ class PreviewService
52
54
 
53
55
  spinner.success
54
56
 
55
- Uffizzi.ui.say('Done')
57
+ Uffizzi.ui.say('Deployed')
58
+ Uffizzi.ui.say("Deployment url: https://#{deployment[:preview_url]}")
59
+ Uffizzi.ui.say("Deployment proxy url: https://#{deployment[:proxy_preview_url]}")
56
60
 
57
61
  activity_items
62
+ rescue ApiClient::ResponseError => e
63
+ Uffizzi::ResponseHelper.handle_failed_response(e.response)
58
64
  end
59
65
 
60
66
  def wait_containers_deploy(deployment, project_slug, activity_items)
@@ -66,16 +72,25 @@ class PreviewService
66
72
  containers_spinners = create_containers_spinners(activity_items, spinner)
67
73
 
68
74
  loop do
69
- response = get_activity_items(server_url, project_slug, deployment[:id])
70
- handle_activity_items_response(response, spinner)
71
- activity_items = response[:body][:activity_items]
75
+ activity_items = fetch_activity_items(project_slug, deployment[:id])
72
76
  update_containers_spinners!(activity_items, containers_spinners)
73
- break if activity_items.all? { |activity_item| activity_item[:state] == 'deployed' || activity_item[:state] == 'failed' }
77
+ break if activity_items.all? { |activity_item| activity_item_finished?(activity_item) }
74
78
 
75
79
  sleep(5)
76
80
  end
77
81
 
78
82
  spinner.success?
83
+ rescue ApiClient::ResponseError => e
84
+ spinner.error
85
+
86
+ descriptions = fetch_k8s_containers_descriptions(deployment, project_slug)
87
+ containers_last_state_messages = descriptions.map do |description|
88
+ container_name = "Last State for container '#{description[:container_name]}':"
89
+ states = description[:last_state].map { |k, v| " #{k}: #{v}" }.join("\n")
90
+ [container_name, states].join("\n")
91
+ end
92
+
93
+ Uffizzi::ResponseHelper.handle_failed_response(e.response, containers_last_state_messages)
79
94
  end
80
95
 
81
96
  def create_containers_spinners(activity_items, spinner)
@@ -90,27 +105,46 @@ class PreviewService
90
105
  end
91
106
 
92
107
  def update_containers_spinners!(activity_items, containers_spinners)
93
- finished_activity_items = activity_items.filter do |activity_item|
94
- activity_item[:state] == 'deployed' || activity_item[:state] == 'failed'
95
- end
108
+ finished_activity_items = activity_items.filter { |activity_item| activity_item_finished?(activity_item) }
96
109
 
97
110
  finished_activity_items.each do |activity_item|
98
111
  container_spinner = containers_spinners.detect { |spinner| spinner[:name] == activity_item[:name] }
99
112
  spinner = container_spinner[:spinner]
100
113
  case activity_item[:state]
101
- when 'deployed'
114
+ when ACTIVITY_ITEM_STATE_DEPLOYED
102
115
  spinner.success
103
- when 'failed'
116
+ when ACTIVITY_ITEM_STATE_FAILED
104
117
  spinner.error
105
118
  end
106
119
  end
107
120
  end
108
121
 
109
- def handle_activity_items_response(response, spinner)
110
- unless Uffizzi::ResponseHelper.ok?(response)
111
- spinner.error
112
- Uffizzi::ResponseHelper.handle_failed_response(response)
122
+ def fetch_activity_items(project_slug, deployment_id)
123
+ response = get_activity_items(server_url, project_slug, deployment_id)
124
+ raise ApiClient::ResponseError.new(response) unless Uffizzi::ResponseHelper.ok?(response)
125
+
126
+ response[:body][:activity_items]
127
+ end
128
+
129
+ def fetch_k8s_containers_descriptions(deployment, project_slug)
130
+ descriptions = []
131
+
132
+ deployment[:containers].each do |container|
133
+ container_name = container[:service_name]
134
+ response = get_k8s_container_description(server_url, project_slug, deployment[:id], container_name)
135
+
136
+ Uffizzi::ResponseHelper.handle_failed_response(response) unless Uffizzi::ResponseHelper.ok?(response)
137
+ last_state = response[:body][:last_state]
138
+ next if last_state.nil?
139
+
140
+ descriptions << { container_name: container_name, last_state: last_state }
113
141
  end
142
+
143
+ descriptions
144
+ end
145
+
146
+ def activity_item_finished?(activity_item)
147
+ activity_item[:state] == ACTIVITY_ITEM_STATE_DEPLOYED || activity_item[:state] == ACTIVITY_ITEM_STATE_FAILED
114
148
  end
115
149
  end
116
150
  end
data/lib/uffizzi/shell.rb CHANGED
@@ -5,28 +5,22 @@ require 'awesome_print'
5
5
  module Uffizzi
6
6
  module UI
7
7
  class Shell
8
+ class ExitError < Thor::Error; end
8
9
  attr_accessor :output_format
9
10
 
10
11
  PRETTY_JSON = 'pretty-json'
11
12
  REGULAR_JSON = 'json'
12
- GITHUB_ACTION = 'github-action'
13
13
 
14
14
  def initialize
15
15
  @shell = Thor::Shell::Basic.new
16
16
  end
17
17
 
18
18
  def say(message)
19
- formatted_message = case output_format
20
- when PRETTY_JSON
21
- format_to_pretty_json(message)
22
- when REGULAR_JSON
23
- format_to_json(message)
24
- when GITHUB_ACTION
25
- format_to_github_action(message)
26
- else
27
- message
28
- end
29
- @shell.say(formatted_message)
19
+ @shell.say(format_message(message))
20
+ end
21
+
22
+ def say_error_and_exit(message)
23
+ raise ExitError.new(format_message(message))
30
24
  end
31
25
 
32
26
  def print_in_columns(messages)
@@ -56,6 +50,10 @@ module Uffizzi
56
50
  $stdout = IO.new(1, 'w')
57
51
  end
58
52
 
53
+ def stdout_pipe?
54
+ $stdout.stat.pipe?
55
+ end
56
+
59
57
  private
60
58
 
61
59
  def format_to_json(data)
@@ -66,10 +64,15 @@ module Uffizzi
66
64
  JSON.pretty_generate(data)
67
65
  end
68
66
 
69
- def format_to_github_action(data)
70
- return '' unless data.is_a?(Hash)
71
-
72
- data.reduce('') { |acc, (key, value)| "#{acc}::set-output name=#{key}::#{value}\n" }
67
+ def format_message(message)
68
+ case output_format
69
+ when PRETTY_JSON
70
+ format_to_pretty_json(message)
71
+ when REGULAR_JSON
72
+ format_to_json(message)
73
+ else
74
+ message
75
+ end
73
76
  end
74
77
  end
75
78
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'uffizzi/helpers/file_helper'
5
+
6
+ module Uffizzi
7
+ class Token
8
+ TOKEN_PATH = "#{Dir.home}/.config/uffizzi/token"
9
+
10
+ class << self
11
+ def token_path
12
+ TOKEN_PATH
13
+ end
14
+
15
+ def delete
16
+ File.truncate(token_path, 0) if exists?
17
+ end
18
+
19
+ def exists?
20
+ File.exist?(token_path) && !read.nil?
21
+ end
22
+
23
+ def read
24
+ content = File.read(token_path)
25
+ content.presence
26
+ rescue Errno::ENOENT
27
+ nil
28
+ end
29
+
30
+ def write(token)
31
+ return nil unless token.is_a?(String)
32
+
33
+ Uffizzi::FileHelper.write_with_lock(token_path, token)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Uffizzi
4
- VERSION = '1.0.5'
4
+ VERSION = '2.0.27'
5
5
  end
data/lib/uffizzi.rb CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  require 'io/console'
4
4
  require 'tty-spinner'
5
+ require 'sentry-ruby'
6
+ require 'active_support'
7
+ require 'active_support/core_ext/hash/indifferent_access'
8
+ require 'active_support/core_ext/object/blank'
5
9
 
6
10
  require 'thor'
7
11
  require 'uffizzi/error'
@@ -11,8 +15,14 @@ require 'uffizzi/version'
11
15
  require 'uffizzi/clients/api/api_client'
12
16
  require 'uffizzi/clients/api/api_routes'
13
17
  require 'uffizzi/config_file'
18
+ require 'uffizzi/token'
14
19
  require_relative '../config/uffizzi'
15
20
 
21
+ Sentry.init do |config|
22
+ config.dsn = Base64.decode64(ENV['LOGGER_KEY'].to_s)
23
+ config.logger = Sentry::Logger.new(nil)
24
+ end
25
+
16
26
  module Uffizzi
17
27
  class << self
18
28
  def ui
data/man/uffizzi CHANGED
@@ -1,6 +1,6 @@
1
1
  .\" generated with Ronn-NG/v0.9.1
2
2
  .\" http://github.com/apjanke/ronn-ng/tree/0.9.1
3
- .TH "UFFIZZI" "" "May 2022" ""
3
+ .TH "UFFIZZI" "" "September 2023" ""
4
4
  .SH "NAME"
5
5
  \fBuffizzi\fR \- manage Uffizzi resources
6
6
  .SH "SYNOPSIS"
@@ -13,11 +13,13 @@ The uffizzi CLI manages authentication, configuration, and
13
13
  interaction with Uffizzi APIs\.
14
14
 
15
15
  For more information on the uffizzi CLI, see:
16
- https://github\.com/UffizziCloud/uffizzi_cli
16
+ https://docs\.uffizzi\.com/references/cli/
17
17
  .fi
18
18
  .SH "GROUP"
19
19
  .nf
20
20
  GROUP is one of the following:
21
+ cluster
22
+ Manage virtual clusters
21
23
 
22
24
  config
23
25
  Configure the uffizzi CLI
@@ -25,12 +27,12 @@ GROUP is one of the following:
25
27
  connect
26
28
  Grant a Uffizzi user account access to external services
27
29
 
28
- preview
29
- Manage Uffizzi previews and view logs
30
+ compose
31
+ Manage Uffizzi compose environments (previews) and view logs
30
32
 
31
33
  project
32
34
  Manage Uffizzi project resources including compose files for
33
- specifying preview configurations and secrets
35
+ specifying compose environment (preview) configurations and secrets
34
36
  .fi
35
37
  .SH "COMMAND"
36
38
  .nf
@@ -0,0 +1,29 @@
1
+ .\" generated with Ronn-NG/v0.9.1
2
+ .\" http://github.com/apjanke/ronn-ng/tree/0.9.1
3
+ .TH "UFFIZZI\-ACCOUNT" "" "August 2023" ""
4
+ .SH "NAME"
5
+ \fBuffizzi\-account\fR \- manage Uffizzi accounts
6
+ .SH "SYNOPSIS"
7
+ .nf
8
+ uffizzi account COMMAND [UFFIZZI_WIDE_FLAG \|\.\|\.\|\.]
9
+ .fi
10
+ .SH "DESCRIPTION"
11
+ .nf
12
+ Manage Uffizzi accounts
13
+
14
+ For more information on Uffizzi accounts, see:
15
+ https://docs\.uffizzi\.com/references/cli/
16
+ .fi
17
+ .SH "COMMANDS"
18
+ .nf
19
+ COMMAND is one of the following:
20
+
21
+ list
22
+ List all accounts associated with the user\'s credentials
23
+
24
+ set\-default
25
+ Set the default account in the uffizzi configuration\. Also see
26
+ $ uffizzi config help for details\.
27
+ .fi
28
+ .P
29
+ Run $ uffizzi account COMMAND \-\-help for more information on a command\.
@@ -0,0 +1,26 @@
1
+ .\" generated with Ronn-NG/v0.9.1
2
+ .\" http://github.com/apjanke/ronn-ng/tree/0.9.1
3
+ .TH "UFFIZZI\-ACCOUNT\-LIST" "" "August 2023" ""
4
+ .SH "NAME"
5
+ \fBuffizzi\-account\-list\fR \- list the accounts of the user
6
+ .SH "SYNOPSIS"
7
+ .nf
8
+ uffizzi account list
9
+ .fi
10
+ .SH "DESCRIPTION"
11
+ .nf
12
+ This command lists all accounts (not projects) for the associated user
13
+
14
+ This command can fail for the following reasons:
15
+ \- The user is not authenticated
16
+
17
+ For more information on preview services, see:
18
+ https://docs\.uffizzi\.com/references/cli/
19
+ .fi
20
+ .SH "EXAMPLES"
21
+ .nf
22
+ The following command lists the user\'s accounts:
23
+
24
+ $ uffizzi account list
25
+ .fi
26
+