uffizzi-cli 0.1.4.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'io/console'
4
+ require 'uffizzi'
5
+ require 'uffizzi/auth_helper'
6
+ require 'uffizzi/response_helper'
7
+ require 'uffizzi/services/compose_file_service'
8
+ require 'uffizzi/services/env_variables_service'
9
+ require 'thor'
10
+
11
+ module Uffizzi
12
+ class CLI::Project::Compose < Thor
13
+ include ApiClient
14
+
15
+ desc 'set', 'set'
16
+ def set
17
+ run(options, 'set')
18
+ end
19
+
20
+ desc 'unset', 'unset'
21
+ def unset
22
+ run(options, 'unset')
23
+ end
24
+
25
+ desc 'describe', 'describe'
26
+ def describe
27
+ run(options, 'describe')
28
+ end
29
+
30
+ private
31
+
32
+ def run(options, command)
33
+ return Uffizzi.ui.say('You are not logged in.') unless Uffizzi::AuthHelper.signed_in?
34
+ return Uffizzi.ui.say('This command needs project to be set in config file') unless Uffizzi::AuthHelper.project_set?
35
+
36
+ file_path = options[:file]
37
+ case command
38
+ when 'set'
39
+ handle_set_command(file_path)
40
+ when 'unset'
41
+ handle_unset_command
42
+ when 'describe'
43
+ handle_describe_command
44
+ when 'validate'
45
+ handle_validate_command(file_path)
46
+ end
47
+ end
48
+
49
+ def handle_set_command(file_path)
50
+ return Uffizzi.ui.say('No file provided') if file_path.nil?
51
+
52
+ hostname = ConfigFile.read_option(:hostname)
53
+ project_slug = ConfigFile.read_option(:project)
54
+ params = prepare_params(file_path)
55
+ response = set_compose_file(hostname, params, project_slug)
56
+
57
+ if ResponseHelper.created?(response)
58
+ Uffizzi.ui.say('compose file created')
59
+ else
60
+ ResponseHelper.handle_failed_response(response)
61
+ end
62
+ end
63
+
64
+ def handle_unset_command
65
+ hostname = ConfigFile.read_option(:hostname)
66
+ project_slug = ConfigFile.read_option(:project)
67
+ response = unset_compose_file(hostname, project_slug)
68
+
69
+ if ResponseHelper.no_content?(response)
70
+ Uffizzi.ui.say('compose file deleted')
71
+ else
72
+ ResponseHelper.handle_failed_response(response)
73
+ end
74
+ end
75
+
76
+ def handle_describe_command
77
+ hostname = ConfigFile.read_option(:hostname)
78
+ project_slug = ConfigFile.read_option(:project)
79
+ response = describe_compose_file(hostname, project_slug)
80
+ compose_file = response[:body][:compose_file]
81
+
82
+ if ResponseHelper.ok?(response)
83
+ if compose_file_valid?(compose_file)
84
+ Uffizzi.ui.say(Base64.decode64(compose_file[:content]))
85
+ else
86
+ ResponseHelper.handle_invalid_compose_response(response)
87
+ end
88
+ else
89
+ ResponseHelper.handle_failed_response(response)
90
+ end
91
+ end
92
+
93
+ def compose_file_valid?(compose_file)
94
+ compose_file[:state] == 'valid_file'
95
+ end
96
+
97
+ def prepare_params(file_path)
98
+ begin
99
+ compose_file_data = EnvVariablesService.substitute_env_variables(File.read(file_path))
100
+ rescue Errno::ENOENT => e
101
+ return Uffizzi.ui.say(e)
102
+ end
103
+
104
+ compose_file_dir = File.dirname(file_path)
105
+ dependencies = ComposeFileService.parse(compose_file_data, compose_file_dir)
106
+ absolute_path = File.absolute_path(file_path)
107
+ compose_file_params = {
108
+ path: absolute_path,
109
+ content: Base64.encode64(compose_file_data),
110
+ source: absolute_path,
111
+ }
112
+
113
+ {
114
+ compose_file: compose_file_params,
115
+ dependencies: dependencies,
116
+ }
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'io/console'
4
+ require 'uffizzi'
5
+ require 'uffizzi/auth_helper'
6
+ require 'uffizzi/response_helper'
7
+ require 'thor'
8
+
9
+ module Uffizzi
10
+ class CLI::Project < Thor
11
+ include ApiClient
12
+
13
+ desc 'compose', 'compose'
14
+ method_option :file, required: false, aliases: '-f'
15
+ require_relative 'project/compose'
16
+ subcommand 'compose', Uffizzi::CLI::Project::Compose
17
+
18
+ desc 'list', 'list'
19
+ def list
20
+ run('list')
21
+ end
22
+
23
+ private
24
+
25
+ def run(command)
26
+ return Uffizzi.ui.say('You are not logged in.') unless Uffizzi::AuthHelper.signed_in?
27
+
28
+ case command
29
+ when 'list'
30
+ handle_list_command
31
+ end
32
+ end
33
+
34
+ def handle_list_command
35
+ hostname = ConfigFile.read_option(:hostname)
36
+ response = fetch_projects(hostname)
37
+
38
+ if ResponseHelper.ok?(response)
39
+ handle_succeed_response(response)
40
+ else
41
+ ResponseHelper.handle_failed_response(response)
42
+ end
43
+ end
44
+
45
+ def handle_succeed_response(response)
46
+ projects = response[:body][:projects]
47
+ return Uffizzi.ui.say('No projects related to this email') if projects.empty?
48
+
49
+ set_default_project(projects.first) if projects.size == 1
50
+ print_projects(projects)
51
+ end
52
+
53
+ def print_projects(projects)
54
+ projects_list = projects.reduce('') do |acc, project|
55
+ "#{acc}#{project[:slug]}\n"
56
+ end
57
+ Uffizzi.ui.say(projects_list)
58
+ end
59
+
60
+ def set_default_project(project)
61
+ ConfigFile.write_option(:project, project[:slug])
62
+ end
63
+ end
64
+ end
data/lib/uffizzi/cli.rb CHANGED
@@ -1,22 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'thor'
4
+ require 'uffizzi'
4
5
 
