lib/core/controlplane.rb in cpl-1.4.0 vs lib/core/controlplane.rb in cpl-2.2.0
- old
+ new
@@ -1,15 +1,19 @@
# frozen_string_literal: true
class Controlplane # rubocop:disable Metrics/ClassLength
attr_reader :config, :api, :gvc, :org
+ NO_IMAGE_AVAILABLE = "NO_IMAGE_AVAILABLE"
+
def initialize(config)
@config = config
@api = ControlplaneApi.new
@gvc = config.app
@org = config.org
+
+ ensure_org_exists! if org
end
# profile
def profile_switch(profile)
@@ -17,28 +21,71 @@
ControlplaneApiDirect.reset_api_token
end
def profile_exists?(profile)
cmd = "cpln profile get #{profile} -o yaml"
- perform_yaml(cmd).length.positive?
+ perform_yaml!(cmd).length.positive?
end
def profile_create(profile, token)
sensitive_data_pattern = /(?<=--token )(\S+)/
cmd = "cpln profile create #{profile} --token #{token}"
- cmd += " > /dev/null" if Shell.should_hide_output?
perform!(cmd, sensitive_data_pattern: sensitive_data_pattern)
end
def profile_delete(profile)
cmd = "cpln profile delete #{profile}"
- cmd += " > /dev/null" if Shell.should_hide_output?
perform!(cmd)
end
# image
+ def latest_image(a_gvc = gvc, a_org = org, refresh: false)
+ @latest_image ||= {}
+ @latest_image[a_gvc] = nil if refresh
+ @latest_image[a_gvc] ||=
+ begin
+ items = query_images(a_gvc, a_org)["items"]
+ latest_image_from(items, app_name: a_gvc)
+ end
+ end
+
+ def latest_image_next(a_gvc = gvc, a_org = org, commit: nil)
+ commit ||= config.options[:commit]
+
+ @latest_image_next ||= {}
+ @latest_image_next[a_gvc] ||= begin
+ latest_image_name = latest_image(a_gvc, a_org)
+ image = latest_image_name.split(":").first
+ image += ":#{extract_image_number(latest_image_name) + 1}"
+ image += "_#{commit}" if commit
+ image
+ end
+ end
+
+ def latest_image_from(items, app_name: gvc, name_only: true)
+ matching_items = items.select { |item| item["name"].start_with?("#{app_name}:") }
+
+ # 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
+ end
+
+ def extract_image_number(image_name)
+ return 0 if image_name.end_with?(NO_IMAGE_AVAILABLE)
+
+ image_name.match(/:(\d+)/)&.captures&.first.to_i
+ end
+
+ def extract_image_commit(image_name)
+ image_name.match(/_(\h+)$/)&.captures&.first
+ end
+
def query_images(a_gvc = gvc, a_org = org, partial_gvc_match: nil)
partial_gvc_match = config.should_app_start_with?(a_gvc) if partial_gvc_match.nil?
gvc_op = partial_gvc_match ? "~" : "="
api.query_images(org: a_org, gvc: a_gvc, gvc_op_type: gvc_op)
@@ -56,35 +103,35 @@
perform!(cmd)
image_push(image) if push
end
+ def fetch_image_details(image)
+ api.fetch_image_details(org: org, image: image)
+ end
+
def image_delete(image)
api.image_delete(org: org, image: image)
end
def image_login(org_name = config.org)
cmd = "cpln image docker-login --org #{org_name}"
- cmd += " > /dev/null 2>&1" if Shell.should_hide_output?
- perform!(cmd)
+ perform!(cmd, output_mode: :none)
end
def image_pull(image)
cmd = "docker pull #{image}"
- cmd += " > /dev/null" if Shell.should_hide_output?
- perform!(cmd)
+ perform!(cmd, output_mode: :none)
end
def image_tag(old_tag, new_tag)
cmd = "docker tag #{old_tag} #{new_tag}"
- cmd += " > /dev/null" if Shell.should_hide_output?
perform!(cmd)
end
def image_push(image)
cmd = "docker push #{image}"
- cmd += " > /dev/null" if Shell.should_hide_output?
perform!(cmd)
end
# gvc
@@ -96,11 +143,11 @@
# When `match_if_app_name_starts_with` is `true`, we query for any gvc containing the name,
# otherwise we query for a gvc with the exact name.
op = config.should_app_start_with?(app_name) ? "~" : "="
cmd = "cpln gvc query --org #{org} -o yaml --prop name#{op}#{app_name}"
- perform_yaml(cmd)
+ perform_yaml!(cmd)
end
def fetch_gvc(a_gvc = gvc, a_org = org)
api.gvc_get(gvc: a_gvc, org: a_org)
end
@@ -143,53 +190,49 @@
workload_op = partial_workload_match ? "~" : "="
api.query_workloads(org: a_org, gvc: a_gvc, workload: workload, gvc_op_type: gvc_op, workload_op_type: workload_op)
end
- def workload_get_replicas(workload, location:)
- cmd = "cpln workload get-replicas #{workload} #{gvc_org} --location #{location} -o yaml"
+ def fetch_workload_replicas(workload, location:)
+ cmd = "cpln workload replica get #{workload} #{gvc_org} --location #{location} -o yaml"
perform_yaml(cmd)
end
- def workload_get_replicas_safely(workload, location:)
- 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
+ def stop_workload_replica(workload, replica, location:)
+ cmd = "cpln workload replica stop #{workload} #{gvc_org} --replica-name #{replica} --location #{location}"
+ perform(cmd, output_mode: :none)
end
def fetch_workload_deployments(workload)
api.workload_deployments(workload: workload, gvc: gvc, org: org)
end
- def workload_deployment_version_ready?(version, next_version, expected_status:)
+ def workload_deployment_version_ready?(version, next_version)
return false unless version["workload"] == next_version
version["containers"]&.all? do |_, container|
- ready = container.dig("resources", "replicas") == container.dig("resources", "replicasReady")
- expected_status == true ? ready : !ready
+ container.dig("resources", "replicas") == container.dig("resources", "replicasReady")
end
end
- def workload_deployments_ready?(workload, expected_status:)
+ def workload_deployments_ready?(workload, location:, expected_status:)
+ deployed_replicas = fetch_workload_replicas(workload, location: location)["items"].length
+ return deployed_replicas.zero? if expected_status == false
+
deployments = fetch_workload_deployments(workload)["items"]
deployments.all? do |deployment|
next_version = deployment.dig("status", "expectedDeploymentVersion")
deployment.dig("status", "versions")&.all? do |version|
- workload_deployment_version_ready?(version, next_version, expected_status: expected_status)
+ workload_deployment_version_ready?(version, next_version)
end
end
end
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.should_hide_output?
perform!(cmd)
end
def set_workload_env_var(workload, container:, name:, value:)
data = fetch_workload!(workload)
@@ -211,13 +254,17 @@
data["spec"]["defaultOptions"]["suspend"] = value
api.update_workload(org: org, gvc: gvc, workload: workload, data: data)
end
+ def workload_suspended?(workload)
+ details = fetch_workload!(workload)
+ details["spec"]["defaultOptions"]["suspend"]
+ end
+
def workload_force_redeployment(workload)
cmd = "cpln workload force-redeployment #{workload} #{gvc_org}"
- cmd += " > /dev/null" if Shell.should_hide_output?
perform!(cmd)
end
def delete_workload(workload, a_gvc = gvc)
api.delete_workload(org: org, gvc: a_gvc, workload: workload)
@@ -225,20 +272,45 @@
def workload_connect(workload, location:, container: nil, shell: nil)
cmd = "cpln workload connect #{workload} #{gvc_org} --location #{location}"
cmd += " --container #{container}" if container
cmd += " --shell #{shell}" if shell
- perform!(cmd)
+ perform!(cmd, output_mode: :all)
end
- def workload_exec(workload, location:, container: nil, command: nil)
- cmd = "cpln workload exec #{workload} #{gvc_org} --location #{location}"
+ def workload_exec(workload, replica, location:, container: nil, command: nil)
+ cmd = "cpln workload exec #{workload} #{gvc_org} --replica #{replica} --location #{location}"
cmd += " --container #{container}" if container
cmd += " -- #{command}"
- perform!(cmd)
+ perform!(cmd, output_mode: :all)
end
+ def start_cron_workload(workload, job_start_yaml, location:)
+ Tempfile.create do |f|
+ f.write(job_start_yaml)
+ f.rewind
+
+ cmd = "cpln workload cron start #{workload} #{gvc_org} --file #{f.path} --location #{location} -o yaml"
+ perform_yaml(cmd)
+ end
+ end
+
+ def fetch_cron_workload(workload, location:)
+ cmd = "cpln workload cron get #{workload} #{gvc_org} --location #{location} -o yaml"
+ perform_yaml(cmd)
+ end
+
+ def cron_workload_deployed_version(workload)
+ current_deployment = fetch_workload_deployments(workload)&.dig("items")&.first
+ return nil unless current_deployment
+
+ ready = current_deployment.dig("status", "ready")
+ last_processed_version = current_deployment.dig("status", "lastProcessedVersion")
+
+ ready ? last_processed_version : nil
+ end
+
# volumeset
def fetch_volumesets(a_gvc = gvc)
api.list_volumesets(org: org, gvc: a_gvc)
end
@@ -289,19 +361,29 @@
api.update_domain(org: org, domain: data["name"], data: data)
end
# logs
- def logs(workload:)
- cmd = "cpln logs '{workload=\"#{workload}\"}' --org #{org} -t -o raw --limit 200"
- perform!(cmd)
+ def logs(workload:, limit:, since:, replica: nil)
+ query_parts = ["gvc=\"#{gvc}\"", "workload=\"#{workload}\""]
+ query_parts.push("replica=\"#{replica}\"") if replica
+ query = "{#{query_parts.join(',')}}"
+
+ cmd = "cpln logs '#{query}' --org #{org} -t -o raw --limit #{limit} --since #{since}"
+ perform!(cmd, output_mode: :all)
end
- def log_get(workload:, from:, to:)
- api.log_get(org: org, gvc: gvc, workload: workload, from: from, to: to)
+ def log_get(workload:, from:, to:, replica: nil)
+ api.log_get(org: org, gvc: gvc, workload: workload, replica: replica, from: from, to: to)
end
+ # secrets
+
+ def fetch_secret(secret)
+ api.fetch_secret(org: org, secret: secret)
+ end
+
# identities
def fetch_identity(identity, a_gvc = gvc)
api.fetch_identity(org: org, gvc: a_gvc, identity: identity)
end
@@ -312,14 +394,18 @@
api.fetch_policy(org: org, policy: policy)
end
def bind_identity_to_policy(identity_link, policy)
cmd = "cpln policy add-binding #{policy} --org #{org} --identity #{identity_link} --permission reveal"
- cmd += " > /dev/null" if Shell.should_hide_output?
perform!(cmd)
end
+ def unbind_identity_from_policy(identity_link, policy)
+ cmd = "cpln policy remove-binding #{policy} --org #{org} --identity #{identity_link} --permission reveal"
+ perform!(cmd)
+ end
+
# apply
def apply_template(data) # rubocop:disable Metrics/MethodLength
Tempfile.create do |f|
f.write(data)
f.rewind
@@ -327,17 +413,21 @@
if Shell.tmp_stderr
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
+ result = Shell.cmd(cmd)
+ parse_apply_result(result[:output]) if result[:success]
else
Shell.debug("CMD", cmd)
- result = `#{cmd}`
- $CHILD_STATUS.success? ? parse_apply_result(result) : exit(1)
+ result = Shell.cmd(cmd)
+ if result[:success]
+ parse_apply_result(result[:output])
+ else
+ Shell.abort("Command exited with non-zero status.")
+ end
end
end
end
def apply_hash(data)
@@ -377,26 +467,84 @@
items
end
private
- def perform(cmd)
- Shell.debug("CMD", cmd)
+ def org_exists?
+ items = api.list_orgs["items"]
+ items.any? { |item| item["name"] == org }
+ end
- system(cmd)
+ def ensure_org_exists!
+ return if org_exists?
+
+ raise "Can't find org '#{org}', please create it in the Control Plane dashboard " \
+ "or ensure that the name is correct."
end
- def perform!(cmd, sensitive_data_pattern: nil)
+ # `output_mode` can be :all, :errors_only or :none.
+ # If not provided, it will be determined based on the `HIDE_COMMAND_OUTPUT` env var
+ # or the return value of `Shell.should_hide_output?`.
+ def build_command(cmd, output_mode: nil) # rubocop:disable Metrics/MethodLength
+ output_mode ||= determine_command_output_mode
+
+ case output_mode
+ when :all
+ cmd
+ when :errors_only
+ "#{cmd} > /dev/null"
+ when :none
+ "#{cmd} > /dev/null 2>&1"
+ else
+ raise "Invalid command output mode '#{output_mode}'."
+ end
+ end
+
+ def determine_command_output_mode
+ if ENV.fetch("HIDE_COMMAND_OUTPUT", nil) == "true"
+ :none
+ elsif Shell.should_hide_output?
+ :errors_only
+ else
+ :all
+ end
+ end
+
+ def perform(cmd, output_mode: nil, sensitive_data_pattern: nil)
+ cmd = build_command(cmd, output_mode: output_mode)
+
Shell.debug("CMD", cmd, sensitive_data_pattern: sensitive_data_pattern)
- system(cmd) || exit(1)
+ kernel_system_with_pid_handling(cmd)
end
+ # NOTE: full analogue of Kernel.system which returns pids and saves it to child_pids for proper killing
+ def kernel_system_with_pid_handling(cmd)
+ pid = Process.spawn(cmd)
+ $child_pids << pid # rubocop:disable Style/GlobalVars
+
+ _, status = Process.wait2(pid)
+ $child_pids.delete(pid) # rubocop:disable Style/GlobalVars
+
+ status.exited? ? status.success? : nil
+ rescue SystemCallError
+ nil
+ end
+
+ def perform!(cmd, output_mode: nil, sensitive_data_pattern: nil)
+ success = perform(cmd, output_mode: output_mode, sensitive_data_pattern: sensitive_data_pattern)
+ success || Shell.abort("Command exited with non-zero status.")
+ end
+
def perform_yaml(cmd)
Shell.debug("CMD", cmd)
- result = `#{cmd}`
- $CHILD_STATUS.success? ? YAML.safe_load(result) : exit(1)
+ result = Shell.cmd(cmd)
+ YAML.safe_load(result[:output], permitted_classes: [Time]) if result[:success]
+ end
+
+ def perform_yaml!(cmd)
+ perform_yaml(cmd) || Shell.abort("Command exited with non-zero status.")
end
def gvc_org
"--gvc #{gvc} --org #{org}"
end