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.
@@ -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.2'
5
5
  end