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