5
6
  module Uffizzi
6
7
  class CLI < Thor
7
8
  require_relative 'cli/common'
9
+ class_option :help, type: :boolean, aliases: ['-h', 'help']
8
10
 
9
- class_option :help, type: :boolean, aliases: HELP_MAPPINGS
10
-
11
- desc 'version', 'Show Version'
11
+ desc 'version', 'show version'
12
12
  def version
13
13
  require_relative 'version'
14
- puts Uffizzi::VERSION
14
+ Uffizzi.ui.say(Uffizzi::VERSION)
15
15
  end
16
16
 
17
17
  desc 'login', 'Login into Uffizzi'
18
18
  method_option :user, required: true, aliases: '-u'
19
- method_option :hostname, required: true, aliases: '-h'
19
+ method_option :hostname, required: true
20
20
  def login
21
21
  require_relative 'cli/login'
22
22
  Login.new(options).run
@@ -30,16 +30,17 @@ module Uffizzi
30
30
  Logout.new.run
31
31
  end
32
32
 
33
- desc 'projects', 'projects'
34
- def projects
35
- require_relative 'cli/projects'
36
- Projects.new.run
37
- end
33
+ desc 'project', 'project'
34
+ require_relative 'cli/project'
35
+ subcommand 'project', CLI::Project
38
36
 
39
37
  desc 'config', 'config'
40
- def config(command, property = nil, value = nil)
41
- require_relative 'cli/config'
42
- Config.new.run(command, property, value)
43
- end
38
+ require_relative 'cli/config'
39
+ subcommand 'config', CLI::Config
40
+
41
+ desc 'preview', 'preview'
42
+ method_option :project, required: false
43
+ require_relative 'cli/preview'
44
+ subcommand 'preview', CLI::Preview
44
45
  end
45
46
  end
