#!/bin/bash # OpenTofu 密钥上传脚本 # 用于将 terraform.tfvars 中的敏感配置批量上传到 Consul set -euo pipefail # 配置 CONSUL_ADDR="${CONSUL_ADDR:-http://master:8500}" CONSUL_TOKEN="${CONSUL_TOKEN:-}" ENVIRONMENT="${ENVIRONMENT:-dev}" TOFU_DIR="${TOFU_DIR:-tofu/environments/${ENVIRONMENT}}" TFVARS_FILE="${TFVARS_FILE:-${TOFU_DIR}/terraform.tfvars}" # 颜色输出 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # 日志函数 log_info() { echo -e "${BLUE}[INFO]${NC} $1" } log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" } log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1" } log_error() { echo -e "${RED}[ERROR]${NC} $1" } # 检查依赖 check_dependencies() { local deps=("curl" "jq") for dep in "${deps[@]}"; do if ! command -v "$dep" &> /dev/null; then log_error "缺少依赖: $dep" exit 1 fi done } # 检查 Consul 连接 check_consul() { log_info "检查 Consul 连接..." if ! curl -s "${CONSUL_ADDR}/v1/status/leader" > /dev/null; then log_error "无法连接到 Consul: ${CONSUL_ADDR}" exit 1 fi log_success "Consul 连接正常" } # 检查 tfvars 文件 check_tfvars_file() { if [[ ! -f "$TFVARS_FILE" ]]; then log_error "找不到 terraform.tfvars 文件: $TFVARS_FILE" exit 1 fi log_info "找到配置文件: $TFVARS_FILE" } # 解析 HCL 配置并转换为 JSON parse_hcl_to_json() { local tfvars_file="$1" local temp_tf_file="/tmp/temp_config.tf" local temp_json_file="/tmp/temp_config.json" # 创建临时 .tf 文件,将变量赋值转换为输出 log_info "解析 HCL 配置..." # 读取 tfvars 文件并转换为 output 格式 cat > "$temp_tf_file" << 'EOF' # 临时配置文件,用于解析 tfvars EOF # 解析每个配置块 while IFS= read -r line; do # 跳过注释和空行 if [[ "$line" =~ ^[[:space:]]*# ]] || [[ -z "${line// }" ]]; then continue fi # 提取变量名和值 if [[ "$line" =~ ^[[:space:]]*([a-zA-Z_][a-zA-Z0-9_]*)[[:space:]]*=[[:space:]]*(.+)$ ]]; then local var_name="${BASH_REMATCH[1]}" local var_value="${BASH_REMATCH[2]}" echo "output \"$var_name\" {" >> "$temp_tf_file" echo " value = $var_value" >> "$temp_tf_file" echo "}" >> "$temp_tf_file" fi done < "$tfvars_file" # 使用 terraform 解析配置 if command -v terraform &> /dev/null; then cd "$(dirname "$temp_tf_file")" terraform init -backend=false > /dev/null 2>&1 || true terraform output -json > "$temp_json_file" 2>/dev/null || { log_warning "无法使用 terraform 解析,尝试手动解析..." manual_parse_tfvars "$tfvars_file" "$temp_json_file" } else log_warning "未找到 terraform,使用手动解析..." manual_parse_tfvars "$tfvars_file" "$temp_json_file" fi echo "$temp_json_file" } # 手动解析 tfvars 文件 manual_parse_tfvars() { local tfvars_file="$1" local output_file="$2" log_info "手动解析 tfvars 文件..." # 创建基础 JSON 结构 echo "{" > "$output_file" local first_item=true local in_block=false local block_name="" local block_content="" while IFS= read -r line; do # 跳过注释和空行 if [[ "$line" =~ ^[[:space:]]*# ]] || [[ -z "${line// }" ]]; then continue fi # 检测配置块开始 if [[ "$line" =~ ^[[:space:]]*([a-zA-Z_][a-zA-Z0-9_]*)[[:space:]]*=[[:space:]]*\{[[:space:]]*$ ]]; then block_name="${BASH_REMATCH[1]}" in_block=true block_content="" continue fi # 检测配置块结束 if [[ "$in_block" == true && "$line" =~ ^[[:space:]]*\}[[:space:]]*$ ]]; then if [[ "$first_item" == false ]]; then echo "," >> "$output_file" fi echo " \"$block_name\": {" >> "$output_file" echo "$block_content" >> "$output_file" echo " }" >> "$output_file" first_item=false in_block=false continue fi # 处理块内容 if [[ "$in_block" == true ]]; then if [[ "$line" =~ ^[[:space:]]*([a-zA-Z_][a-zA-Z0-9_]*)[[:space:]]*=[[:space:]]*\"([^\"]*)\"|^[[:space:]]*([a-zA-Z_][a-zA-Z0-9_]*)[[:space:]]*=[[:space:]]*([^[:space:]]+) ]]; then local key="${BASH_REMATCH[1]:-${BASH_REMATCH[3]}}" local value="${BASH_REMATCH[2]:-${BASH_REMATCH[4]}}" if [[ -n "$block_content" ]]; then block_content+="," fi block_content+="\n \"$key\": \"$value\"" fi continue fi # 处理简单变量 if [[ "$line" =~ ^[[:space:]]*([a-zA-Z_][a-zA-Z0-9_]*)[[:space:]]*=[[:space:]]*\"([^\"]*)\"|^[[:space:]]*([a-zA-Z_][a-zA-Z0-9_]*)[[:space:]]*=[[:space:]]*([^[:space:]]+) ]]; then local var_name="${BASH_REMATCH[1]:-${BASH_REMATCH[3]}}" local var_value="${BASH_REMATCH[2]:-${BASH_REMATCH[4]}}" if [[ "$first_item" == false ]]; then echo "," >> "$output_file" fi echo " \"$var_name\": \"$var_value\"" >> "$output_file" first_item=false fi done < "$tfvars_file" echo "}" >> "$output_file" } # 上传配置到 Consul upload_config_to_consul() { local config_file="$1" local uploaded_count=0 log_info "开始上传配置到 Consul..." # 读取 JSON 配置 if [[ ! -f "$config_file" ]]; then log_error "配置文件不存在: $config_file" return 1 fi # 上传 Oracle Cloud 配置 local oci_tenancy=$(jq -r '.oci_tenancy_ocid // empty' "$config_file") local oci_user=$(jq -r '.oci_user_ocid // empty' "$config_file") local oci_fingerprint=$(jq -r '.oci_fingerprint // empty' "$config_file") local oci_private_key_path=$(jq -r '.oci_private_key_path // empty' "$config_file") local oci_compartment=$(jq -r '.oci_compartment_ocid // empty' "$config_file") local oci_region=$(jq -r '.oci_region // empty' "$config_file") if [[ -n "$oci_tenancy" && "$oci_tenancy" != "null" && "$oci_tenancy" != "" ]]; then ======= # 上传配置到 Consul upload_config_to_consul() { local config_file="$1" local uploaded_count=0 log_info "开始上传配置到 Consul..." # 读取 JSON 配置 if [[ ! -f "$config_file" ]]; then log_error "配置文件不存在: $config_file" return 1 fi # 上传 Oracle Cloud 配置 local oci_tenancy=$(jq -r '.oci_tenancy_ocid // empty' "$config_file") local oci_user=$(jq -r '.oci_user_ocid // empty' "$config_file") local oci_fingerprint=$(jq -r '.oci_fingerprint // empty' "$config_file") local oci_private_key_path=$(jq -r '.oci_private_key_path // empty' "$config_file") local oci_compartment=$(jq -r '.oci_compartment_ocid // empty' "$config_file") local oci_region=$(jq -r '.oci_region // empty' "$config_file") if [[ -n "$oci_tenancy" && "$oci_tenancy" != "null" && "$oci_tenancy" != "" ]]; then log_info "上传 Oracle Cloud 配置..." local base_path="config/${ENVIRONMENT}/oracle" local tenancy_ocid=$(jq -r '.oci_config.tenancy_ocid // empty' "$config_file") local user_ocid=$(jq -r '.oci_config.user_ocid // empty' "$config_file") local fingerprint=$(jq -r '.oci_config.fingerprint // empty' "$config_file") local private_key_path=$(jq -r '.oci_config.private_key_path // empty' "$config_file") local compartment_ocid=$(jq -r '.oci_config.compartment_ocid // empty' "$config_file") local region=$(jq -r '.oci_config.region // "ap-seoul-1"' "$config_file") # 上传非空配置 [[ -n "$tenancy_ocid" && "$tenancy_ocid" != "null" ]] && { curl -s -X PUT "${CONSUL_ADDR}/v1/kv/${base_path}/tenancy_ocid" -d "$tenancy_ocid" > /dev/null ((uploaded_count++)) } [[ -n "$user_ocid" && "$user_ocid" != "null" ]] && { curl -s -X PUT "${CONSUL_ADDR}/v1/kv/${base_path}/user_ocid" -d "$user_ocid" > /dev/null ((uploaded_count++)) } [[ -n "$fingerprint" && "$fingerprint" != "null" ]] && { curl -s -X PUT "${CONSUL_ADDR}/v1/kv/${base_path}/fingerprint" -d "$fingerprint" > /dev/null ((uploaded_count++)) } [[ -n "$compartment_ocid" && "$compartment_ocid" != "null" ]] && { curl -s -X PUT "${CONSUL_ADDR}/v1/kv/${base_path}/compartment_ocid" -d "$compartment_ocid" > /dev/null ((uploaded_count++)) } [[ -n "$region" && "$region" != "null" ]] && { curl -s -X PUT "${CONSUL_ADDR}/v1/kv/${base_path}/region" -d "$region" > /dev/null ((uploaded_count++)) } # 上传私钥文件内容 if [[ -n "$private_key_path" && "$private_key_path" != "null" && -f "$private_key_path" ]]; then local private_key_content=$(cat "$private_key_path") curl -s -X PUT "${CONSUL_ADDR}/v1/kv/${base_path}/private_key" -d "$private_key_content" > /dev/null ((uploaded_count++)) fi log_success "Oracle Cloud 配置已上传" fi # 上传华为云配置 if jq -e '.huawei_config' "$config_file" > /dev/null 2>&1; then log_info "上传华为云配置..." local base_path="config/${ENVIRONMENT}/huawei" local access_key=$(jq -r '.huawei_config.access_key // empty' "$config_file") local secret_key=$(jq -r '.huawei_config.secret_key // empty' "$config_file") local region=$(jq -r '.huawei_config.region // "cn-north-4"' "$config_file") local project_id=$(jq -r '.huawei_config.project_id // empty' "$config_file") [[ -n "$access_key" && "$access_key" != "null" && "$access_key" != "" ]] && { curl -s -X PUT "${CONSUL_ADDR}/v1/kv/${base_path}/access_key" -d "$access_key" > /dev/null ((uploaded_count++)) } [[ -n "$secret_key" && "$secret_key" != "null" && "$secret_key" != "" ]] && { curl -s -X PUT "${CONSUL_ADDR}/v1/kv/${base_path}/secret_key" -d "$secret_key" > /dev/null ((uploaded_count++)) } [[ -n "$region" && "$region" != "null" ]] && { curl -s -X PUT "${CONSUL_ADDR}/v1/kv/${base_path}/region" -d "$region" > /dev/null ((uploaded_count++)) } [[ -n "$project_id" && "$project_id" != "null" && "$project_id" != "" ]] && { curl -s -X PUT "${CONSUL_ADDR}/v1/kv/${base_path}/project_id" -d "$project_id" > /dev/null ((uploaded_count++)) } log_success "华为云配置已上传" fi # 上传 AWS 配置 if jq -e '.aws_config' "$config_file" > /dev/null 2>&1; then log_info "上传 AWS 配置..." local base_path="config/${ENVIRONMENT}/aws" local access_key=$(jq -r '.aws_config.access_key // empty' "$config_file") local secret_key=$(jq -r '.aws_config.secret_key // empty' "$config_file") local region=$(jq -r '.aws_config.region // "ap-northeast-2"' "$config_file") [[ -n "$access_key" && "$access_key" != "null" && "$access_key" != "" ]] && { curl -s -X PUT "${CONSUL_ADDR}/v1/kv/${base_path}/access_key" -d "$access_key" > /dev/null ((uploaded_count++)) } [[ -n "$secret_key" && "$secret_key" != "null" && "$secret_key" != "" ]] && { curl -s -X PUT "${CONSUL_ADDR}/v1/kv/${base_path}/secret_key" -d "$secret_key" > /dev/null ((uploaded_count++)) } [[ -n "$region" && "$region" != "null" ]] && { curl -s -X PUT "${CONSUL_ADDR}/v1/kv/${base_path}/region" -d "$region" > /dev/null ((uploaded_count++)) } log_success "AWS 配置已上传" fi # 上传 DigitalOcean 配置 if jq -e '.do_config' "$config_file" > /dev/null 2>&1; then log_info "上传 DigitalOcean 配置..." local base_path="config/${ENVIRONMENT}/digitalocean" local token=$(jq -r '.do_config.token // empty' "$config_file") local region=$(jq -r '.do_config.region // "sgp1"' "$config_file") [[ -n "$token" && "$token" != "null" && "$token" != "" ]] && { curl -s -X PUT "${CONSUL_ADDR}/v1/kv/${base_path}/token" -d "$token" > /dev/null ((uploaded_count++)) } [[ -n "$region" && "$region" != "null" ]] && { curl -s -X PUT "${CONSUL_ADDR}/v1/kv/${base_path}/region" -d "$region" > /dev/null ((uploaded_count++)) } log_success "DigitalOcean 配置已上传" fi # 上传 Google Cloud 配置 if jq -e '.gcp_config' "$config_file" > /dev/null 2>&1; then log_info "上传 Google Cloud 配置..." local base_path="config/${ENVIRONMENT}/gcp" local project_id=$(jq -r '.gcp_config.project_id // empty' "$config_file") local region=$(jq -r '.gcp_config.region // "asia-northeast3"' "$config_file") local zone=$(jq -r '.gcp_config.zone // "asia-northeast3-a"' "$config_file") local credentials_file=$(jq -r '.gcp_config.credentials_file // empty' "$config_file") [[ -n "$project_id" && "$project_id" != "null" && "$project_id" != "" ]] && { curl -s -X PUT "${CONSUL_ADDR}/v1/kv/${base_path}/project_id" -d "$project_id" > /dev/null ((uploaded_count++)) } [[ -n "$region" && "$region" != "null" ]] && { curl -s -X PUT "${CONSUL_ADDR}/v1/kv/${base_path}/region" -d "$region" > /dev/null ((uploaded_count++)) } [[ -n "$zone" && "$zone" != "null" ]] && { curl -s -X PUT "${CONSUL_ADDR}/v1/kv/${base_path}/zone" -d "$zone" > /dev/null ((uploaded_count++)) } # 上传凭证文件内容 if [[ -n "$credentials_file" && "$credentials_file" != "null" && -f "$credentials_file" ]]; then local credentials_content=$(cat "$credentials_file") curl -s -X PUT "${CONSUL_ADDR}/v1/kv/${base_path}/credentials" -d "$credentials_content" > /dev/null ((uploaded_count++)) fi log_success "Google Cloud 配置已上传" fi log_success "总共上传了 $uploaded_count 个配置项到 Consul" } # 列出 Consul 中的配置 list_consul_configs() { log_info "列出 Consul 中的配置..." local base_path="config/${ENVIRONMENT}" echo "=== Consul 中的配置 ===" # 获取所有配置键 local keys=$(curl -s "${CONSUL_ADDR}/v1/kv/${base_path}/?keys" | jq -r '.[]' 2>/dev/null || echo "") if [[ -z "$keys" ]]; then log_warning "Consul 中没有找到配置" return fi echo "$keys" | while read -r key; do local value=$(curl -s "${CONSUL_ADDR}/v1/kv/${key}?raw" 2>/dev/null || echo "无法读取") # 隐藏敏感信息 if [[ "$key" =~ (secret|key|token|password) ]]; then echo "$key: [已隐藏]" else echo "$key: $value" fi done } # 清理 Consul 配置 cleanup_consul_configs() { log_warning "清理 Consul 配置..." read -p "确定要删除环境 '$ENVIRONMENT' 的所有配置吗?(y/N): " confirm if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then log_info "操作已取消" return fi local base_path="config/${ENVIRONMENT}" curl -s -X DELETE "${CONSUL_ADDR}/v1/kv/${base_path}?recurse" > /dev/null log_success "环境 '$ENVIRONMENT' 的配置已清理" } # 显示帮助信息 show_help() { cat << EOF OpenTofu 密钥上传脚本 用法: $0 [选项] 选项: upload 上传 terraform.tfvars 中的配置到 Consul list 列出 Consul 中的配置 cleanup 清理 Consul 中的配置 help 显示此帮助信息 环境变量: CONSUL_ADDR Consul 地址 (默认: http://localhost:8500) CONSUL_TOKEN Consul ACL Token (可选) ENVIRONMENT 环境名称 (默认: dev) TOFU_DIR OpenTofu 目录 (默认: tofu/environments/\${ENVIRONMENT}) TFVARS_FILE 变量文件路径 (默认: \${TOFU_DIR}/terraform.tfvars) 示例: # 上传配置到 Consul $0 upload # 列出 Consul 中的配置 $0 list # 清理配置 $0 cleanup # 指定不同环境 ENVIRONMENT=production $0 upload EOF } # 主函数 main() { check_dependencies case "${1:-help}" in "upload") check_consul check_tfvars_file log_info "解析配置文件: $TFVARS_FILE" local config_json=$(manual_parse_tfvars "$TFVARS_FILE" "/tmp/parsed_config.json") upload_config_to_consul "/tmp/parsed_config.json" # 清理临时文件 rm -f /tmp/parsed_config.json /tmp/temp_config.tf ;; "list") check_consul list_consul_configs ;; "cleanup") check_consul cleanup_consul_configs ;; "help"|*) show_help ;; esac } main "$@"