From 97be1a1226e587452362fc2393cb66438b090277 Mon Sep 17 00:00:00 2001 From: Rafael Gomes Date: Wed, 25 Oct 2023 05:03:15 -0300 Subject: [PATCH] Small improvements (#83) * feat: add open-console command * feat: show help when calling cpl with no command * feat: allow setting org with CPLN_ORG env var * feat: add --verbose option * docs: update changelog * docs: update readme --- CHANGELOG.md | 10 +++++++ README.md | 4 +-- docs/commands.md | 9 +++++++ lib/command/base.rb | 12 +++++++++ lib/command/config.rb | 5 ++++ lib/command/copy_image_from_upstream.rb | 7 +++-- lib/command/info.rb | 7 +++-- lib/command/no_command.rb | 8 +++--- lib/command/open_console.rb | 26 ++++++++++++++++++ lib/core/config.rb | 23 +++++++++++++--- lib/core/controlplane.rb | 35 ++++++++++++++++++------- lib/core/controlplane_api_direct.rb | 2 ++ lib/core/shell.rb | 14 +++++++++- lib/cpl.rb | 2 +- 14 files changed, 140 insertions(+), 24 deletions(-) create mode 100644 lib/command/open_console.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eaf3056..663b8c65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,16 @@ _Please add entries here for your pull requests that are not yet released._ - Fixed failed build on MacOS by adding platform flag and fixed multiple files in yaml document for template. [PR 81](https://github.com/shakacode/heroku-to-control-plane/pull/81) by [justin808](https://github.com/justin808). +### Added + +- Added `open-console` command to open the app console on Control Plane. [PR 83](https://github.com/shakacode/heroku-to-control-plane/pull/83) by [Rafael Gomes](https://github.com/rafaelgomesxyz). +- Added option to set the org with a `CPLN_ORG` env var. [PR 83](https://github.com/shakacode/heroku-to-control-plane/pull/83) by [Rafael Gomes](https://github.com/rafaelgomesxyz). +- Added `--verbose` option to all commands for more detailed logs. [PR 83](https://github.com/shakacode/heroku-to-control-plane/pull/83) by [Rafael Gomes](https://github.com/rafaelgomesxyz). + +### Changed + +- Calling `cpl` with no command now shows the help menu. [PR 83](https://github.com/shakacode/heroku-to-control-plane/pull/83) by [Rafael Gomes](https://github.com/rafaelgomesxyz). + ## [1.1.1] - 2023-09-23 ### Fixed diff --git a/README.md b/README.md index 2566f1fc..b70482c8 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ For the typical Rails app, this means: ## Installation -1. Ensure your [Control Plane](https://controlplane.com) account is set up. Set up an `organization` for testing in that account and modify the value for `aliases.common.cpln_org` in `.controlplane/controlplane.yml`. If you need an organization, please [contact Shakacode](mailto:controlplane@shakacode.com). +1. Ensure your [Control Plane](https://controlplane.com) account is set up. Set up an `organization` for testing in that account and modify the value for `aliases.common.cpln_org` in `.controlplane/controlplane.yml`, or you can also set it with the `CPLN_ORG` environment variable. If you need an organization, please [contact Shakacode](mailto:controlplane@shakacode.com). 2. Install [Node.js](https://nodejs.org/en) (required for Control Plane CLI). @@ -138,7 +138,7 @@ The `cpl` gem is based on several configuration files within a `/.controlplane` ├─ entrypoint.sh ``` -1. `controlplane.yml` describes the overall application. Be sure to have as the value for `aliases.common.cpln_org`. +1. `controlplane.yml` describes the overall application. Be sure to have as the value for `aliases.common.cpln_org`, or set it with the `CPLN_ORG` environment variable. 2. `Dockerfile` builds the production application. `entrypoint.sh` is an _example_ entrypoint script for the production application, referenced in your Dockerfile. 3. `templates` directory contains the templates for the various workloads, such as `rails.yml` and `postgres.yml`. 4. `templates/gvc.yml` defines your project's GVC (like a Heroku app). More importantly, it contains ENV values for the app. diff --git a/docs/commands.md b/docs/commands.md index 2117359a..a1e021f3 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -227,6 +227,15 @@ cpl open -a $APP_NAME cpl open -a $APP_NAME -w $WORKLOAD_NAME ``` +### `open-console` + +- Opens the app console on Control Plane in the default browser +- Can also go directly to a workload page if `--workload` is provided + +```sh +cpl open-console -a $APP_NAME +``` + ### `promote-app-from-upstream` - Copies the latest image from upstream, runs a release script (optional), and deploys the image diff --git a/lib/command/base.rb b/lib/command/base.rb index 038a636d..99c12d10 100644 --- a/lib/command/base.rb +++ b/lib/command/base.rb @@ -176,6 +176,18 @@ def self.wait_option(title = "", required: false) } end + def self.verbose_option(required: false) + { + name: :verbose, + params: { + aliases: ["-d"], + desc: "Shows detailed logs", + type: :boolean, + required: required + } + } + end + def self.all_options methods.grep(/_option$/).map { |method| send(method.to_s) } end diff --git a/lib/command/config.rb b/lib/command/config.rb index fd6465cd..3b5cdd7e 100644 --- a/lib/command/config.rb +++ b/lib/command/config.rb @@ -21,6 +21,11 @@ class Config < Base EX def call # rubocop:disable Metrics/MethodLength + if config.org_comes_from_env + puts Shell.color("Org comes from CPLN_ORG env var", :yellow) + puts + end + if config.app puts "#{Shell.color("Current config (app '#{config.app}')", :blue)}:" puts pretty_print(config.current) diff --git a/lib/command/copy_image_from_upstream.rb b/lib/command/copy_image_from_upstream.rb index 7cafc60e..be052dff 100644 --- a/lib/command/copy_image_from_upstream.rb +++ b/lib/command/copy_image_from_upstream.rb @@ -29,7 +29,7 @@ def call # rubocop:disable Metrics/MethodLength ensure_docker_running! @upstream = config[:upstream] - @upstream_org = config.apps[@upstream.to_sym][:cpln_org] + @upstream_org = config.apps[@upstream.to_sym][:cpln_org] || ENV.fetch("CPLN_ORG_UPSTREAM", nil) ensure_upstream_org! create_upstream_profile @@ -51,7 +51,10 @@ def ensure_docker_running! end def ensure_upstream_org! - raise "Can't find option 'cpln_org' for app '#{@upstream}' in 'controlplane.yml'." unless @upstream_org + return if @upstream_org + + raise "Can't find option 'cpln_org' for app '#{@upstream}' in 'controlplane.yml', " \ + "and CPLN_ORG_UPSTREAM env var is not set." end def create_upstream_profile diff --git a/lib/command/info.rb b/lib/command/info.rb index d855e5ce..9fdd4a4b 100644 --- a/lib/command/info.rb +++ b/lib/command/info.rb @@ -81,17 +81,20 @@ def fetch_app_workloads(org) # rubocop:disable Metrics/MethodLength end end - def orgs # rubocop:disable Metrics/MethodLength + def orgs # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity result = [] if config.options[:org] result.push(config.options[:org]) else + org_from_env = ENV.fetch("CPLN_ORG", nil) + result.push(org_from_env) if org_from_env + config.apps.each do |app_name, app_options| next if config.app && !app_matches?(config.app, app_name, app_options) org = app_options[:cpln_org] - result.push(org) unless result.include?(org) + result.push(org) if org && !result.include?(org) end end diff --git a/lib/command/no_command.rb b/lib/command/no_command.rb index af168c01..69fda9b1 100644 --- a/lib/command/no_command.rb +++ b/lib/command/no_command.rb @@ -11,9 +11,11 @@ class NoCommand < Base HIDE = true def call - return unless config.options[:version] - - Cpl::Cli.start(["version"]) + if config.options[:version] + Cpl::Cli.start(["version"]) + else + Cpl::Cli.start(["help"]) + end end end end diff --git a/lib/command/open_console.rb b/lib/command/open_console.rb new file mode 100644 index 00000000..d9280025 --- /dev/null +++ b/lib/command/open_console.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Command + class OpenConsole < Base + NAME = "open-console" + OPTIONS = [ + app_option(required: true), + workload_option + ].freeze + DESCRIPTION = "Opens the app console on Control Plane in the default browser" + LONG_DESCRIPTION = <<~DESC + - Opens the app console on Control Plane in the default browser + - Can also go directly to a workload page if `--workload` is provided + DESC + + def call + workload = config.options[:workload] + url = "https://console.cpln.io/console/org/#{config.org}/gvc/#{config.app}" + url += "/workload/#{workload}" if workload + url += "/-info" + opener = `which xdg-open open`.split("\n").grep_v("not found").first + + exec %(#{opener} "#{url}") + end + end +end diff --git a/lib/core/config.rb b/lib/core/config.rb index 58a52aab..c40131d1 100644 --- a/lib/core/config.rb +++ b/lib/core/config.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -class Config +class Config # rubocop:disable Metrics/ClassLength attr_reader :config, :current, - :org, :app, :apps, :app_dir, + :org, :org_comes_from_env, :app, :apps, :app_dir, # command line options :args, :options @@ -12,10 +12,13 @@ def initialize(args, options) @args = args @options = options @org = options[:org] + @org_comes_from_env = false @app = options[:app] load_app_config load_apps + + Shell.verbose_mode(options[:verbose]) end def [](key) @@ -48,6 +51,13 @@ def ensure_current_config_app!(app_name) raise "Can't find app '#{app_name}' in 'controlplane.yml'." unless current end + def ensure_current_config_org!(app_name) + return if @org + + raise "Can't find option 'cpln_org' for app '#{app_name}' in 'controlplane.yml', " \ + "and CPLN_ORG env var is not set." + end + def ensure_config! raise "'controlplane.yml' is empty." unless config end @@ -66,8 +76,15 @@ def app_matches_current?(app_name, app_options) def pick_current_config(app_name, app_options) @current = app_options - @org = self[:cpln_org] ensure_current_config_app!(app_name) + + if current.key?(:cpln_org) + @org = current.fetch(:cpln_org) + else + @org = ENV.fetch("CPLN_ORG", nil) + @org_comes_from_env = true + end + ensure_current_config_org!(app_name) end def load_apps # rubocop:disable Metrics/MethodLength diff --git a/lib/core/controlplane.rb b/lib/core/controlplane.rb index f09d6dce..0b5e0227 100644 --- a/lib/core/controlplane.rb +++ b/lib/core/controlplane.rb @@ -24,13 +24,13 @@ def profile_exists?(profile) def profile_create(profile, token) cmd = "cpln profile create #{profile} --token #{token}" - cmd += " > /dev/null" if Shell.tmp_stderr + cmd += " > /dev/null" if Shell.should_hide_output? perform!(cmd) end def profile_delete(profile) cmd = "cpln profile delete #{profile}" - cmd += " > /dev/null" if Shell.tmp_stderr + cmd += " > /dev/null" if Shell.should_hide_output? perform!(cmd) end @@ -61,25 +61,25 @@ def image_delete(image) def image_login(org_name = config.org) cmd = "cpln image docker-login --org #{org_name}" - cmd += " > /dev/null 2>&1" if Shell.tmp_stderr + cmd += " > /dev/null 2>&1" if Shell.should_hide_output? perform!(cmd) end def image_pull(image) cmd = "docker pull #{image}" - cmd += " > /dev/null" if Shell.tmp_stderr + cmd += " > /dev/null" if Shell.should_hide_output? perform!(cmd) end def image_tag(old_tag, new_tag) cmd = "docker tag #{old_tag} #{new_tag}" - cmd += " > /dev/null" if Shell.tmp_stderr + cmd += " > /dev/null" if Shell.should_hide_output? perform!(cmd) end def image_push(image) cmd = "docker push #{image}" - cmd += " > /dev/null" if Shell.tmp_stderr + cmd += " > /dev/null" if Shell.should_hide_output? perform!(cmd) end @@ -148,7 +148,11 @@ def workload_get_replicas(workload, location:) end def workload_get_replicas_safely(workload, location:) - cmd = "cpln workload get-replicas #{workload} #{gvc_org} --location #{location} -o yaml 2> /dev/null" + cmd = "cpln workload get-replicas #{workload} #{gvc_org} --location #{location} -o yaml" + cmd += " 2> /dev/null" if Shell.should_hide_output? + + Shell.debug("CMD", cmd) + result = `#{cmd}` $CHILD_STATUS.success? ? YAML.safe_load(result) : nil end @@ -180,7 +184,7 @@ def workload_deployments_ready?(workload, expected_status:) def workload_set_image_ref(workload, container:, image:) cmd = "cpln workload update #{workload} #{gvc_org}" cmd += " --set spec.containers.#{container}.image=/org/#{config.org}/image/#{image}" - cmd += " > /dev/null" if Shell.tmp_stderr + cmd += " > /dev/null" if Shell.should_hide_output? perform!(cmd) end @@ -208,7 +212,7 @@ def set_workload_suspend(workload, value) def workload_force_redeployment(workload) cmd = "cpln workload force-redeployment #{workload} #{gvc_org}" - cmd += " > /dev/null" if Shell.tmp_stderr + cmd += " > /dev/null" if Shell.should_hide_output? perform!(cmd) end @@ -282,10 +286,15 @@ def apply_template(data) # rubocop:disable Metrics/MethodLength f.rewind cmd = "cpln apply #{gvc_org} --file #{f.path}" if Shell.tmp_stderr - cmd += " 2> #{Shell.tmp_stderr.path}" + cmd += " 2> #{Shell.tmp_stderr.path}" if Shell.should_hide_output? + + Shell.debug("CMD", cmd) + result = `#{cmd}` $CHILD_STATUS.success? ? parse_apply_result(result) : false else + Shell.debug("CMD", cmd) + result = `#{cmd}` $CHILD_STATUS.success? ? parse_apply_result(result) : exit(false) end @@ -332,14 +341,20 @@ def parse_apply_result(result) # rubocop:disable Metrics/CyclomaticComplexity, M private def perform(cmd) + Shell.debug("CMD", cmd) + system(cmd) end def perform!(cmd) + Shell.debug("CMD", cmd) + system(cmd) || exit(false) end def perform_yaml(cmd) + Shell.debug("CMD", cmd) + result = `#{cmd}` $CHILD_STATUS.success? ? YAML.safe_load(result) : exit(false) end diff --git a/lib/core/controlplane_api_direct.rb b/lib/core/controlplane_api_direct.rb index 1bb3db3c..ef280e0d 100644 --- a/lib/core/controlplane_api_direct.rb +++ b/lib/core/controlplane_api_direct.rb @@ -24,6 +24,8 @@ def call(url, method:, host: :api, body: nil) # rubocop:disable Metrics/MethodLe request["Authorization"] = api_token request.body = body.to_json if body + Shell.debug(method.upcase, "#{uri} #{body&.to_json}") + response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(request) } case response diff --git a/lib/core/shell.rb b/lib/core/shell.rb index 12fa7b4d..9595f322 100644 --- a/lib/core/shell.rb +++ b/lib/core/shell.rb @@ -2,7 +2,7 @@ class Shell class << self - attr_reader :tmp_stderr + attr_reader :tmp_stderr, :verbose end def self.shell @@ -50,4 +50,16 @@ def self.warn_deprecated(message) def self.abort(message) Kernel.abort(color("ERROR: #{message}", :red)) end + + def self.verbose_mode(verbose) + @verbose = verbose + end + + def self.debug(prefix, message) + stderr.puts("\n[#{color(prefix, :red)}] #{message}") if verbose + end + + def self.should_hide_output? + tmp_stderr && !verbose + end end diff --git a/lib/cpl.rb b/lib/cpl.rb index 624336c1..652dabf7 100644 --- a/lib/cpl.rb +++ b/lib/cpl.rb @@ -141,7 +141,7 @@ def self.all_base_commands usage = command_class::USAGE.empty? ? name : command_class::USAGE requires_args = command_class::REQUIRES_ARGS default_args = command_class::DEFAULT_ARGS - command_options = command_class::OPTIONS + command_options = command_class::OPTIONS + [::Command::Base.verbose_option] description = command_class::DESCRIPTION long_description = command_class::LONG_DESCRIPTION examples = command_class::EXAMPLES