@@ -7,27 +7,93 @@ module ApiClient
7
7
  include ApiRoutes
8
8
  def create_session(hostname, params = {})
9
9
  uri = session_uri(hostname)
10
- response = Uffizzi::HttpClient.make_request(uri, :post, false, params)
10
+ response = Uffizzi::HttpClient.make_post_request(uri, params, false)
11
11
 
12
12
  build_response(response)
13
13
  end
14
14
 
15
15
  def destroy_session(hostname)
16
16
  uri = session_uri(hostname)
17
- response = Uffizzi::HttpClient.make_request(uri, :delete, true)
17
+ response = Uffizzi::HttpClient.make_delete_request(uri)
18
18
 
19
19
  build_response(response)
20
20
  end
21
21
 
22
22
  def fetch_projects(hostname)
23
23
  uri = projects_uri(hostname)
24
- response = Uffizzi::HttpClient.make_request(uri, :get, true)
24
+ response = Uffizzi::HttpClient.make_get_request(uri)
25
25
 
26
26
  build_response(response)
27
27
  end
28
28
 
29
- def print_errors(errors)
30
- puts errors.keys.reduce([]) { |acc, key| acc.push(errors[key]) }
29
+ def set_compose_file(hostname, params, project_slug)
30
+ uri = compose_file_uri(hostname, project_slug)
31
+ response = Uffizzi::HttpClient.make_post_request(uri, params)
32
+
33
+ build_response(response)
34
+ end
35
+
36
+ def unset_compose_file(hostname, project_slug)
37
+ uri = compose_file_uri(hostname, project_slug)
38
+ response = Uffizzi::HttpClient.make_delete_request(uri, true)
39
+
40
+ build_response(response)
41
+ end
42
+
43
+ def describe_compose_file(hostname, project_slug)
44
+ uri = compose_file_uri(hostname, project_slug)
45
+ response = Uffizzi::HttpClient.make_get_request(uri)
46
+
47
+ build_response(response)
48
+ end
49
+
50
+ def validate_compose_file(hostname, project_slug)
51
+ uri = validate_compose_file_uri(hostname, project_slug)
52
+ response = Uffizzi::HttpClient.make_get_request(uri)
53
+
54
+ build_response(response)
55
+ end
56
+
57
+ def fetch_deployments(hostname, project_slug)
58
+ uri = deployments_uri(hostname, project_slug)
59
+ response = Uffizzi::HttpClient.make_get_request(uri)
60
+
61
+ build_response(response)
62
+ end
63
+
64
+ def create_deployment(hostname, project_slug, params)
65
+ uri = deployments_uri(hostname, project_slug)
66
+ response = Uffizzi::HttpClient.make_post_request(uri, params)
67
+
68
+ build_response(response)
69
+ end
70
+
71
+ def delete_deployment(hostname, project_slug, deployment_id)
72
+ uri = deployment_uri(hostname, project_slug, deployment_id)
73
+ response = Uffizzi::HttpClient.make_delete_request(uri, true)
74
+
75
+ build_response(response)
76
+ end
77
+
78
+ def describe_deployment(hostname, project_slug, deployment_id)
79
+ uri = deployment_uri(hostname, project_slug, deployment_id)
80
+ response = Uffizzi::HttpClient.make_get_request(uri)
81
+
82
+ build_response(response)
83
+ end
84
+
85
+ def get_activity_items(hostname, project_slug, deployment_id)
86
+ uri = activity_items_uri(hostname, project_slug, deployment_id)
87
+ response = Uffizzi::HttpClient.make_get_request(uri)
88
+
89
+ build_response(response)
90
+ end
91
+
92
+ def deploy_containers(hostname, project_slug, deployment_id, params)
93
+ uri = deploy_containers_uri(hostname, project_slug, deployment_id)
94
+ response = Uffizzi::HttpClient.make_post_request(uri, params)
95
+
96
+ build_response(response)
31
97
  end
32
98
 
33
99
  private
@@ -8,4 +8,28 @@ module ApiRoutes
8
8
  def projects_uri(hostname)
