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