From 06a2197eab1fdfd5df90b72ded19d68cdc7ed012 Mon Sep 17 00:00:00 2001 From: Zipofar Date: Fri, 6 Oct 2023 19:07:09 +0300 Subject: [PATCH] [343] Add dev describe --- Makefile | 6 ++ lib/uffizzi/cli/cluster.rb | 111 +++++++++++------------- lib/uffizzi/cli/dev.rb | 77 ++++++++++++++-- lib/uffizzi/helpers/config_helper.rb | 17 ++++ lib/uffizzi/services/cluster_service.rb | 28 ++++++ man/uffizzi | 5 +- man/uffizzi-dev-describe | 35 ++++++++ man/uffizzi-dev-describe.ronn | 26 ++++++ man/uffizzi.ronn | 3 + test/support/mocks/mock_prompt.rb | 11 ++- test/uffizzi/cli/dev_test.rb | 76 ++++++++++++++++ 11 files changed, 326 insertions(+), 69 deletions(-) create mode 100644 man/uffizzi-dev-describe create mode 100644 man/uffizzi-dev-describe.ronn diff --git a/Makefile b/Makefile index 68583f56..2826148b 100644 --- a/Makefile +++ b/Makefile @@ -67,4 +67,10 @@ gem_build_install: gem_uninstall: gem uninstall uffizzi-cli +brew_add_tap: + brew tap UffizziCloud/tap + +brew_tap_install: + brew install uffizzicloud/tap/uffizzi + .PHONY: test diff --git a/lib/uffizzi/cli/cluster.rb b/lib/uffizzi/cli/cluster.rb index 02bbd31f..d1adb1fd 100644 --- a/lib/uffizzi/cli/cluster.rb +++ b/lib/uffizzi/cli/cluster.rb @@ -68,31 +68,28 @@ def run(command, command_args = {}) raise Uffizzi::Error.new('You are not logged in.') unless Uffizzi::AuthHelper.signed_in? raise Uffizzi::Error.new('This command needs project to be set in config file') unless CommandService.project_set?(options) - project_slug = options[:project].nil? ? ConfigFile.read_option(:project) : options[:project] - case command when 'list' - handle_list_command(project_slug) + handle_list_command when 'create' - handle_create_command(project_slug, command_args) + handle_create_command(command_args) when 'describe' - handle_describe_command(project_slug, command_args) + handle_describe_command(command_args) when 'delete' - handle_delete_command(project_slug, command_args) + handle_delete_command(command_args) when 'update-kubeconfig' - handle_update_kubeconfig_command(project_slug, command_args) + handle_update_kubeconfig_command(command_args) when 'disconnect' ClusterDisconnectService.handle(options) end end - def handle_list_command(project_slug) + def handle_list_command is_all = options[:all] response = if is_all - get_account_clusters(ConfigFile.read_option(:server), ConfigFile.read_option(:account, :id)) + get_account_clusters(server, ConfigFile.read_option(:account, :id)) else - oidc_token = ConfigFile.read_option(:oidc_token) - get_project_clusters(ConfigFile.read_option(:server), project_slug, oidc_token: oidc_token) + get_project_clusters(server, project_slug, oidc_token: oidc_token) end if ResponseHelper.ok?(response) @@ -103,7 +100,7 @@ def handle_list_command(project_slug) end # rubocop:disable Metrics/PerceivedComplexity - def handle_create_command(project_slug, command_args) + def handle_create_command(command_args) Uffizzi.ui.disable_stdout if Uffizzi.ui.output_format if options[:name] @@ -121,13 +118,13 @@ def handle_create_command(project_slug, command_args) manifest_file_path = options[:manifest] params = cluster_creation_params(cluster_name, creation_source, manifest_file_path) - response = create_cluster(ConfigFile.read_option(:server), project_slug, params) + response = create_cluster(server, project_slug, params) return ResponseHelper.handle_failed_response(response) unless ResponseHelper.created?(response) spinner = TTY::Spinner.new("[:spinner] Creating cluster #{cluster_name}...", format: :dots) spinner.auto_spin - cluster_data = ClusterService.wait_cluster_deploy(project_slug, cluster_name, ConfigFile.read_option(:oidc_token)) + cluster_data = ClusterService.wait_cluster_deploy(project_slug, cluster_name, oidc_token) if ClusterService.failed?(cluster_data[:state]) spinner.error @@ -137,26 +134,31 @@ def handle_create_command(project_slug, command_args) spinner.success handle_succeed_create_response(cluster_data) rescue SystemExit, Interrupt, SocketError - handle_interrupt_creation(cluster_name, ConfigFile.read_option(:server), project_slug) + handle_interrupt_creation(cluster_name) end # rubocop:enable Metrics/PerceivedComplexity - def handle_describe_command(project_slug, command_args) - cluster_data = fetch_cluster_data(project_slug, command_args[:cluster_name]) + def handle_describe_command(command_args) + cluster_data = ClusterService.fetch_cluster_data(command_args[:cluster_name], **cluster_api_connection_params) + render_data = ClusterService.build_render_data(cluster_data) - handle_succeed_describe(cluster_data) + if Uffizzi.ui.output_format.nil? + Uffizzi.ui.say(ClusterService.stringify_render_data(render_data)) + else + Uffizzi.ui.say(render_data) + end end - def handle_delete_command(project_slug, command_args) + def handle_delete_command(command_args) cluster_name = command_args[:cluster_name] is_delete_kubeconfig = options[:'delete-config'] - return handle_delete_cluster(project_slug, cluster_name) unless is_delete_kubeconfig + return handle_delete_cluster(cluster_name) unless is_delete_kubeconfig - cluster_data = fetch_cluster_data(project_slug, cluster_name) + cluster_data = ClusterService.fetch_cluster_data(cluster_name, **cluster_api_connection_params) kubeconfig = parse_kubeconfig(cluster_data[:kubeconfig]) - handle_delete_cluster(project_slug, cluster_name) + handle_delete_cluster(cluster_name) exclude_kubeconfig(cluster_data[:id], kubeconfig) if kubeconfig.present? end @@ -180,12 +182,12 @@ def exclude_kubeconfig(cluster_id, kubeconfig) end end - def handle_delete_cluster(project_slug, cluster_name) + def handle_delete_cluster(cluster_name) params = { cluster_name: cluster_name, - oidc_token: ConfigFile.read_option(:oidc_token), + oidc_token: oidc_token, } - response = delete_cluster(ConfigFile.read_option(:server), project_slug, params) + response = delete_cluster(server, project_slug, params) if ResponseHelper.no_content?(response) Uffizzi.ui.say("Cluster #{cluster_name} deleted") @@ -194,9 +196,9 @@ def handle_delete_cluster(project_slug, cluster_name) end end - def handle_update_kubeconfig_command(project_slug, command_args) + def handle_update_kubeconfig_command(command_args) kubeconfig_path = options[:kubeconfig] || KubeconfigService.default_path - cluster_data = fetch_cluster_data(project_slug, command_args[:cluster_name]) + cluster_data = ClusterService.fetch_cluster_data(command_args[:cluster_name], **cluster_api_connection_params) unless cluster_data[:kubeconfig].present? say_error_update_kubeconfig(cluster_data) @@ -241,8 +243,6 @@ def say_error_update_kubeconfig(cluster_data) def cluster_creation_params(name, creation_source, manifest_file_path) manifest_content = load_manifest_file(manifest_file_path) - oidc_token = Uffizzi::ConfigFile.read_option(:oidc_token) - { cluster: { name: name, @@ -261,7 +261,7 @@ def load_manifest_file(file_path) raise Uffizzi::Error.new(e.message) end - def handle_interrupt_creation(cluster_name, server, project_slug) + def handle_interrupt_creation(cluster_name) deletion_response = delete_cluster(server, project_slug, cluster_name: cluster_name) deletion_message = if ResponseHelper.no_content?(deletion_response) "The cluster #{cluster_name} has been disabled." @@ -297,23 +297,6 @@ def render_plain_cluster_list(clusters) end.join("\n") end - def handle_succeed_describe(cluster_data) - prepared_cluster_data = { - name: cluster_data[:name], - status: cluster_data[:state], - created: Time.strptime(cluster_data[:created_at], '%Y-%m-%dT%H:%M:%S.%N').strftime('%a %b %d %H:%M:%S %Y'), - url: cluster_data[:host], - } - - rendered_cluster_data = if Uffizzi.ui.output_format.nil? - prepared_cluster_data.map { |k, v| "- #{k.to_s.upcase}: #{v}" }.join("\n").strip - else - prepared_cluster_data - end - - Uffizzi.ui.say(rendered_cluster_data) - end - def handle_succeed_create_response(cluster_data) kubeconfig_path = options[:kubeconfig] || KubeconfigService.default_path is_update_current_context = options[:'update-current-context'] @@ -377,25 +360,31 @@ def parse_kubeconfig(kubeconfig) Psych.safe_load(Base64.decode64(kubeconfig)) end - def fetch_cluster_data(project_slug, cluster_name) - params = { - cluster_name: cluster_name, - oidc_token: ConfigFile.read_option(:oidc_token), - } - response = get_cluster(ConfigFile.read_option(:server), project_slug, params) - - if ResponseHelper.ok?(response) - response.dig(:body, :cluster) - else - ResponseHelper.handle_failed_response(response) - end - end - def save_previous_current_context(kubeconfig_path, current_context) return if kubeconfig_path.nil? || ConfigHelper.previous_current_context_by_path(kubeconfig_path).present? previous_current_contexts = Uffizzi::ConfigHelper.set_previous_current_context_by_path(kubeconfig_path, current_context) ConfigFile.write_option(:previous_current_contexts, previous_current_contexts) end + + def cluster_api_connection_params + { + server: server, + project_slug: project_slug, + oidc_token: oidc_token, + } + end + + def oidc_token + @oidc_token ||= ConfigFile.read_option(:oidc_token) + end + + def project_slug + @project_slug ||= ConfigFile.read_option(:project) + end + + def server + @server ||= ConfigFile.read_option(:server) + end end end diff --git a/lib/uffizzi/cli/dev.rb b/lib/uffizzi/cli/dev.rb index a41bbdb0..d6698c75 100644 --- a/lib/uffizzi/cli/dev.rb +++ b/lib/uffizzi/cli/dev.rb @@ -20,6 +20,7 @@ def start(config_path = 'skaffold.yaml') check_login cluster_id, cluster_name = start_create_cluster kubeconfig = wait_cluster_creation(cluster_name) + save_config_dev_environment(cluster_name, config_path) if options[:quiet] launch_demonise_skaffold(config_path) @@ -30,6 +31,7 @@ def start(config_path = 'skaffold.yaml') if defined?(cluster_name).present? && defined?(cluster_id).present? kubeconfig = defined?(kubeconfig).present? ? kubeconfig : nil handle_delete_cluster(cluster_id, cluster_name, kubeconfig) + delete_config_dev_environment(cluster_name) end end @@ -47,6 +49,26 @@ def stop File.delete(DevService.pid_path) end + desc 'describe [NAME]', 'Describe dev environment' + def describe(name = nil) + check_login + dev_environment = get_dev_environment(name) + + if dev_environment.nil? && name.present? + return Uffizzi.ui.say("No running dev environment by name: #{name}") + elsif dev_environment.nil? + return Uffizzi.ui.say('No running dev environments') + end + + cluster_name = dev_environment[:name] + cluster_data = ClusterService.fetch_cluster_data(cluster_name, **cluster_api_connection_params) + cluster_render_data = ClusterService.build_render_data(cluster_data) + dev_environment_render_data = cluster_render_data.merge(config_path: dev_environment[:config_path]) + rendered_data = dev_environment_render_data.map { |k, v| "- #{k.to_s.upcase}: #{v}" }.join("\n").strip + + Uffizzi.ui.say(rendered_data) + end + private def check_login @@ -59,7 +81,7 @@ def start_create_cluster creation_source = ClusterService::MANUAL_CREATION_SOURCE params = cluster_creation_params(cluster_name, creation_source) Uffizzi.ui.say('Start creating a cluster') - response = create_cluster(ConfigFile.read_option(:server), project_slug, params) + response = create_cluster(server, project_slug, params) return ResponseHelper.handle_failed_response(response) unless ResponseHelper.created?(response) cluster_id = response.dig(:body, :cluster, :id) @@ -70,7 +92,7 @@ def start_create_cluster def wait_cluster_creation(cluster_name) Uffizzi.ui.say('Checking the cluster status...') - cluster_data = ClusterService.wait_cluster_deploy(project_slug, cluster_name, ConfigFile.read_option(:oidc_token)) + cluster_data = ClusterService.wait_cluster_deploy(project_slug, cluster_name, oidc_token) if ClusterService.failed?(cluster_data[:state]) Uffizzi.ui.say_error_and_exit("Cluster with name: #{cluster_name} failed to be created.") @@ -111,8 +133,6 @@ def update_clusters_config(id, params) end def cluster_creation_params(name, creation_source) - oidc_token = Uffizzi::ConfigFile.read_option(:oidc_token) - { cluster: { name: name, @@ -130,9 +150,9 @@ def handle_delete_cluster(cluster_id, cluster_name, kubeconfig) params = { cluster_name: cluster_name, - oidc_token: ConfigFile.read_option(:oidc_token), + oidc_token: oidc_token, } - response = delete_cluster(ConfigFile.read_option(:server), project_slug, params) + response = delete_cluster(server, project_slug, params) if ResponseHelper.no_content?(response) Uffizzi.ui.say("Cluster #{cluster_name} deleted") @@ -191,8 +211,53 @@ def launch_demonise_skaffold(config_path) File.open(DevService.logs_path, 'a') { |f| f.puts(e.message) } end + def save_config_dev_environment(cluster_name, config_path) + params = options.merge(config_path: File.expand_path(config_path)) + dev_environments = Uffizzi::ConfigHelper.set_dev_environment(cluster_name, params) + ConfigFile.write_option(:dev_environments, dev_environments) + end + + def delete_config_dev_environment(cluster_name) + dev_environments = Uffizzi::ConfigHelper.dev_environments_without(cluster_name) + ConfigFile.write_option(:dev_environments, dev_environments) + end + + def get_dev_environment(name) + dev_environments = ConfigHelper.dev_environments + + if name.present? + ConfigHelper.dev_environments_by_name(name) + elsif dev_environments.count == 1 + dev_environments.last + elsif dev_environments.count > 1 + choices = dev_environments.map do |dev_env| + { name: dev_env[:config_path], value: dev_env[:name] } + end + + question = 'You have several dev environments, select one for describe:' + answer = Uffizzi.prompt.select(question, choices) + ConfigHelper.dev_environments_by_name(answer) + end + end + + def cluster_api_connection_params + { + server: server, + project_slug: project_slug, + oidc_token: oidc_token, + } + end + def project_slug @project_slug ||= ConfigFile.read_option(:project) end + + def oidc_token + @oidc_token ||= ConfigFile.read_option(:oidc_token) + end + + def server + @server ||= ConfigFile.read_option(:server) + end end end diff --git a/lib/uffizzi/helpers/config_helper.rb b/lib/uffizzi/helpers/config_helper.rb index 88f61b3a..65a6fb0b 100644 --- a/lib/uffizzi/helpers/config_helper.rb +++ b/lib/uffizzi/helpers/config_helper.rb @@ -52,6 +52,23 @@ def previous_current_context_by_path(path) cluster_previous_current_contexts.detect { |c| c[:kubeconfig_path] == path } end + def set_dev_environment(name, params = {}) + current_dev_environments = dev_environments_without(name) + current_dev_environments << { name: name }.merge(params) + end + + def dev_environments_without(name) + dev_environments.reject { |c| c[:name] == name } + end + + def dev_environments_by_name(name) + dev_environments.detect { |c| c[:name] == name } + end + + def dev_environments + read_option_from_config(:dev_environments) || [] + end + private def clusters diff --git a/lib/uffizzi/services/cluster_service.rb b/lib/uffizzi/services/cluster_service.rb index 283d2d57..1ac1bb8a 100644 --- a/lib/uffizzi/services/cluster_service.rb +++ b/lib/uffizzi/services/cluster_service.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require 'uffizzi/response_helper' require 'uffizzi/clients/api/api_client' class ClusterService @@ -57,5 +58,32 @@ def valid_name?(name) regex = /\A[a-zA-Z0-9-]*\z/ regex.match?(name) end + + def fetch_cluster_data(cluster_name, server:, project_slug:, oidc_token:) + params = { + cluster_name: cluster_name, + oidc_token: oidc_token, + } + response = get_cluster(server, project_slug, params) + + if Uffizzi::ResponseHelper.ok?(response) + response.dig(:body, :cluster) + else + Uffizzi::ResponseHelper.handle_failed_response(response) + end + end + + def build_render_data(cluster_data) + { + name: cluster_data[:name], + status: cluster_data[:state], + created: Time.strptime(cluster_data[:created_at], '%Y-%m-%dT%H:%M:%S.%N').strftime('%a %b %d %H:%M:%S %Y'), + url: cluster_data[:host], + } + end + + def stringify_render_data(data) + data.map { |k, v| "- #{k.to_s.upcase}: #{v}" }.join("\n").strip + end end end diff --git a/man/uffizzi b/man/uffizzi index 99d483b6..283a84c3 100644 --- a/man/uffizzi +++ b/man/uffizzi @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.9.1 .\" http://github.com/apjanke/ronn-ng/tree/0.9.1 -.TH "UFFIZZI" "" "September 2023" "" +.TH "UFFIZZI" "" "October 2023" "" .SH "NAME" \fBuffizzi\fR \- manage Uffizzi resources .SH "SYNOPSIS" @@ -33,6 +33,9 @@ GROUP is one of the following: project Manage Uffizzi project resources including compose files for specifying compose environment (preview) configurations and secrets + + dev + Creates a Uffizzi cluster preconfigured for development workflows .fi .SH "COMMAND" .nf diff --git a/man/uffizzi-dev-describe b/man/uffizzi-dev-describe new file mode 100644 index 00000000..235021fe --- /dev/null +++ b/man/uffizzi-dev-describe @@ -0,0 +1,35 @@ +.\" generated with Ronn-NG/v0.9.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.9.1 +.TH "UFFIZZI\-DEV\-DESCRIBE" "" "October 2023" "" +.SH "NAME" +\fBuffizzi\-dev\-describe\fR +.P +$ uffizzi dev describe \-h uffizzi\-dev\-describe \- show metadata for a dev environment ================================================================ +.SH "SYNOPSIS" +.nf +uffizzi dev describe [NAME] +.fi +.SH "DESCRIPTION" +.nf +Shows metadata for a dev environment + +This command can fail for the following reasons: + \- The dev environment specified does not exist\. + \- The dev environment specified belongs to a different project\. + +For more information on Uffizzi clusters, see: +https://docs\.uffizzi\.com/references/cli/ +.fi +.SH "POSITIONAL ARGUMENTS" +.nf +[NAME] + NAME for the dev environment you want to describe\. +.fi +.SH "EXAMPLES" +.nf +The following command prints metadata for the dev +environment: + + $ uffizzi dev describe +.fi + diff --git a/man/uffizzi-dev-describe.ronn b/man/uffizzi-dev-describe.ronn new file mode 100644 index 00000000..75e4fa88 --- /dev/null +++ b/man/uffizzi-dev-describe.ronn @@ -0,0 +1,26 @@ +$ uffizzi dev describe -h +uffizzi-dev-describe - show metadata for a dev environment +================================================================ + +## SYNOPSIS + uffizzi dev describe [NAME] + +## DESCRIPTION + Shows metadata for a dev environment + + This command can fail for the following reasons: + - The dev environment specified does not exist. + - The dev environment specified belongs to a different project. + + For more information on Uffizzi clusters, see: + https://docs.uffizzi.com/references/cli/ + +## POSITIONAL ARGUMENTS + [NAME] + NAME for the dev environment you want to describe. + +## EXAMPLES + The following command prints metadata for the dev + environment: + + $ uffizzi dev describe diff --git a/man/uffizzi.ronn b/man/uffizzi.ronn index 305f5d9b..15f19305 100644 --- a/man/uffizzi.ronn +++ b/man/uffizzi.ronn @@ -29,6 +29,9 @@ uffizzi - manage Uffizzi resources Manage Uffizzi project resources including compose files for specifying compose environment (preview) configurations and secrets + dev + Creates a Uffizzi cluster preconfigured for development workflows + ## COMMAND COMMAND is one of the following: diff --git a/test/support/mocks/mock_prompt.rb b/test/support/mocks/mock_prompt.rb index 3435cc55..1051317d 100644 --- a/test/support/mocks/mock_prompt.rb +++ b/test/support/mocks/mock_prompt.rb @@ -37,7 +37,16 @@ def promise_question_answer(question, answer) private def get_answer(question) - answer_index = @question_answers.index { |question_answer| question_answer[:question] == question } + answer_index = @question_answers.index do |question_answer| + question_answer[:question] == question + case question_answer[:question] + when Regexp + question_answer[:question].match?(question) + else + question_answer[:question] == question + end + end + answer = @question_answers[answer_index].fetch(:answer) @question_answers.delete_at(answer_index) diff --git a/test/uffizzi/cli/dev_test.rb b/test/uffizzi/cli/dev_test.rb index 38775a73..8fd86f5c 100644 --- a/test/uffizzi/cli/dev_test.rb +++ b/test/uffizzi/cli/dev_test.rb @@ -143,4 +143,80 @@ def test_start_dev_with_kubeconfig_and_default_repo_flags assert_requested(stubbed_uffizzi_cluster_get_request) assert_requested(stubbed_uffizzi_cluster_delete_request) end + + def test_describe_dev_by_name + cluster_get_body = json_fixture('files/uffizzi/uffizzi_cluster_deployed.json') + stubbed_uffizzi_cluster_get_request = stub_get_cluster_request(cluster_get_body, @project_slug) + + config_path1 = '/skaffold.yaml' + config_path2 = '/skaffold_2.yaml' + cluster_name1 = cluster_get_body.dig(:cluster, :name) + cluster_name2 = 'cluster-2' + dev_environments = [ + { name: cluster_name1, config_path: config_path1 }, + { name: cluster_name2, config_path: config_path2 }, + ] + + Uffizzi::ConfigFile.write_option(:dev_environments, dev_environments) + + @dev.describe(cluster_name1) + + assert_match("- CONFIG_PATH: #{config_path1}", Uffizzi.ui.last_message) + assert_match("- NAME: #{cluster_name1}", Uffizzi.ui.last_message) + assert_requested(stubbed_uffizzi_cluster_get_request) + end + + def test_describe_single_dev + cluster_get_body = json_fixture('files/uffizzi/uffizzi_cluster_deployed.json') + stubbed_uffizzi_cluster_get_request = stub_get_cluster_request(cluster_get_body, @project_slug) + + config_path = '/skaffold.yaml' + cluster_name = cluster_get_body.dig(:cluster, :name) + dev_environments = [{ name: cluster_name, config_path: config_path }] + Uffizzi::ConfigFile.write_option(:dev_environments, dev_environments) + + @dev.describe + + assert_match("- CONFIG_PATH: #{config_path}", Uffizzi.ui.last_message) + assert_match("- NAME: #{cluster_name}", Uffizzi.ui.last_message) + assert_requested(stubbed_uffizzi_cluster_get_request) + end + + def test_describe_multiple_dev + cluster_get_body = json_fixture('files/uffizzi/uffizzi_cluster_deployed.json') + stubbed_uffizzi_cluster_get_request = stub_get_cluster_request(cluster_get_body, @project_slug) + + config_path1 = '/skaffold.yaml' + config_path2 = '/skaffold_2.yaml' + cluster_name1 = cluster_get_body.dig(:cluster, :name) + cluster_name2 = 'cluster-2' + dev_environments = [ + { name: cluster_name1, config_path: config_path1 }, + { name: cluster_name2, config_path: config_path2 }, + ] + + @mock_prompt.promise_question_answer(/You have several dev environments/, :first) + + Uffizzi::ConfigFile.write_option(:dev_environments, dev_environments) + + @dev.describe + + assert_match("- CONFIG_PATH: #{config_path1}", Uffizzi.ui.last_message) + assert_match("- NAME: #{cluster_name1}", Uffizzi.ui.last_message) + assert_requested(stubbed_uffizzi_cluster_get_request) + end + + def test_describe_zero_dev + @dev.describe + + assert_match('No running dev environments', Uffizzi.ui.last_message) + end + + def test_describe_dev_with_wrong_name + name = 'wrong_name' + + @dev.describe(name) + + assert_match("No running dev environment by name: #{name}", Uffizzi.ui.last_message) + end end