9
9
  "#{hostname}/api/cli/v1/projects"
10
10
  end
11
+
12
+ def compose_file_uri(hostname, project_slug)
13
+ "#{hostname}/api/cli/v1/projects/#{project_slug}/compose_file"
14
+ end
15
+
16
+ def validate_compose_file_uri(hostname, project_slug)
17
+ "#{compose_files_uri(hostname, project_slug)}/validate"
18
+ end
19
+
20
+ def deployments_uri(hostname, project_slug)
21
+ "#{hostname}/api/cli/v1/projects/#{project_slug}/deployments"
22
+ end
23
+
24
+ def deployment_uri(hostname, project_slug, deployment_id)
25
+ "#{hostname}/api/cli/v1/projects/#{project_slug}/deployments/#{deployment_id}"
26
+ end
27
+
28
+ def activity_items_uri(hostname, project_slug, deployment_id)
29
+ "#{hostname}/api/cli/v1/projects/#{project_slug}/deployments/#{deployment_id}/activity_items"
30
+ end
31
+
32
+ def deploy_containers_uri(hostname, project_slug, deployment_id)
33
+ "#{hostname}/api/cli/v1/projects/#{project_slug}/deployments/#{deployment_id}/deploy_containers"
34
+ end
11
35
  end
@@ -3,11 +3,30 @@
3
3
  require 'net/http'
4
4
  require 'json'
5
5
  require 'uffizzi/config_file'
6
+ require 'uffizzi/response_helper'
6
7
 
7
8
  module Uffizzi
8
9
  class HttpClient
9
10
  class << self
10
- def make_request(request_uri, method, require_cookies, params = {})
11
+ def make_get_request(request_uri, cookies_required = true)
12
+ make_request(:get, request_uri, cookies_required)
13
+ end
14
+
15
+ def make_post_request(request_uri, params = {}, cookies_required = true)
16
+ make_request(:post, request_uri, cookies_required, params)
17
+ end
18
+
19
+ def make_put_request(request_uri, cookies_required = true)
20
+ make_request(:put, request_uri, cookies_required)
21
+ end
22
+
23
+ def make_delete_request(request_uri, cookies_required = true)
24
+ make_request(:delete, request_uri, cookies_required)
25
+ end
26
+
27
+ private
28
+
29
+ def make_request(method, request_uri, require_cookies, params = {})
11
30
  uri = URI(request_uri)
12
31
  use_ssl = request_uri.start_with?('https')
13
32
 
@@ -24,8 +43,6 @@ module Uffizzi
24
43
  response
25
44
  end
26
45
 
27
- private
28
-
29
46
  def build_request(uri, params, method, require_cookies)
30
47
  headers = { 'Content-Type' => 'application/json' }
31
48
  request = case method
@@ -45,6 +62,7 @@ module Uffizzi
45
62
  if ConfigFile.exists? && ConfigFile.option_exists?(:basic_auth_user) && ConfigFile.option_exists?(:basic_auth_password)
46
63
  request.basic_auth(ConfigFile.read_option(:basic_auth_user), ConfigFile.read_option(:basic_auth_password))
47
64
  end
65
+
48
66
  request
49
67
  end
50
68
  end
@@ -23,22 +23,21 @@ module Uffizzi
23
23
 
24
24
  def read_option(option)
25
25
  data = read
26
- return nil if data.nil?
26
+ return nil unless data.is_a?(Hash)
27
27
 
28
- puts "The option #{option} doesn't exist in config file" if data[option].nil?
29
28
  data[option]
30
29
  end
31
30
 
32
31
  def option_exists?(option)
33
32
  data = read
34
- return false if data.nil?
33
+ return false unless data.is_a?(Hash)
35
34
 
36
35
  data.key?(option)
37
36
  end
38
37
 
39
38
  def write_option(key, value)
40
39
  data = exists? ? read : {}
41
- return nil if data.nil?
40
+ return nil unless data.is_a?(Hash)
42
41
 
43
42
  data[key] = value
44
43
  write(data.to_json)
