From dd381a579d6008eabedd54dc618ddc96a27e2f62 Mon Sep 17 00:00:00 2001 From: Zipofar Date: Thu, 16 Nov 2023 19:37:38 +0300 Subject: [PATCH] [374] WIP --- lib/uffizzi/cli.rb | 4 + lib/uffizzi/cli/install.rb | 409 ++++++++---------- lib/uffizzi/cli/uninstall.rb | 98 +++++ lib/uffizzi/clients/api/api_client.rb | 28 ++ lib/uffizzi/clients/api/api_routes.rb | 8 + lib/uffizzi/services/install_service.rb | 153 +++++++ .../uffizzi_account_controller_settings.json | 9 + ...zzi_account_controller_settings_empty.json | 3 + test/support/mocks/mock_shell.rb | 18 +- test/support/uffizzi_stub_support.rb | 31 +- test/test_helper.rb | 4 + test/uffizzi/cli/install_test.rb | 106 ++++- test/uffizzi/cli/uninstall_test.rb | 38 ++ 13 files changed, 630 insertions(+), 279 deletions(-) create mode 100644 lib/uffizzi/cli/uninstall.rb create mode 100644 lib/uffizzi/services/install_service.rb create mode 100644 test/fixtures/files/uffizzi/uffizzi_account_controller_settings.json create mode 100644 test/fixtures/files/uffizzi/uffizzi_account_controller_settings_empty.json create mode 100644 test/uffizzi/cli/uninstall_test.rb diff --git a/lib/uffizzi/cli.rb b/lib/uffizzi/cli.rb index 146e7ade..79f9575b 100644 --- a/lib/uffizzi/cli.rb +++ b/lib/uffizzi/cli.rb @@ -79,6 +79,10 @@ def disconnect(credential_type) require_relative 'cli/install' subcommand 'install', Cli::Install + desc 'uninstall', 'uninstall' + require_relative 'cli/uninstall' + subcommand 'uninstall', Cli::Uninstall + map preview: :compose class << self diff --git a/lib/uffizzi/cli/install.rb b/lib/uffizzi/cli/install.rb index 4a4c464f..26e52f5c 100644 --- a/lib/uffizzi/cli/install.rb +++ b/lib/uffizzi/cli/install.rb @@ -2,316 +2,251 @@ require 'uffizzi' require 'uffizzi/config_file' +require 'uffizzi/services/kubeconfig_service' module Uffizzi class Cli::Install < Thor - HELM_REPO_NAME = 'uffizzi' - HELM_DEPLOYED_STATUS = 'deployed' - CHART_NAME = 'uffizzi-app' - VALUES_FILE_NAME = 'helm_values.yaml' - DEFAULT_NAMESPACE = 'uffizzi' - DEFAULT_APP_PREFIX = 'uffizzi' - DEFAULT_CLUSTER_ISSUER = 'letsencrypt' - - desc 'application', 'Install uffizzi to cluster' + include ApiClient + + default_task :controller + + desc 'controller [HOSTNMAE]', 'Install uffizzi controller to cluster' method_option :namespace, type: :string - method_option :domain, type: :string - method_option :'user-email', type: :string - method_option :'user-password', type: :string + method_option :email, type: :string, required: true + method_option :context, type: :string method_option :issuer, type: :string, enum: ['letsencrypt', 'zerossl'] - method_option :repo, type: :string - method_option :'print-values', type: :boolean - def application - run_installation do - if options.except(:repo, :'print-values').present? - build_installation_options - else - ask_installation_params - end - end - end + method_option :'repo-url', type: :string + def controller(hostname) + Uffizzi::AuthHelper.check_login - desc 'wildcard-tls', 'Add wildcard tls from files' - method_option :domain, type: :string - method_option :cert, type: :string - method_option :key, type: :string - method_option :namespace, type: :string - method_option :repo, type: :string - def wildcard_tls - kubectl_exists? - - params = if options.except(:repo).present? && wildcard_tls_options_valid? - { - namespace: options[:namespace] || DEFAULT_NAMESPACE, - domain: options[:domain], - wildcard_cert_path: options[:cert], - wildcard_key_path: options[:key], - } - else - namespace = Uffizzi.prompt.ask('Namespace: ', required: true, default: DEFAULT_NAMESPACE) - domain = Uffizzi.prompt.ask('Root Domain: ', required: true, default: 'example.com') - wildcard_cert_paths = ask_wildcard_cert(has_user_wildcard_cert: true, domain: domain) + InstallService.kubectl_exists? + InstallService.helm_exists? - { namespace: namespace, domain: domain }.merge(wildcard_cert_paths) + if options[:context].present? && options[:context] != InstallService.kubeconfig_current_context + InstallService.set_current_context(options[:context]) end - kubectl_add_wildcard_tls(params) - helm_values = helm_get_values(namespace, namespace) - helm_values['uffizzi-controller']['tlsPerDeploymentEnabled'] = false.to_s - create_helm_values_file(helm_values) - helm_set_repo unless options[:repo] - helm_install(release_name: namespace, namespace: namespace, repo: options[:repo]) - end + ask_confirmation - default_task :application + uri = parse_hostname(hostname) + installation_options = build_installation_options(uri) + check_existence_controller_settings(uri, installation_options) + helm_values = build_helm_values(installation_options) - private + InstallService.create_helm_values_file(helm_values) + helm_set_repo + InstallService.helm_install!(namespace) + InstallService.delete_helm_values_file + Uffizzi.ui.say('Helm release is deployed') - def wildcard_tls_options_valid? - required_options = [:namespace, :domain, :cert, :key] - missing_options = required_options - options.symbolize_keys.keys + controller_setting_params = build_controller_setting_params(uri, installation_options) + create_controller_settings(controller_setting_params) if existing_controller_setting.blank? + Uffizzi.ui.say('Controller settings are saved') + say_success(uri) + end - return true if missing_options.empty? + private - rendered_missing_options = missing_options.map { |o| "'--#{o}'" }.join(', ') + def helm_set_repo + return if InstallService.helm_repo_search.present? - Uffizzi.ui.say_error_and_exit("No value provided for required options #{rendered_missing_options}") + InstallService.helm_repo_add(options[:'repo-url']) end - def run_installation - kubectl_exists? - helm_exists? - params = yield - helm_values = build_helm_values(params) - return Uffizzi.ui.say(helm_values.to_yaml) if options[:'print-values'] - - namespace = params[:namespace] - release_name = params[:namespace] - - create_helm_values_file(helm_values) - helm_set_repo unless options[:repo] - helm_install(release_name: release_name, namespace: namespace, repo: options[:repo]) - delete_helm_values_file + def build_installation_options(uri) + { + uri: uri, + controller_username: Faker::Lorem.characters(number: 10), + controller_password: generate_password, + cert_email: options[:email], + cluster_issuer: options[:issuer] || InstallService::DEFAULT_CLUSTER_ISSUER, + } + end - ingress_ip = get_web_ingress_ip_address(release_name, namespace) + def wait_ip + spinner = TTY::Spinner.new('[:spinner] Waiting IP addess...', format: :dots) + spinner.auto_spin - Uffizzi.ui.say('Helm release is deployed') - Uffizzi.ui.say("The uffizzi application url is 'https://#{DEFAULT_APP_PREFIX}.#{params[:domain]}'") - Uffizzi.ui.say("Create a DNS A record for domain '*.#{params[:domain]}' with value '#{ingress_ip}'") - end + ip = nil + try = 0 - def get_web_ingress_ip_address(release_name, namespace) - Uffizzi.ui.say('Getting an ingress ip address...') + loop do + ip = InstallService.get_controller_ip(namespace) + break if ip.present? - 10.times do - web_ingress = kubectl_get_web_ingress(release_name, namespace) - ingresses = web_ingress.dig('status', 'loadBalancer', 'ingress') || [] - ip_address = ingresses.first&.fetch('ip', nil) + if try == 30 + spinner.error - return ip_address if ip_address.present? + return 'unknown' + end - sleep(1) + try += 1 + sleep(2) end - Uffizzi.ui.say_error_and_exit('We can`t get the uffizzi ingress ip address') - end + spinner.success - def kubectl_exists? - cmd = 'kubectl version -o json' - execute_command(cmd, say: false).present? + ip end - def helm_exists? - cmd = 'helm version --short' - execute_command(cmd, say: false).present? - end + def wait_certificate_request_ready(uri) + spinner = TTY::Spinner.new('[:spinner] Waiting create certificate for controller host...', format: :dots) + spinner.auto_spin - def helm_set_repo - repo = helm_repo_search - return if repo.present? + try = 0 - helm_repo_add - end + loop do + requests = InstallService.get_certificate_request(namespace, uri) + break if requests.all? { |r| r['status'].downcase == 'true' } - def helm_repo_add - cmd = "helm repo add #{HELM_REPO_NAME} https://uffizzicloud.github.io/uffizzi" - execute_command(cmd) - end + if try == 60 + spinner.error - def helm_repo_search - cmd = "helm search repo #{HELM_REPO_NAME}/#{CHART_NAME} -o json" + return Uffizzi.ui.say('Stop waiting creation certificate') + end - execute_command(cmd) do |result, err| - err.present? ? nil : JSON.parse(result) + try += 1 + sleep(2) end - end - - def helm_install(release_name:, namespace:, repo:) - Uffizzi.ui.say('Start helm release installation') - - repo = repo || "#{HELM_REPO_NAME}/#{CHART_NAME}" - cmd = "helm upgrade #{release_name} #{repo}" \ - " --values #{helm_values_file_path}" \ - " --namespace #{namespace}" \ - ' --create-namespace' \ - ' --install' \ - ' --output json' - res = execute_command(cmd, say: false) - info = JSON.parse(res)['info'] - - return if info['status'] == HELM_DEPLOYED_STATUS - - Uffizzi.ui.say_error_and_exit(info) + spinner.success end - def helm_get_values(release_name, namespace) - cmd = "helm get values #{release_name} -n #{namespace} -o json" - res = execute_command(cmd, say: false) - JSON.parse(res) + def build_helm_values(params) + { + global: { + uffizzi: { + controller: { + username: params[:controller_username], + password: params[:controller_password], + }, + }, + }, + clusterIssuer: params.fetch(:cluster_issuer), + tlsPerDeploymentEnabled: true.to_s, + certEmail: params.fetch(:cert_email), + 'ingress-nginx' => { + controller: { + ingressClassResource: { + default: true, + }, + }, + }, + ingress: { + hostname: InstallService.build_controller_host(params[:uri].host), + }, + }.deep_stringify_keys end - def kubectl_add_wildcard_tls(params) - cmd = "kubectl create secret tls wildcard.#{params.fetch(:domain)}" \ - " --cert=#{params.fetch(:wildcard_cert_path)}" \ - " --key=#{params.fetch(:wildcard_key_path)}" \ - " --namespace #{params.fetch(:namespace)}" - - execute_command(cmd) + def generate_password + hexatridecimal_base = 36 + length = 8 + rand(hexatridecimal_base**length).to_s(hexatridecimal_base) end - def kubectl_get_web_ingress(release_name, namespace) - cmd = "kubectl get ingress/#{release_name}-web-ingress -n #{namespace} -o json" + def check_existence_controller_settings(uri, installation_options) + return if existing_controller_setting.blank? + + Uffizzi.ui.say_error_and_exit('Installation canceled') unless update_and_continue? - res = execute_command(cmd, say: false) - JSON.parse(res) + controller_setting_params = build_controller_setting_params(uri, installation_options) + update_controller_settings(existing_controller_setting[:id], controller_setting_params) end - def ask_wildcard_cert(has_user_wildcard_cert: nil, domain: nil) - has_user_wildcard_cert ||= Uffizzi.prompt.yes?('Uffizzi use a wildcard tls certificate. Do you have it?') + def ask_confirmation + msg = "\r\n"\ + 'This command will install Uffizzi into the default namespace of'\ + " the '#{InstallService.kubeconfig_current_context}' context."\ + "\r\n"\ + "To install in a different place, use options '--namespace' and/or '--context'."\ + "\r\n\r\n"\ + "After installation, new environments created for account '#{account_name}' will be deployed to this host cluster."\ + "\r\n\r\n" - if !has_user_wildcard_cert - Uffizzi.ui.say('Uffizzi does not work properly without a wildcard certificate.') - Uffizzi.ui.say('You can add wildcard cert later with command:') - Uffizzi.ui.say("uffizzi install wildcard-tls --domain #{domain} --cert /path/to/cert --key /path/to/key") + Uffizzi.ui.say(msg) - return {} - end + question = 'Okay to proceed?' + Uffizzi.ui.say_error_and_exit('Installation canceled') unless Uffizzi.prompt.yes?(question) + end - cert_path = Uffizzi.prompt.ask('Path to cert: ', required: true) - Uffizzi.ui.say_error_and_exit("File '#{cert_path}' does not exists") unless File.exist?(cert_path) + def update_and_continue? + msg = "\r\n"\ + 'You already have installation controller params. '\ + "\r\n"\ + 'You can update previous params and continue installation or cancel installation.'\ + "\r\n" - key_path = Uffizzi.prompt.ask('Path to key: ', required: true) - Uffizzi.ui.say_error_and_exit("File '#{key_path}' does not exists") unless File.exist?(key_path) + Uffizzi.ui.say(msg) - { wildcard_cert_path: cert_path, wildcard_key_path: key_path } + question = 'Do you want update the controller settings?' + Uffizzi.prompt.yes?(question) end - def ask_installation_params - namespace = Uffizzi.prompt.ask('Namespace: ', required: true, default: DEFAULT_NAMESPACE) - domain = Uffizzi.prompt.ask('Root domain: ', required: true, default: 'example.com') - user_email = Uffizzi.prompt.ask('First user email: ', required: true, default: "admin@#{domain}") - user_password = Uffizzi.prompt.ask('First user password: ', required: true, default: generate_password) - wildcard_cert_paths = ask_wildcard_cert(domain: domain) + def fetch_controller_settings + response = get_account_controller_settings(server, account_id) + return Uffizzi::ResponseHelper.handle_failed_response(response) unless Uffizzi::ResponseHelper.ok?(response) - { - namespace: namespace, - domain: domain, - user_email: user_email, - user_password: user_password, - controller_password: generate_password, - cert_email: user_email, - cluster_issuer: DEFAULT_CLUSTER_ISSUER, - }.merge(wildcard_cert_paths) + response.dig(:body, :controller_settings) end - def build_installation_options - { - namespace: options[:namespace] || DEFAULT_NAMESPACE, - domain: options[:domain], - user_email: options[:'user-email'] || "admin@#{options[:domain]}", - user_password: options[:'user-password'] || generate_password, - controller_password: generate_password, - cert_email: options[:'user-email'], - cluster_issuer: options[:issuer] || DEFAULT_CLUSTER_ISSUER, - } + def update_controller_settings(controller_setting_id, params) + response = update_account_controller_settings(server, account_id, controller_setting_id, params) + Uffizzi::ResponseHelper.handle_failed_response(response) unless Uffizzi::ResponseHelper.ok?(response) end - def build_helm_values(params) - domain = params.fetch(:domain) - namespace = params.fetch(:namespace) - app_host = [DEFAULT_APP_PREFIX, domain].join('.') + def create_controller_settings(params) + response = create_account_controller_settings(server, account_id, params) + Uffizzi::ResponseHelper.handle_failed_response(response) unless Uffizzi::ResponseHelper.created?(response) + end + def build_controller_setting_params(uri, installation_options) { - app_url: "https://#{app_host}", - webHostname: app_host, - allowed_hosts: app_host, - managed_dns_zone_dns_name: domain, - global: { - uffizzi: { - firstUser: { - email: params.fetch(:user_email), - password: params.fetch(:user_password), - }, - controller: { - password: params.fetch(:controller_password), - }, - }, - }, - 'uffizzi-controller' => { - ingress: { - disabled: true, - }, - clusterIssuer: params.fetch(:cluster_issuer), - tlsPerDeploymentEnabled: true.to_s, - certEmail: params.fetch(:cert_email), - 'ingress-nginx' => { - controller: { - ingressClassResource: { - default: true, - }, - extraArgs: { - 'default-ssl-certificate' => "#{namespace}/wildcard.#{domain}", - }, - }, - }, - }, - }.deep_stringify_keys + url: URI::HTTPS.build(host: InstallService.build_controller_host(uri.host)).to_s, + managed_dns_zone: uri.host, + login: installation_options[:controller_username], + password: installation_options[:controller_password], + } end - def execute_command(command, say: true) - stdout_str, stderr_str, status = Uffizzi.ui.capture3(command) + def say_success(uri) + ip_address = wait_ip + wait_certificate_request_ready(uri) - return yield(stdout_str, stderr_str) if block_given? + msg = 'Your Uffizzi controller is ready. To configure DNS,'\ + " create a record for the hostname '*.#{uri.host}' pointing to '#{ip_address}'" + Uffizzi.ui.say(msg) + end - Uffizzi.ui.say_error_and_exit(stderr_str) unless status.success? + def parse_hostname(hostname) + uri = URI.parse(hostname) + host = uri.host || hostname - say ? Uffizzi.ui.say(stdout_str) : stdout_str - rescue Errno::ENOENT => e - Uffizzi.ui.say_error_and_exit(e.message) + case uri + when URI::HTTP, URI::HTTPS + uri + else + URI::HTTPS.build(host: host) + end end - def create_helm_values_file(values) - FileUtils.mkdir_p(helm_values_dir_path) unless File.directory?(helm_values_dir_path) - File.write(helm_values_file_path, values.to_yaml) + def namespace + options[:namespace] || InstallService::DEFAULT_NAMESPACE end - def delete_helm_values_file - File.delete(helm_values_file_path) if File.exist?(helm_values_file_path) + def server + @server ||= ConfigFile.read_option(:server) end - def helm_values_file_path - File.join(helm_values_dir_path, VALUES_FILE_NAME) + def account_id + @account_id ||= ConfigFile.read_option(:account, :id) end - def helm_values_dir_path - File.dirname(Uffizzi::ConfigFile.config_path) + def account_name + @account_name ||= ConfigFile.read_option(:account, :name) end - def generate_password - hexatridecimal_base = 36 - length = 8 - rand(hexatridecimal_base**length).to_s(hexatridecimal_base) + def existing_controller_setting + @existing_controller_setting ||= fetch_controller_settings[0] end end end diff --git a/lib/uffizzi/cli/uninstall.rb b/lib/uffizzi/cli/uninstall.rb new file mode 100644 index 00000000..ce5d21c1 --- /dev/null +++ b/lib/uffizzi/cli/uninstall.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require 'uffizzi' +require 'uffizzi/config_file' +require 'uffizzi/services/install_service' +require 'uffizzi/services/kubeconfig_service' + +module Uffizzi + class Cli::Uninstall < Thor + include ApiClient + + default_task :controller + + desc 'controller [HOSTNMAE]', 'Install uffizzi controller to cluster' + method_option :namespace, type: :string + method_option :context, type: :string + def controller + Uffizzi::AuthHelper.check_login + + InstallService.kubectl_exists? + InstallService.helm_exists? + + if options[:context].present? && options[:context] != InstallService.kubeconfig_current_context + InstallService.set_current_context(options[:context]) + end + + ask_confirmation + delete_controller_settings + InstallService.helm_uninstall!(namespace) + + helm_unset_repo + end + + private + + def helm_unset_repo + return if InstallService.helm_repo_search.blank? + + InstallService.helm_repo_remove + end + + def ask_confirmation + msg = "This command will uninstall Uffizzi from the '#{namespace}'"\ + " namespace of the '#{InstallService.kubeconfig_current_context}' context."\ + "\r\n"\ + "To uninstall a different installation, use options '--namespace' and/or '--context'."\ + "\r\n\r\n"\ + "After uninstalling, new environments created for account '#{account_name}'"\ + "\r\n"\ + 'will be deployed to Uffizzi Cloud (app.uffizzi.com).'\ + "\r\n\r\n" + + Uffizzi.ui.say(msg) + + question = 'Okay to proceed?' + Uffizzi.ui.say_error_and_exit('Uninstallation canceled') unless Uffizzi.prompt.yes?(question) + end + + def fetch_controller_settings + response = get_account_controller_settings(server, account_id) + return Uffizzi::ResponseHelper.handle_failed_response(response) unless Uffizzi::ResponseHelper.ok?(response) + + response.dig(:body, :controller_settings) + end + + def delete_controller_settings + return if existing_controller_setting.blank? + + response = delete_account_controller_settings(server, account_id, existing_controller_setting[:id]) + + if ResponseHelper.no_content?(response) + Uffizzi.ui.say('Controller settings deleted') + else + ResponseHelper.handle_failed_response(response) + end + end + + def namespace + options[:namespace] || InstallService::DEFAULT_NAMESPACE + end + + def server + @server ||= ConfigFile.read_option(:server) + end + + def account_id + @account_id ||= ConfigFile.read_option(:account, :id) + end + + def account_name + @account_name ||= ConfigFile.read_option(:account, :name) + end + + def existing_controller_setting + @existing_controller_setting ||= fetch_controller_settings[0] + end + end +end diff --git a/lib/uffizzi/clients/api/api_client.rb b/lib/uffizzi/clients/api/api_client.rb index 58ab5a6f..361a513d 100644 --- a/lib/uffizzi/clients/api/api_client.rb +++ b/lib/uffizzi/clients/api/api_client.rb @@ -321,6 +321,34 @@ def get_account_clusters(server, account_id) build_response(response) end + def get_account_controller_settings(server, account_id) + uri = account_controller_settings_uri(server, account_id) + response = http_client.make_get_request(uri) + + build_response(response) + end + + def create_account_controller_settings(server, account_id, params = {}) + uri = account_controller_settings_uri(server, account_id) + response = http_client.make_post_request(uri, params) + + build_response(response) + end + + def update_account_controller_settings(server, account_id, id, params = {}) + uri = account_controller_setting_uri(server, account_id, id) + response = http_client.make_put_request(uri, params) + + build_response(response) + end + + def delete_account_controller_settings(server, account_id, id) + uri = account_controller_setting_uri(server, account_id, id) + response = http_client.make_delete_request(uri) + + build_response(response) + end + private def http_client diff --git a/lib/uffizzi/clients/api/api_routes.rb b/lib/uffizzi/clients/api/api_routes.rb index 2727ba0a..6ff4b82d 100644 --- a/lib/uffizzi/clients/api/api_routes.rb +++ b/lib/uffizzi/clients/api/api_routes.rb @@ -139,4 +139,12 @@ def browser_sign_in_url(server, session_id) def account_clusters_uri(server, account_id) "#{server}/api/cli/v1/accounts/#{account_id}/clusters" end + + def account_controller_settings_uri(server, account_id) + "#{server}/api/cli/v1/accounts/#{account_id}/controller_settings" + end + + def account_controller_setting_uri(server, account_id, id) + "#{server}/api/cli/v1/accounts/#{account_id}/controller_settings/#{id}" + end end diff --git a/lib/uffizzi/services/install_service.rb b/lib/uffizzi/services/install_service.rb new file mode 100644 index 00000000..a0a2c989 --- /dev/null +++ b/lib/uffizzi/services/install_service.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true + +require 'uffizzi/response_helper' +require 'uffizzi/clients/api/api_client' + +class InstallService + DEFAULT_HELM_RELEASE_NAME = 'uffizzi' + INGRESS_NAME = "#{DEFAULT_HELM_RELEASE_NAME}-controller" + DEFAULT_HELM_REPO_NAME = 'uffizzi' + DEFAULT_CONTROLLER_CHART_NAME = 'uffizzi-controller' + HELM_DEPLOYED_STATUS = 'deployed' + VALUES_FILE_NAME = 'helm_values.yaml' + DEFAULT_NAMESPACE = 'default' + DEFAULT_CLUSTER_ISSUER = 'letsencrypt' + DEFAULT_CONTROLLER_REPO_URL = 'https://uffizzicloud.github.io/uffizzi_controller' + DEFAULT_CONTROLLER_DOMAIN_PREFIX = 'controller' + + class << self + include ApiClient + + def kubectl_exists? + cmd = 'kubectl version -o json' + execute_command(cmd, say: false).present? + end + + def helm_exists? + cmd = 'helm version --short' + execute_command(cmd, say: false).present? + end + + def helm_repo_remove + cmd = "helm repo remove #{DEFAULT_HELM_REPO_NAME}" + execute_command(cmd, skip_error: true) + end + + def helm_repo_search + cmd = "helm search repo #{DEFAULT_HELM_REPO_NAME}/#{DEFAULT_CONTROLLER_CHART_NAME} -o json" + + execute_command(cmd) do |result, err| + err.present? ? nil : JSON.parse(result) + end + end + + def helm_repo_add(repo_url) + repo_url = repo_url || DEFAULT_CONTROLLER_REPO_URL + cmd = "helm repo add #{DEFAULT_HELM_REPO_NAME} #{repo_url}" + execute_command(cmd) + end + + def helm_install!(namespace) + Uffizzi.ui.say('Start helm release installation') + + repo = "#{DEFAULT_HELM_REPO_NAME}/#{DEFAULT_CONTROLLER_CHART_NAME}" + cmd = "helm upgrade #{DEFAULT_HELM_RELEASE_NAME} #{repo}" \ + " --values #{helm_values_file_path}" \ + " --namespace #{namespace}" \ + ' --create-namespace' \ + ' --install' \ + ' --output json' + + res = execute_command(cmd, say: false) + info = JSON.parse(res)['info'] + + return if info['status'] == HELM_DEPLOYED_STATUS + + Uffizzi.ui.say_error_and_exit(info) + end + + def helm_uninstall!(namespace) + Uffizzi.ui.say('Start helm release uninstallation') + + cmd = "helm uninstall #{DEFAULT_HELM_RELEASE_NAME} --namespace #{namespace}" + + execute_command(cmd) + end + + def set_current_context(context) + cmd = "kubectl config use-context #{context}" + execute_command(cmd) + end + + def kubeconfig_current_context + cmd = 'kubectl config current-context' + + execute_command(cmd, say: false) { |stdout| stdout.present? && stdout.chop } + end + + def get_controller_ip(namespace) + cmd = "kubectl get ingress -n #{namespace} -o json" + res = execute_command(cmd, say: false) + ingress = JSON.parse(res)['items'].detect { |i| i['metadata']['name'] = INGRESS_NAME } + + return if ingress.blank? + + load_balancers = ingress.dig('status', 'loadBalancer', 'ingress') + return if load_balancers.blank? + + load_balancers.map { |i| i['ip'] }[0] + end + + def get_certificate_request(namespace, uri) + cmd = "kubectl get certificaterequests -n #{namespace} -o json" + res = execute_command(cmd, say: false) + certificate_request = JSON.parse(res)['items'].detect { |i| i['metadata']['name'].include?(uri.host) } + + return if certificate_request.nil? + + conditions = certificate_request.dig('status', 'conditions') || [] + conditions.map { |c| c.slice('type', 'status') } + end + + def build_controller_host(host) + [DEFAULT_CONTROLLER_DOMAIN_PREFIX, host].join('.') + end + + def delete_helm_values_file + File.delete(helm_values_file_path) if File.exist?(helm_values_file_path) + end + + def create_helm_values_file(values) + FileUtils.mkdir_p(helm_values_dir_path) unless File.directory?(helm_values_dir_path) + File.write(helm_values_file_path, values.to_yaml) + end + + def helm_values_file_path + File.join(helm_values_dir_path, VALUES_FILE_NAME) + end + + def helm_values_dir_path + File.dirname(Uffizzi::ConfigFile.config_path) + end + + private + + def execute_command(command, say: true, skip_error: false) + stdout_str, stderr_str, status = Uffizzi.ui.capture3(command) + + return yield(stdout_str, stderr_str) if block_given? + + if !status.success? && !skip_error + return Uffizzi.ui.say_error_and_exit(stderr_str) + end + + if !status.success? && skip_error + return Uffizzi.ui.say(stderr_str) + end + + say ? Uffizzi.ui.say(stdout_str) : stdout_str + rescue Errno::ENOENT => e + Uffizzi.ui.say_error_and_exit(e.message) + end + end +end diff --git a/test/fixtures/files/uffizzi/uffizzi_account_controller_settings.json b/test/fixtures/files/uffizzi/uffizzi_account_controller_settings.json new file mode 100644 index 00000000..53798468 --- /dev/null +++ b/test/fixtures/files/uffizzi/uffizzi_account_controller_settings.json @@ -0,0 +1,9 @@ +{ + "controller_settings": [ + { + "id": 1, + "url": "https://controller.my-host.com", + "managed_dns_zone": "my-host.com" + } + ] +} diff --git a/test/fixtures/files/uffizzi/uffizzi_account_controller_settings_empty.json b/test/fixtures/files/uffizzi/uffizzi_account_controller_settings_empty.json new file mode 100644 index 00000000..e7394711 --- /dev/null +++ b/test/fixtures/files/uffizzi/uffizzi_account_controller_settings_empty.json @@ -0,0 +1,3 @@ +{ + "controller_settings": [] +} diff --git a/test/support/mocks/mock_shell.rb b/test/support/mocks/mock_shell.rb index dc03dec7..ce1cf024 100644 --- a/test/support/mocks/mock_shell.rb +++ b/test/support/mocks/mock_shell.rb @@ -2,6 +2,7 @@ class MockShell class ExitError < StandardError; end + class MockProcessStatus def initialize(success) @success = success @@ -108,23 +109,6 @@ def promise_execute(command, stdout: nil, stderr: nil, waiter: nil) private - def get_command_response(command) - response_index = @command_responses.index do |command_response| - case command_response[:command] - when Regexp - command_response[:command].match?(command) - else - command_response[:command] == command - end - end - - stdout = @command_responses[response_index].fetch(:stdout) - stderr = @command_responses[response_index].fetch(:stderr) - @command_responses.delete_at(response_index) - - [stdout, stderr] - end - def format_to_json(data) data.to_json end diff --git a/test/support/uffizzi_stub_support.rb b/test/support/uffizzi_stub_support.rb index d343ea36..69e45cd2 100644 --- a/test/support/uffizzi_stub_support.rb +++ b/test/support/uffizzi_stub_support.rb @@ -181,13 +181,6 @@ def stub_uffizzi_create_cluster(body, project_slug) stub_request(:post, uri).to_return(status: 201, body: body.to_json) end - def stub_get_cluster_request(body, project_slug) - uri = cluster_uri(Uffizzi.configuration.server, project_slug, cluster_name: nil, oidc_token: nil) - uri = %r{#{uri}([A-Za-z0-9\-_]+)} - - stub_request(:get, uri).to_return(status: 200, body: body.to_json) - end - def stub_get_cluster_ingresses_request(body, project_slug, cluster_name) uri = project_cluster_ingresses_uri(Uffizzi.configuration.server, project_slug, cluster_name: cluster_name, oidc_token: nil) @@ -229,4 +222,28 @@ def stub_create_token_request(body) stub_request(:post, uri).to_return(status: 201, body: body.to_json) end + + def stub_get_account_controller_settings_request(body, account_id) + uri = account_controller_settings_uri(Uffizzi.configuration.server, account_id) + + stub_request(:get, uri).to_return(status: 200, body: body.to_json) + end + + def stub_create_account_controller_settings_request(body, account_id) + uri = account_controller_settings_uri(Uffizzi.configuration.server, account_id) + + stub_request(:post, uri).to_return(status: 201, body: body.to_json) + end + + def stub_update_account_controller_settings_request(body, account_id, controller_settings_id) + uri = account_controller_setting_uri(Uffizzi.configuration.server, account_id, controller_settings_id) + + stub_request(:put, uri).to_return(status: 200, body: body.to_json) + end + + def stub_delete_account_controller_settings_request(account_id, controller_settings_id) + uri = account_controller_setting_uri(Uffizzi.configuration.server, account_id, controller_settings_id) + + stub_request(:delete, uri).to_return(status: 204, body: '') + end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 34128679..3764049a 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -85,4 +85,8 @@ def command_options(options) def render_server_error(error) "#{Uffizzi::RESPONSE_SERVER_ERROR_HEADER}#{error}" end + + def tmp_dir_name + (Time.now.utc.to_f * 100_000).to_i + end end diff --git a/test/uffizzi/cli/install_test.rb b/test/uffizzi/cli/install_test.rb index 5971ca6f..c96e5f7d 100644 --- a/test/uffizzi/cli/install_test.rb +++ b/test/uffizzi/cli/install_test.rb @@ -8,44 +8,114 @@ class InstallTest < Minitest::Test def setup @install = Uffizzi::Cli::Install.new - tmp_dir_name = (Time.now.utc.to_f * 100_000).to_i - helm_values_path = "/tmp/test/#{tmp_dir_name}/helm_values.yaml" - Uffizzi::ConfigFile.stubs(:config_path).returns(helm_values_path) + sign_in + Uffizzi::ConfigFile.write_option(:project, 'uffizzi') + helm_values_dir_path = "/tmp/test/#{tmp_dir_name}" + InstallService.stubs(:helm_values_dir_path).returns(helm_values_dir_path) end - def test_install_by_wizard - @mock_prompt.promise_question_answer('Namespace: ', 'uffizzi') - @mock_prompt.promise_question_answer('Root domain: ', 'my-domain.com') - @mock_prompt.promise_question_answer('First user email: ', 'admin@my-domain.com') - @mock_prompt.promise_question_answer('First user password: ', 'password') - @mock_prompt.promise_question_answer('Uffizzi use a wildcard tls certificate. Do you have it?', 'n') + def test_install + host = 'my-host.com' + account_id = 1 @mock_shell.promise_execute(/kubectl version/, stdout: '1.23.00') @mock_shell.promise_execute(/helm version/, stdout: '3.00') @mock_shell.promise_execute(/helm search repo/, stdout: [].to_json) @mock_shell.promise_execute(/helm repo add/, stdout: 'ok') - @mock_shell.promise_execute(/helm list/, stdout: [].to_json) + @mock_shell.promise_execute(/kubectl config current-context/, stdout: 'my-context') @mock_shell.promise_execute(/helm upgrade/, stdout: { info: { status: 'deployed' } }.to_json) - @mock_shell.promise_execute(/kubectl get ingress/, stdout: { status: { loadBalancer: { ingress: [{ ip: '34.31.68.232' }] } } }.to_json) + ingress_answer = { + items: [ + metadata: { + name: InstallService::INGRESS_NAME, + }, + status: { + loadBalancer: { + ingress: [{ ip: '34.31.68.232' }], + }, + }, + ], + } - @install.application + cert_request_answer = { + items: [ + metadata: { + name: host, + }, + status: { + conditions: [ + { type: 'Approved', status: 'True' }, + { type: 'Ready', status: 'True' }, + ], + }, + ], + } + + @mock_shell.promise_execute(/kubectl get ingress/, stdout: ingress_answer.to_json) + @mock_shell.promise_execute(/kubectl get certificaterequests/, stdout: cert_request_answer.to_json) + @mock_prompt.promise_question_answer('Okay to proceed?', 'y') + + empty_body = json_fixture('files/uffizzi/uffizzi_account_controller_settings_empty.json') + stub_get_account_controller_settings_request(empty_body, account_id) + stub_create_account_controller_settings_request({}, account_id) + + @install.options = command_options(email: 'admin@my-domain.com') + @install.controller(host) last_message = Uffizzi.ui.last_message - assert_match('Create a DNS A record for domain', last_message) + assert_match('Your Uffizzi controller is ready', last_message) end - def test_install_by_options + def test_install_if_settgins_exists + host = 'my-host.com' + account_id = 1 + @mock_shell.promise_execute(/kubectl version/, stdout: '1.23.00') @mock_shell.promise_execute(/helm version/, stdout: '3.00') @mock_shell.promise_execute(/helm search repo/, stdout: [].to_json) @mock_shell.promise_execute(/helm repo add/, stdout: 'ok') + @mock_shell.promise_execute(/kubectl config current-context/, stdout: 'my-context') @mock_shell.promise_execute(/helm upgrade/, stdout: { info: { status: 'deployed' } }.to_json) - @mock_shell.promise_execute(/kubectl get ingress/, stdout: { status: { loadBalancer: { ingress: [{ ip: '34.31.68.232' }] } } }.to_json) + ingress_answer = { + items: [ + metadata: { + name: InstallService::INGRESS_NAME, + }, + status: { + loadBalancer: { + ingress: [{ ip: '34.31.68.232' }], + }, + }, + ], + } + + cert_request_answer = { + items: [ + metadata: { + name: host, + }, + status: { + conditions: [ + { type: 'Approved', status: 'True' }, + { type: 'Ready', status: 'True' }, + ], + }, + ], + } + + @mock_shell.promise_execute(/kubectl get ingress/, stdout: ingress_answer.to_json) + @mock_shell.promise_execute(/kubectl get certificaterequests/, stdout: cert_request_answer.to_json) + @mock_prompt.promise_question_answer('Okay to proceed?', 'y') + @mock_prompt.promise_question_answer('Do you want update the controller settings?', 'y') + + body = json_fixture('files/uffizzi/uffizzi_account_controller_settings.json') + stub_get_account_controller_settings_request(body, account_id) + stub_update_account_controller_settings_request(body, account_id, body[:controller_settings][0][:id]) - @install.options = command_options(domain: 'my-domain.com', 'without-wildcard-tls' => true) - @install.application + @install.options = command_options(email: 'admin@my-domain.com') + @install.controller(host) last_message = Uffizzi.ui.last_message - assert_match('Create a DNS A record for domain', last_message) + assert_match('Your Uffizzi controller is ready', last_message) end end diff --git a/test/uffizzi/cli/uninstall_test.rb b/test/uffizzi/cli/uninstall_test.rb new file mode 100644 index 00000000..0dfd3ec6 --- /dev/null +++ b/test/uffizzi/cli/uninstall_test.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'byebug' +require 'psych' +require 'base64' +require 'test_helper' + +class UninstallTest < Minitest::Test + def setup + @uninstall = Uffizzi::Cli::Uninstall.new + + sign_in + Uffizzi::ConfigFile.write_option(:project, 'uffizzi') + helm_values_dir_path = "/tmp/test/#{tmp_dir_name}" + InstallService.stubs(:helm_values_dir_path).returns(helm_values_dir_path) + end + + def test_uninstall + account_id = 1 + + @mock_shell.promise_execute(/kubectl version/, stdout: '1.23.00') + @mock_shell.promise_execute(/helm version/, stdout: '3.00') + @mock_shell.promise_execute(/helm search repo/, stdout: [].to_json) + @mock_shell.promise_execute(/helm repo add/, stdout: 'ok') + @mock_shell.promise_execute(/kubectl config current-context/, stdout: 'my-context') + @mock_shell.promise_execute(/helm uninstall/, stdout: 'Helm release is uninstalled') + @mock_prompt.promise_question_answer('Okay to proceed?', 'y') + + body = json_fixture('files/uffizzi/uffizzi_account_controller_settings.json') + stub_get_account_controller_settings_request(body, account_id) + stub_delete_account_controller_settings_request(account_id, body[:controller_settings][0][:id]) + + @uninstall.controller + + last_message = Uffizzi.ui.last_message + assert_match('Helm release is uninstalled', last_message) + end +end