lib/command/base.rb in cpl-1.4.0 vs lib/command/base.rb in cpl-2.2.0

- old
+ new

@@ -6,10 +6,14 @@ class Base # rubocop:disable Metrics/ClassLength attr_reader :config include Helpers + VALIDATIONS_WITHOUT_ADDITIONAL_OPTIONS = %w[config].freeze + VALIDATIONS_WITH_ADDITIONAL_OPTIONS = %w[templates].freeze + ALL_VALIDATIONS = VALIDATIONS_WITHOUT_ADDITIONAL_OPTIONS + VALIDATIONS_WITH_ADDITIONAL_OPTIONS + # Used to call the command (`cpl NAME`) # NAME = "" # Displayed when running `cpl help` or `cpl help NAME` (defaults to `NAME`) USAGE = "" # Throws error if `true` and no arguments are passed to the command @@ -30,13 +34,13 @@ EXAMPLES = "" # If `true`, hides the command from `cpl help` HIDE = false # Whether or not to show key information like ORG and APP name in commands WITH_INFO_HEADER = true + # Which validations to run before the command + VALIDATIONS = %w[config].freeze - NO_IMAGE_AVAILABLE = "NO_IMAGE_AVAILABLE" - def initialize(config) @config = config end def self.all_commands @@ -49,10 +53,11 @@ def self.common_options [org_option, verbose_option, trace_option] end + # rubocop:disable Metrics/MethodLength def self.org_option(required: false) { name: :org, params: { aliases: ["-o"], @@ -88,10 +93,23 @@ required: required } } end + def self.replica_option(required: false) + { + name: :replica, + params: { + aliases: ["-r"], + banner: "REPLICA_NAME", + desc: "Replica name", + type: :string, + required: required + } + } + end + def self.image_option(required: false) { name: :image, params: { aliases: ["-i"], @@ -101,10 +119,24 @@ required: required } } end + def self.log_method_option(required: false) + { + name: :log_method, + params: { + type: :numeric, + banner: "LOG_METHOD", + desc: "Log method", + required: required, + valid_values: [1, 2, 3], + default: 3 + } + } + end + def self.commit_option(required: false) { name: :commit, params: { aliases: ["-c"], @@ -196,11 +228,12 @@ name: :terminal_size, params: { banner: "ROWS,COLS", desc: "Override remote terminal size (e.g. `--terminal-size 10,20`)", type: :string, - required: required + required: required, + valid_regex: /^\d+,\d+$/ } } end def self.wait_option(title = "", required: false) @@ -235,27 +268,27 @@ required: required } } end - def self.clean_on_failure_option(required: false) + def self.skip_secret_access_binding_option(required: false) { - name: :clean_on_failure, + name: :skip_secret_access_binding, + new_name: :skip_secrets_setup, params: { - desc: "Deletes workload when finished with failure (success always deletes)", + desc: "Skips secret access binding", type: :boolean, - required: required, - default: true + required: required } } end - def self.skip_secret_access_binding_option(required: false) + def self.skip_secrets_setup_option(required: false) { - name: :skip_secret_access_binding, + name: :skip_secrets_setup, params: { - desc: "Skips secret access binding", + desc: "Skips secrets setup", type: :boolean, required: required } } end @@ -269,78 +302,161 @@ required: required } } end - def self.all_options - methods.grep(/_option$/).map { |method| send(method.to_s) } + def self.logs_limit_option(required: false) + { + name: :limit, + params: { + banner: "NUMBER", + desc: "Limit on number of log entries to show", + type: :numeric, + required: required, + default: 200 + } + } end - def self.all_options_by_key_name - all_options.each_with_object({}) do |option, result| - option[:params][:aliases]&.each { |current_alias| result[current_alias.to_s] = option } - result["--#{option[:name]}"] = option - end + def self.logs_since_option(required: false) + { + name: :since, + params: { + banner: "DURATION", + desc: "Loopback window for showing logs " \ + "(see https://www.npmjs.com/package/parse-duration for the accepted formats, e.g., '1h')", + type: :string, + required: required, + default: "1h" + } + } end - def wait_for_workload(workload) - step("Waiting for workload", retry_on_failure: true) do - cp.fetch_workload(workload) - end + def self.interactive_option(required: false) + { + name: :interactive, + params: { + desc: "Runs interactive command", + type: :boolean, + required: required + } + } end - def wait_for_replica(workload, location) - step("Waiting for replica", retry_on_failure: true) do - cp.workload_get_replicas_safely(workload, location: location)&.dig("items", 0) - end + def self.detached_option(required: false) + { + name: :detached, + params: { + desc: "Runs non-interactive command, detaches, and prints commands to log and stop the job", + type: :boolean, + required: required + } + } end - def ensure_workload_deleted(workload) - step("Deleting workload") do - cp.delete_workload(workload) - end + def self.cpu_option(required: false) + { + name: :cpu, + params: { + banner: "CPU", + desc: "Overrides CPU millicores " \ + "(e.g., '100m' for 100 millicores, '1' for 1 core)", + type: :string, + required: required, + valid_regex: /^\d+m?$/ + } + } end - def latest_image_from(items, app_name: config.app, name_only: true) - matching_items = items.select { |item| item["name"].start_with?("#{app_name}:") } + def self.memory_option(required: false) + { + name: :memory, + params: { + banner: "MEMORY", + desc: "Overrides memory size " \ + "(e.g., '100Mi' for 100 mebibytes, '1Gi' for 1 gibibyte)", + type: :string, + required: required, + valid_regex: /^\d+[MG]i$/ + } + } + end - # Or special string to indicate no image available - if matching_items.empty? - name_only ? "#{app_name}:#{NO_IMAGE_AVAILABLE}" : nil - else - latest_item = matching_items.max_by { |item| extract_image_number(item["name"]) } - name_only ? latest_item["name"] : latest_item - end + def self.entrypoint_option(required: false) + { + name: :entrypoint, + params: { + banner: "ENTRYPOINT", + desc: "Overrides entrypoint " \ + "(must be a single command or a script path that exists in the container)", + type: :string, + required: required, + valid_regex: /^\S+$/ + } + } end - def latest_image(app = config.app, org = config.org) - @latest_image ||= {} - @latest_image[app] ||= - begin - items = cp.query_images(app, org)["items"] - latest_image_from(items, app_name: app) - end + def self.validations_option(required: false) + { + name: :validations, + params: { + banner: "VALIDATION_1,VALIDATION_2,...", + desc: "Which validations to run " \ + "(must be separated by a comma)", + type: :string, + required: required, + default: VALIDATIONS_WITHOUT_ADDITIONAL_OPTIONS.join(","), + valid_regex: /^(#{ALL_VALIDATIONS.join("|")})(,(#{ALL_VALIDATIONS.join("|")}))*$/ + } + } end - def latest_image_next(app = config.app, org = config.org, commit: nil) - # debugger - commit ||= config.options[:commit] + def self.skip_post_creation_hook_option(required: false) + { + name: :skip_post_creation_hook, + params: { + desc: "Skips post-creation hook", + type: :boolean, + required: required + } + } + end - @latest_image_next ||= {} - @latest_image_next[app] ||= begin - latest_image_name = latest_image(app, org) - image = latest_image_name.split(":").first - image += ":#{extract_image_number(latest_image_name) + 1}" - image += "_#{commit}" if commit - image - end + def self.skip_pre_deletion_hook_option(required: false) + { + name: :skip_pre_deletion_hook, + params: { + desc: "Skips pre-deletion hook", + type: :boolean, + required: required + } + } end - def extract_image_commit(image_name) - image_name.match(/_(\h+)$/)&.captures&.first + def self.add_app_identity_option(required: false) + { + name: :add_app_identity, + params: { + desc: "Adds app identity template if it does not exist", + type: :boolean, + required: required + } + } end + # rubocop:enable Metrics/MethodLength + def self.all_options + methods.grep(/_option$/).map { |method| send(method.to_s) } + end + + def self.all_options_by_key_name + all_options.each_with_object({}) do |option, result| + option[:params][:aliases]&.each { |current_alias| result[current_alias.to_s] = option } + result["--#{option[:name]}"] = option + end + end + # NOTE: use simplified variant atm, as shelljoin do different escaping # TODO: most probably need better logic for escaping various quotes def args_join(args) args.join(" ") end @@ -375,11 +491,11 @@ begin if retry_on_failure until (success = yield) progress.print(".") - sleep 1 + Kernel.sleep(1) end else success = yield end rescue RuntimeError => e @@ -392,43 +508,33 @@ def cp @cp ||= Controlplane.new(config) end - def perform!(cmd) - system(cmd) || exit(1) - end + def ensure_docker_running! + result = Shell.cmd("docker", "version", capture_stderr: true) + return if result[:success] - def app_location_link - "/org/#{config.org}/location/#{config.location}" + raise "Can't run Docker. Please make sure that it's installed and started, then try again." end - def app_image_link - "/org/#{config.org}/image/#{latest_image}" - end + def run_command_in_latest_image(command, title:) + # Need to prefix the command with '.controlplane/' + # if it's a file in the '.controlplane' directory, + # for backwards compatibility + path = Pathname.new("#{config.app_cpln_dir}/#{command}").expand_path + command = ".controlplane/#{command}" if File.exist?(path) - def app_identity - "#{config.app}-identity" - end + progress.puts("Running #{title}...\n\n") - def app_identity_link - "/org/#{config.org}/gvc/#{config.app}/identity/#{app_identity}" - end + begin + Cpl::Cli.start(["run", "-a", config.app, "--image", "latest", "--", command]) + rescue SystemExit => e + progress.puts - def app_secrets - "#{config.app_prefix}-secrets" - end + raise "Failed to run #{title}." if e.status.nonzero? - def app_secrets_policy - "#{app_secrets}-policy" - end - - private - - # returns 0 if no prior image - def extract_image_number(image_name) - return 0 if image_name.end_with?(NO_IMAGE_AVAILABLE) - - image_name.match(/:(\d+)/)&.captures&.first.to_i + progress.puts("Finished running #{title}.\n\n") + end end end end