@@ -46,7 +45,7 @@ module Uffizzi
46
45
 
47
46
  def delete_option(key)
48
47
  data = read
49
- return nil if data.nil?
48
+ return nil unless data.is_a?(Hash)
50
49
 
51
50
  new_data = data.except(key)
52
51
  write(new_data.to_json)
@@ -58,11 +57,16 @@ module Uffizzi
58
57
 
59
58
  def list
60
59
  data = read
61
- return nil if data.nil?
60
+ return nil unless data.is_a?(Hash)
62
61
 
63
- data.each do |property, value|
64
- puts "#{property} - #{value}"
62
+ content = data.reduce('') do |acc, pair|
63
+ property, value = pair
64
+ "#{acc}#{property} - #{value}\n"
65
65
  end
66
+
67
+ Uffizzi.ui.say(content)
68
+
69
+ data
66
70
  end
67
71
 
68
72
  private
@@ -70,9 +74,11 @@ module Uffizzi
70
74
  def read
71
75
  JSON.parse(File.read(CONFIG_PATH), symbolize_names: true)
72
76
  rescue Errno::ENOENT => e
73
- puts e
77
+ Uffizzi.ui.say(e)
78
+ nil
74
79
  rescue JSON::ParserError
75
- puts 'Config file is in incorrect format'
80
+ Uffizzi.ui.say('Config file is in incorrect format')
81
+ nil
76
82
  end
77
83
 
78
84
  def write(data)
@@ -92,9 +98,7 @@ module Uffizzi
92
98
  def create_file
93
99
  dir = File.dirname(CONFIG_PATH)
94
100
 
95
- unless File.directory?(dir)
96
- FileUtils.mkdir_p(dir)
97
- end
101
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
98
102
 
99
103
  File.new(CONFIG_PATH, 'w')
100
104
  end
@@ -22,6 +22,30 @@ module Uffizzi
22
22
  def no_content?(response)
23
23
  response[:code] == Net::HTTPNoContent
24
24
  end
25
+
26
+ def ok?(response)
27
+ response[:code] == Net::HTTPOK
28
+ end
29
+
30
+ def handle_failed_response(response)
31
+ print_errors(response[:body][:errors])
32
+ end
33
+
34
+ def handle_invalid_compose_response(response)
35
+ print_errors(response[:body][:compose_file][:payload][:errors])
36
+ end
37
+
38
+ private
39
+
40
+ def print_errors(errors)
41
+ errors.each_key do |key|
42
+ if errors[key].is_a?(Array)
43
+ errors[key].each { |error_message| Uffizzi.ui.say(error_message) }
44
+ else
45
+ Uffizzi.ui.say(errors[key])
46
+ end
47
+ end
48
+ end
25
49
  end
26
50
  end
27
51
  end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'psych'
