uffizzi-cli 1.0.4 → 2.0.29

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.
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 +3 -4
  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 +4 -2
  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 +131 -19
@@ -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
@@ -7,7 +7,9 @@ require 'base64'
7
7
  class EnvVariablesService
8
8
  class << self
9
9
  def substitute_env_variables(compose_file_data)
10
- compose_file_data.gsub(/\$\{?([?:\-_A-Za-z0-9]+)\}?/) do |variable|
10
+ compose_file_data.gsub(/\${1,2}\{?([?:\-_A-Za-z0-9]+)\}?/) do |variable|
11
+ next variable if variable.start_with?('$$')
12
+
11
13
  variable_content = variable.match(/[?:\-_A-Za-z0-9]+/).to_s
12
14
  fetch_variable_value(variable_content)
13
15
  end
@@ -26,7 +28,7 @@ class EnvVariablesService
26
28
  else
27
29
  "Environment variable #{variable_name} doesn't exist"
28
30
  end
29
- raise StandardError.new(error_message)
31
+ raise Uffizzi::Error.new(error_message)
30
32
  end
31
33
 
32
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.4'
4
+ VERSION = '2.0.29'
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
+