From f3d2384392a25c1f3be5d04e23914df8f08d367c Mon Sep 17 00:00:00 2001 From: A <258483684+la14-1@users.noreply.github.com> Date: Fri, 13 Feb 2026 10:15:47 -0800 Subject: [PATCH] refactor: decompose GCP and Cherry create_server functions (#965) GCP create_server was 64 lines (largest function across all cloud libs). Cherry create_server was 54 lines. Both are now under 30 lines each by extracting focused helpers: GCP (64 -> 25 lines): - _gcp_prepare_instance_files: startup script + SSH key temp files - _gcp_run_create: gcloud command execution with error diagnostics - _gcp_get_instance_ip: IP extraction from instance describe Cherry (54 -> 27 lines): - _cherry_build_server_body: JSON payload construction - _cherry_submit_create: API call with error handling Agent: complexity-hunter Co-authored-by: A <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Haiku 4.5 --- cherry/lib/common.sh | 75 +++++++++++++++++------------- gcp/lib/common.sh | 106 +++++++++++++++++++++++++------------------ 2 files changed, 106 insertions(+), 75 deletions(-) diff --git a/cherry/lib/common.sh b/cherry/lib/common.sh index a1a13409..04614a65 100755 --- a/cherry/lib/common.sh +++ b/cherry/lib/common.sh @@ -152,6 +152,46 @@ _cherry_wait_for_ip() { CHERRY_SERVER_IP "Server" 60 } +# Build JSON request body for Cherry Servers server creation +# Usage: _cherry_build_server_body PLAN REGION IMAGE HOSTNAME SSH_KEY_ID +_cherry_build_server_body() { + python3 -c " +import json, sys +data = { + 'plan': sys.argv[1], + 'region': sys.argv[2], + 'image': sys.argv[3], + 'hostname': sys.argv[4], + 'ssh_keys': [int(sys.argv[5])] +} +print(json.dumps(data)) +" "$1" "$2" "$3" "$4" "$5" +} + +# Submit server creation request and extract server ID +# Prints server ID on success, returns 1 on failure with diagnostics +_cherry_submit_create() { + local project_id="$1" payload="$2" + + local response + response=$(cherry_api POST "/projects/${project_id}/servers" "$payload") + + local server_id + server_id=$(_extract_json_field "$response" "d.get('id','')") + + if [[ -z "$server_id" ]]; then + log_error "Failed to create Cherry Servers server" + log_error "API Error: $(extract_api_error_message "$response" "$response")" + log_warn "Common issues:" + log_warn " - Insufficient account balance" + log_warn " - Plan unavailable in region (try different CHERRY_DEFAULT_PLAN or CHERRY_DEFAULT_REGION)" + log_warn " - Server limit reached for your account" + return 1 + fi + + printf '%s' "$server_id" +} + create_server() { local hostname="$1" local plan="${CHERRY_DEFAULT_PLAN}" @@ -172,40 +212,13 @@ create_server() { log_info "Plan: $plan, Region: $region, Image: $image" local payload - payload=$(python3 -c " -import json, sys -data = { - 'plan': sys.argv[1], - 'region': sys.argv[2], - 'image': sys.argv[3], - 'hostname': sys.argv[4], - 'ssh_keys': [int(sys.argv[5])] -} -print(json.dumps(data)) -" "$plan" "$region" "$image" "$hostname" "${CHERRY_SSH_KEY_ID}") + payload=$(_cherry_build_server_body "$plan" "$region" "$image" "$hostname" "${CHERRY_SSH_KEY_ID}") - local response - response=$(cherry_api POST "/projects/${project_id}/servers" "$payload") - - local server_id - server_id=$(_extract_json_field "$response" "d.get('id','')") - - if [[ -z "$server_id" ]]; then - log_error "Failed to create Cherry Servers server" - log_error "API Error: $(extract_api_error_message "$response" "$response")" - log_warn "Common issues:" - log_warn " - Insufficient account balance" - log_warn " - Plan unavailable in region (try different CHERRY_DEFAULT_PLAN or CHERRY_DEFAULT_REGION)" - log_warn " - Server limit reached for your account" - return 1 - fi - - log_info "Server created with ID: $server_id" - CHERRY_SERVER_ID="$server_id" + CHERRY_SERVER_ID=$(_cherry_submit_create "$project_id" "$payload") || return 1 export CHERRY_SERVER_ID + log_info "Server created with ID: $CHERRY_SERVER_ID" - # Wait for IP assignment - _cherry_wait_for_ip "$server_id" + _cherry_wait_for_ip "$CHERRY_SERVER_ID" } # ============================================================ diff --git a/gcp/lib/common.sh b/gcp/lib/common.sh index 37f40ed8..0fc61633 100644 --- a/gcp/lib/common.sh +++ b/gcp/lib/common.sh @@ -95,6 +95,63 @@ touch /tmp/.cloud-init-complete CLOUD_INIT_EOF } +# Prepare startup script and SSH metadata temp files for gcloud instance creation +# Sets startup_script_file and pub_key variables in caller's scope +_gcp_prepare_instance_files() { + startup_script_file=$(mktemp) + track_temp_file "${startup_script_file}" + get_cloud_init_userdata > "${startup_script_file}" + + pub_key=$(cat "${HOME}/.ssh/id_ed25519.pub") +} + +# Run gcloud compute instances create and handle errors +# Returns 0 on success, 1 on failure with diagnostic output +_gcp_run_create() { + local name="${1}" zone="${2}" machine_type="${3}" + local image_family="${4}" image_project="${5}" startup_script_file="${6}" pub_key="${7}" + + local gcloud_err + gcloud_err=$(mktemp) + track_temp_file "${gcloud_err}" + + if gcloud compute instances create "${name}" \ + --zone="${zone}" \ + --machine-type="${machine_type}" \ + --image-family="${image_family}" \ + --image-project="${image_project}" \ + --metadata-from-file="startup-script=${startup_script_file}" \ + --metadata="ssh-keys=${GCP_USERNAME}:${pub_key}" \ + --project="${GCP_PROJECT}" \ + --quiet \ + >/dev/null 2>"${gcloud_err}"; then + return 0 + fi + + log_error "Failed to create GCP instance" + local err_output + err_output=$(cat "${gcloud_err}" 2>/dev/null) + if [[ -n "${err_output}" ]]; then + log_error "gcloud error: ${err_output}" + fi + log_warn "Common issues:" + log_warn " - Billing not enabled for the project (enable at https://console.cloud.google.com/billing)" + log_warn " - Compute Engine API not enabled (enable at https://console.cloud.google.com/apis)" + log_warn " - Instance quota exceeded in zone (try different GCP_ZONE)" + log_warn " - Machine type unavailable in zone (try different GCP_MACHINE_TYPE or GCP_ZONE)" + return 1 +} + +# Get the external IP of a GCP instance +# Usage: _gcp_get_instance_ip NAME ZONE +_gcp_get_instance_ip() { + local name="${1}" zone="${2}" + gcloud compute instances describe "${name}" \ + --zone="${zone}" \ + --project="${GCP_PROJECT}" \ + --format='get(networkInterfaces[0].accessConfigs[0].natIP)' 2>/dev/null +} + create_server() { local name="${1}" local machine_type="${GCP_MACHINE_TYPE:-e2-medium}" @@ -108,55 +165,16 @@ create_server() { log_step "Creating GCP instance '${name}' (type: ${machine_type}, zone: ${zone})..." - local pub_key - pub_key=$(cat "${HOME}/.ssh/id_ed25519.pub") + local startup_script_file pub_key + _gcp_prepare_instance_files - # Write startup script to a temp file to avoid --metadata comma delimiter issues - local startup_script_file - startup_script_file=$(mktemp) - track_temp_file "${startup_script_file}" - get_cloud_init_userdata > "${startup_script_file}" + _gcp_run_create "${name}" "${zone}" "${machine_type}" \ + "${image_family}" "${image_project}" "${startup_script_file}" "${pub_key}" || return 1 - local gcloud_err - gcloud_err=$(mktemp) - track_temp_file "${gcloud_err}" - - if ! gcloud compute instances create "${name}" \ - --zone="${zone}" \ - --machine-type="${machine_type}" \ - --image-family="${image_family}" \ - --image-project="${image_project}" \ - --metadata-from-file="startup-script=${startup_script_file}" \ - --metadata="ssh-keys=${GCP_USERNAME}:${pub_key}" \ - --project="${GCP_PROJECT}" \ - --quiet \ - >/dev/null 2>"${gcloud_err}"; then - log_error "Failed to create GCP instance" - local err_output - err_output=$(cat "${gcloud_err}" 2>/dev/null) - if [[ -n "${err_output}" ]]; then - log_error "gcloud error: ${err_output}" - fi - log_warn "Common issues:" - log_warn " - Billing not enabled for the project (enable at https://console.cloud.google.com/billing)" - log_warn " - Compute Engine API not enabled (enable at https://console.cloud.google.com/apis)" - log_warn " - Instance quota exceeded in zone (try different GCP_ZONE)" - log_warn " - Machine type unavailable in zone (try different GCP_MACHINE_TYPE or GCP_ZONE)" - return 1 - fi - - # Export instance metadata for use by calling script # shellcheck disable=SC2034 # Variables exported for use by sourcing scripts export GCP_INSTANCE_NAME_ACTUAL="${name}" export GCP_ZONE="${zone}" - - # Get external IP - local server_ip - server_ip=$(gcloud compute instances describe "${name}" \ - --zone="${zone}" \ - --project="${GCP_PROJECT}" \ - --format='get(networkInterfaces[0].accessConfigs[0].natIP)' 2>/dev/null) - export GCP_SERVER_IP="${server_ip}" + export GCP_SERVER_IP="$(_gcp_get_instance_ip "${name}" "${zone}")" log_info "Instance created: IP=${GCP_SERVER_IP}" }