4
+ require 'pathname'
5
+ require 'base64'
6
+
7
+ class ComposeFileService
8
+ class << self
9
+ def parse(compose_content, compose_file_path)
10
+ compose_data = parse_compose_content_to_object(compose_content)
11
+
12
+ env_files = prepare_services_env_files(compose_data['services']).flatten.uniq
13
+ config_files = fetch_configs(compose_data['configs'])
14
+ prepare_dependencies(env_files, config_files, compose_file_path)
15
+ end
16
+
17
+ private
18
+
19
+ def prepare_dependencies(env_files, config_files, compose_file_path)
20
+ prepare_dependency_files_data(env_files + config_files, compose_file_path)
21
+ end
22
+
23
+ def prepare_dependency_files_data(dependency_files, compose_file_path)
24
+ dependency_files.map do |dependency_file|
25
+ dependency_file_data = Psych.load(File.read("#{compose_file_path}/#{dependency_file}"))
26
+ {
27
+ path: dependency_file,
28
+ source: dependency_file,
29
+ content: Base64.encode64(dependency_file_data),
30
+ }
31
+ end
32
+ end
33
+
34
+ def fetch_configs(configs_data)
35
+ return [] if configs_data.nil?
36
+
37
+ Uffizzi.ui.say("Unsupported type of #{:configs} option") unless configs_data.is_a?(Hash)
38
+
39
+ configs = []
40
+ configs_data.each_pair do |config_name, config_data|
41
+ Uffizzi.ui.say("#{config_name} has an empty file") if config_data['file'].empty? || config_data['file'].nil?
42
+
43
+ configs << prepare_file_path(config_data['file'])
44
+ end
45
+
46
+ configs
47
+ end
48
+
49
+ def prepare_file_path(file_path)
50
+ pathname = Pathname.new(file_path)
51
+
52
+ pathname.cleanpath.to_s.strip.delete_prefix('/')
53
+ end
54
+
55
+ def parse_env_file(env_file)
56
+ case env_file
57
+ when String
58
+ Uffizzi.ui.say('env_file contains an empty value') if env_file.nil? || env_file.empty?
59
+ [prepare_file_path(env_file)]
60
+ when Array
61
+ Uffizzi.ui.say('env_file contains an empty value') if env_file.any? { |file| file.nil? || file.empty? }
62
+ env_file.map { |env_file_path| prepare_file_path(env_file_path) }
63
+ else
64
+ Uffizzi.ui.say("Unsupported type of #{:env_file} option")
65
+ end
66
+ end
67
+
68
+ def prepare_services_env_files(services)
69
+ return [] if services.nil?
70
+
71
+ services.keys.map do |service|
72
+ service_env_files = prepare_service_env_files(services.fetch(service))
73
+
74
+ service_env_files
75
+ end
76
+ end
77
+
78
+ def prepare_service_env_files(service_data)
79
+ env_files_data = []
80
+ service_data.each_pair do |key, value|
81
+ key_sym = key.to_sym
82
+ if key_sym == :env_file
83
+ env_files_data << parse_env_file(value)
84
+ end
85
+ end
86
+
87
+ env_files_data
88
+ end
89
+
90
+ def parse_compose_content_to_object(compose_content)
91
+ begin
92
+ compose_data = Psych.safe_load(compose_content)
93
+ rescue Psych::SyntaxError
94
+ Uffizzi.ui.say('Invalid compose file')
95
+ end
96
+
97
+ Uffizzi.ui.say('Unsupported compose file') if compose_data.nil?
98
+
99
+ compose_data
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'psych'
4
+ require 'pathname'
5
+ require 'base64'
6
+
7
+ class EnvVariablesService
8
+ class << self
9
+ def substitute_env_variables(compose_file_data)
10
+ compose_file_data.gsub(/\$\{?([?:\-_A-Za-z0-9]+)\}?/) do |variable|
11
+ variable_content = variable.match(/[?:\-_A-Za-z0-9]+/).to_s
12
+ fetch_variable_value(variable_content)
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def fetch_variable_value(variable_content)
19
+ variable_name = variable_content.match(/^[_A-Za-z0-9]+/).to_s
20
+ variable_value = ENV[variable_name]
21
+ return variable_value unless variable_value.nil?
22
+ return fetch_variable_default_value(variable_content) if variable_has_default_value?(variable_content)
23
+
24
+ error_message = if variable_has_error_message?(variable_content)
25
+ fetch_env_error_message(variable_content)
26
+ else
27
+ "Environment variable #{variable_name} doesn't exist"
28
+ end
29
+ raise StandardError.new(error_message)
30
+ end
31
+
32
+ def variable_has_default_value?(variable_content)
33
+ variable_content.include?('-')
34
+ end
35
+
36
+ def fetch_variable_default_value(variable_content)
37
+ variable_content.split('-', 2).last
38
+ end
39
+
40
+ def variable_has_error_message?(variable_content)
41
+ variable_content.include?('?')
42
+ end
43
+
44
+ def fetch_env_error_message(variable_content)
45
+ variable_content.split('?', 2).last
46
+ end
47
+ end
48
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Uffizzi
4
- VERSION = '0.1.4.3'
4
+ VERSION = '0.2.0'
5
5
  end