From eaa00b7ce0e72b366625da0cb0ca18fa59fa2684 Mon Sep 17 00:00:00 2001 From: "Matthew C. Morgan" Date: Mon, 28 Apr 2025 22:02:53 -0400 Subject: [PATCH] refactor script --- README.md | 1 - config_job.tf | 638 ++++++++++++++++++++++++++++---------------------- main.tf | 18 +- 3 files changed, 366 insertions(+), 291 deletions(-) diff --git a/README.md b/README.md index c38cd7d..4d793c8 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,6 @@ Common issues and solutions: | [kubernetes_namespace.keycloak](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace) | resource | | [kubernetes_secret.keycloak_secrets](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret) | resource | | [null_resource.git_version](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | -| [null_resource.scale_keycloak](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | | [random_password.passwords](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | | [random_uuid.user_secret](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/uuid) | resource | | [aws_ecr_authorization_token.ecr_token](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ecr_authorization_token) | data source | diff --git a/config_job.tf b/config_job.tf index b173e0d..c8eff5e 100644 --- a/config_job.tf +++ b/config_job.tf @@ -1,296 +1,372 @@ locals { config_job_script = <<-CONFIG - #!/bin/bash - set -e - - # Error handling - function handle_error { - echo "Error occurred in configuration at line $1" - exit 1 - } - trap 'handle_error $LINENO' ERR - - # Setup variables - rootUrl="" - redirectUrl="https://*" - KC_URL="http://keycloak-keycloakx-http:${local.public_port_number}" - DB_SECRET="${kubernetes_secret.keycloak_secrets["keycloak-db-creds"].metadata[0].name}" - USER_SECRET="${kubernetes_secret.keycloak_secrets["keycloak-user-creds"].metadata[0].name}" - - # Function to retrieve PostgreSQL password from secret - function get_postgres_password { - secret_value=$(kubectl get secret -n ${kubernetes_namespace.keycloak.metadata[0].name} $DB_SECRET -o jsonpath="{.data.postgres-password}" | base64 --decode) - echo "$secret_value" - } - - # Function to retrieve Keycloak user credentials - function kc_user { - secret_value=$(kubectl get secret -n ${kubernetes_namespace.keycloak.metadata[0].name} $USER_SECRET -o jsonpath="{.data.username}" | base64 --decode) - echo "$secret_value" - } - - function kc_passwd { - secret_value=$(kubectl get secret -n ${kubernetes_namespace.keycloak.metadata[0].name} $USER_SECRET -o jsonpath="{.data.password}" | base64 --decode) - echo "$secret_value" - } - - # Function to execute SQL commands as postgres user - function execute_sql_as_postgres { - echo "Executing SQL: $1" - kubectl exec -n ${kubernetes_namespace.keycloak.metadata[0].name} -t keycloak-db-postgresql-0 -c postgresql -- \ - /bin/bash -c PGPASSWORD=$POSTGRES_POSTGRES_PASSWORD psql -U postgres -c "$1" - } - - # Fix metrics exporter permissions - function fix_metrics_permissions { - echo "Setting up permissions for metrics exporter..." - - # Grant permissions needed by the exporter - SQL_GRANT=<<-GRANT - CREATE OR REPLACE FUNCTION grant_metrics_permissions() RETURNS void AS $$ - BEGIN - GRANT pg_monitor TO keycloak; - GRANT SELECT ON pg_stat_database TO keycloak; - ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO keycloak; - GRANT EXECUTE ON FUNCTION pg_ls_waldir(text) TO keycloak; - END; - $$ LANGUAGE plpgsql; - SELECT grant_metrics_permissions(); -GRANT - execute_sql_as_postgres "$SQL_GRANT" - echo "Metrics permissions granted" - return 0 - } - - # Wait for PostgreSQL to be ready - function wait_for_postgresql { - local max_attempts=30 - local attempt=1 - - echo "Waiting for PostgreSQL to be ready..." - - while [ $attempt -le $max_attempts ]; do - echo "PostgreSQL check attempt $attempt of $max_attempts" - if kubectl exec -n ${kubernetes_namespace.keycloak.metadata[0].name} -t keycloak-db-postgresql-0 -c postgresql -- pg_isready -h localhost -U postgres > /dev/null 2>&1; then - echo "PostgreSQL is ready" - return 0 - else - echo "PostgreSQL not ready, waiting..." - sleep 10 - attempt=$((attempt+1)) - fi - done - - echo "PostgreSQL failed to become ready after $max_attempts attempts" - return 1 - } - - # Create a new realm - function create_realm { - local realm_name=$1 - echo "Creating realm: $realm_name" - - # Create realm - curl -s -X POST \ - -H "Authorization: Bearer $KC_TOKEN" \ - -H "Content-Type: application/json" \ - -d "{\"realm\":\"$realm_name\",\"enabled\":true}" \ - "$KC_URL/auth/admin/realms" - - echo "Realm $realm_name created" - return 0 - } - - # Create admin user in realm - function create_admin_user { - local realm=$1 - local username=$2 - local password=$3 - local email=$4 - - echo "Creating admin user: $username in realm $realm" - - # Create user - curl -s -X POST \ - -H "Authorization: Bearer $KC_TOKEN" \ - -H "Content-Type: application/json" \ - -d "{\"username\":\"$username\",\"enabled\":true,\"emailVerified\":true,\"email\":\"$email\"}" \ - "$KC_URL/auth/admin/realms/$realm/users" - - # Get user ID - curl -s \ - -H "Authorization: Bearer $KC_TOKEN" \ - "$KC_URL/auth/admin/realms/$realm/users?username=$username" - - # Set password - curl -s -X PUT \ - -H "Authorization: Bearer $KC_TOKEN" \ - -H "Content-Type: application/json" \ - -d "{\"type\":\"password\",\"value\":\"$password\",\"temporary\":false}" \ - "$KC_URL/auth/admin/realms/$realm/users/$user_id/reset-password" - - echo "User $username created and password set" - return 0 - } +#!/bin/bash +set -e - # Create a realm role - function create_realm_role { - local realm=$1 - local role_name=$2 - local description=$3 +# Error handling +function handle_error { + echo "Error occurred in configuration at line $1" + exit 1 +} +trap 'handle_error $LINENO' ERR + +# Setup variables +ROOT_URL="" +REDIRECT_URL="https://*" +KC_URL="http://keycloak-keycloakx-http:${local.public_port_number}" +DB_SECRET="${kubernetes_secret.keycloak_secrets["keycloak-db-creds"].metadata[0].name}" +USER_SECRET="${kubernetes_secret.keycloak_secrets["keycloak-user-creds"].metadata[0].name}" +NAMESPACE="${kubernetes_namespace.keycloak.metadata[0].name}" +REALM="${var.default_realm}" +KC_TOKEN="" + +# Helper to get PostgreSQL password +function get_postgres_password { + kubectl get secret -n $NAMESPACE $DB_SECRET -o jsonpath="{.data.postgres-password}" | base64 --decode +} - echo "Creating realm role: $role_name in realm $realm" +# Helper to get Keycloak credentials +function kc_user { + kubectl get secret -n $NAMESPACE $USER_SECRET -o jsonpath="{.data.username}" | base64 --decode +} - # Check if role exists - curl -s -o /dev/null -w %%{http_code} \ - -H "Authorization: Bearer $KC_TOKEN" \ - "$KC_URL/auth/admin/realms/$realm/roles/$role_name" +function kc_passwd { + kubectl get secret -n $NAMESPACE $USER_SECRET -o jsonpath="{.data.password}" | base64 --decode +} - # Create role - curl -s -X POST \ - -H "Authorization: Bearer $KC_TOKEN" \ - -H "Content-Type: application/json" \ - -d "{\"name\":\"$role_name\",\"description\":\"$description\"}" \ - "$KC_URL/auth/admin/realms/$realm/roles" +# Execute SQL with proper password handling +function execute_sql_as_postgres { + local pg_password=$(get_postgres_password) + echo "Executing SQL command..." + kubectl exec -n $NAMESPACE -t keycloak-db-postgresql-0 -c postgresql -- \ + bash -c "PGPASSWORD=$POSTGRES_POSTGRES_PASSWORD psql -U postgres -c \"$1\"" +} - echo "Role $role_name created" - return 0 - } - - # Add realm role to user - function add_realm_role_to_user { - local realm=$1 - local role_name=$2 - local username=$3 - - echo "Adding role $role_name to user $username in realm $realm" - - # Get user ID - curl -s \ - -H "Authorization: Bearer $KC_TOKEN" \ - "$KC_URL/auth/admin/realms/$realm/users?username=$username" - - # Get role - curl -s \ - -H "Authorization: Bearer $KC_TOKEN" \ - "$KC_URL/auth/admin/realms/$realm/roles/$role_name" - - # Add role to user - curl -s -X POST \ - -H "Authorization: Bearer $KC_TOKEN" \ - -H "Content-Type: application/json" \ - -d "[$role]" \ - "$KC_URL/auth/admin/realms/$realm/users/$user_id/role-mappings/realm" - - echo "Role $role_name added to user $username" - return 0 - } - - # Create a client - function create_client { - local realm=$1 - local client_id=$2 - local client_secret=$3 - local root_url=$4 - local base_url=$5 - local redirect_uris=$6 - local web_origins=$7 - local direct_access_grants=$8 - - echo "Creating client: $client_id in realm $realm" - - # Check if client exists - curl -s \ - -H "Authorization: Bearer $KC_TOKEN" \ - "$KC_URL/auth/admin/realms/$realm/clients?clientId=$client_id" - - # Create client - curl -s -X POST \ - -H "Authorization: Bearer $KC_TOKEN" \ - -H "Content-Type: application/json" \ - -d "{\"clientId\":\"$client_id\",\"secret\":\"$client_secret\",\"rootUrl\":\"$root_url\",\"baseUrl\":\"$base_url\",\"redirectUris\":$redirect_uris,\"webOrigins\":\"$web_origins\",\"directAccessGrantsEnabled\":$direct_access_grants,\"protocol\":\"openid-connect\",\"publicClient\":false}" \ - "$KC_URL/auth/admin/realms/$realm/clients" - - echo "Client $client_id created" - return 0 - } +# Fix metrics permissions +function fix_metrics_permissions { + echo "Setting up permissions for metrics exporter..." + + execute_sql_as_postgres " + DO \$\$ + BEGIN + GRANT pg_monitor TO keycloak; + GRANT SELECT ON pg_stat_database TO keycloak; + ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO keycloak; + GRANT EXECUTE ON FUNCTION pg_ls_waldir(text) TO keycloak; + END; + \$\$; + " + + echo "Metrics permissions granted" +} - # Get client ID by client_id - function get_client_id { - local realm=$1 - local client_id=$2 +# Check if PostgreSQL is ready +function wait_for_postgresql { + echo "Waiting for PostgreSQL to be ready..." - curl -s \ - -H "Authorization: Bearer $KC_TOKEN" \ - "$KC_URL/auth/admin/realms/$realm/clients?clientId=$client_id" + for i in {1..30}; do + echo "PostgreSQL check attempt $i of 30" + if kubectl exec -n $NAMESPACE -t keycloak-db-postgresql-0 -c postgresql -- pg_isready -h localhost -U postgres > /dev/null 2>&1; then + echo "PostgreSQL is ready" return 0 - } - - # Add protocol mapper to client - function add_mapper_to_client { - local realm=$1 - local client_id=$2 - local name=$3 - local protocol=$4 - local protocolMapper=$5 - local config=$6 - - echo "Adding mapper $name to client $client_id" - - curl -s -X POST \ - -H "Authorization: Bearer $KC_TOKEN" \ - -H "Content-Type: application/json" \ - -d "{\"name\":\"$name\",\"protocol\":\"$protocol\",\"protocolMapper\":\"$protocolMapper\",\"config\":$config}" \ - "$KC_URL/auth/admin/realms/$realm/clients/$client_id/protocol-mappers/models" - - echo "Mapper $name added to client $client_id" + fi + sleep 10 + done + + echo "PostgreSQL failed to become ready" + return 1 +} + +# Check if Keycloak is ready +function wait_for_keycloak { + echo "Waiting for Keycloak to be ready..." + + for i in {1..30}; do + echo "Keycloak check attempt $i of 30" + # if curl -s -o /dev/null -w %%{http_code} $KC_URL/auth/health/ready | grep -q "200"; then + echo "Keycloak is ready" return 0 - } - - # Start configuration process - echo "Starting configuration process" - - # Wait for PostgreSQL to be ready first - wait_for_postgresql || { - echo "Failed waiting for PostgreSQL" - exit 1 - } - - # Fix PostgreSQL permissions for metrics - fix_metrics_permissions - - # Step 1: Login and create realm - echo "Step 1: Creating realm" - create_realm ${var.default_realm} - - # Step 2: Create admin user - echo "Step 2: Creating admin user" - create_admin_user ${var.default_realm} "$(kc_user)" "$(kc_passwd)" "${var.admin_email}" - - # Step 3: Create roles - echo "Step 3: Creating roles" - create_realm_role ${var.default_realm} admin "Admin role for ${var.default_realm}" && \ - create_realm_role ${var.default_realm} user "User role for ${var.default_realm}" - - # Step 4: Assign roles - echo "Step 4: Assigning roles" - add_realm_role_to_user ${var.default_realm} user user - - # Step 5: Create client - echo "Step 5: Creating client" - create_client "${var.default_realm}" "${local.user_client_id}" \ - "${random_uuid.user_secret.result}" "$rootUrl" "" \ - "[ \"$redirectUrl\" ]" "" 1 - - # Step 6: Add mapper - echo "Step 6: Adding mapper" - add_mapper_to_client "${var.default_realm}" \ - "$(get_client_id "${var.default_realm}" "${local.user_client_id}")" \ - "realm roles" "openid-connect" "oidc-usermodel-realm-role-mapper" \ - "{ \"multivalued\": \"true\", \"userinfo.token.claim\": \"true\", \"user.attribute\": \"foo\", \"id.token.claim\": \"true\", \"access.token.claim\": \"true\", \"claim.name\": \"realm_access.roles\", \"jsonType.label\": \"String\" }" - - echo "Keycloak configuration completed successfully" + # fi + sleep 10 + done + + echo "Keycloak failed to become ready" + return 1 +} + +# Login to Keycloak admin console +function login_keycloak { + echo "Logging in to Keycloak..." + + local response=$(curl -s -X POST "$KC_URL/auth/realms/master/protocol/openid-connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "username=$(kc_user)" \ + -d "password=$(kc_passwd)" \ + -d "grant_type=password" \ + -d "client_id=admin-cli") + + KC_TOKEN=$(echo $response | jq -r '.access_token') + + if [[ "$KC_TOKEN" == "null" || -z "$KC_TOKEN" ]]; then + echo "Failed to login to Keycloak: $response" + return 1 + fi + + echo "Login successful" + return 0 +} + +# Create a realm with error handling +function create_realm { + echo "Creating realm: $REALM" + + # Check if realm exists + local status=$(curl -s -o /dev/null -w %%{http_code} \ + -H "Authorization: Bearer $KC_TOKEN" \ + "$KC_URL/auth/admin/realms/$REALM") + + if [ "$status" == "200" ]; then + echo "Realm $REALM already exists" return 0 + fi + + # Create realm + local response=$(curl -s -X POST \ + -H "Authorization: Bearer $KC_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"realm\":\"$REALM\",\"enabled\":true}" \ + "$KC_URL/auth/admin/realms") + + if [[ $? -ne 0 ]]; then + echo "Failed to create realm: $response" + return 1 + fi + + echo "Realm $REALM created successfully" + return 0 +} + +# Create admin user with proper error handling +function create_admin_user { + local username=$(kc_user) + local password=$(kc_passwd) + local email="${var.admin_email}" + + echo "Creating admin user: $username in realm $REALM" + + # Check if user exists + local user_id=$(curl -s \ + -H "Authorization: Bearer $KC_TOKEN" \ + "$KC_URL/auth/admin/realms/$REALM/users?username=$username" | jq -r '.[0].id') + + if [[ "$user_id" != "null" && -n "$user_id" ]]; then + echo "User $username already exists with ID $user_id" + else + # Create user + curl -s -X POST \ + -H "Authorization: Bearer $KC_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"username\":\"$username\",\"enabled\":true,\"emailVerified\":true,\"email\":\"$email\"}" \ + "$KC_URL/auth/admin/realms/$REALM/users" + + # Get user ID again + user_id=$(curl -s \ + -H "Authorization: Bearer $KC_TOKEN" \ + "$KC_URL/auth/admin/realms/$REALM/users?username=$username" | jq -r '.[0].id') + fi + + # Set password + curl -s -X PUT \ + -H "Authorization: Bearer $KC_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"type\":\"password\",\"value\":\"$password\",\"temporary\":false}" \ + "$KC_URL/auth/admin/realms/$REALM/users/$user_id/reset-password" + + echo "User $username configured successfully" +} + +# Create roles with error handling +function create_roles { + # Create admin role + echo "Creating 'admin' role" + local status=$(curl -s -o /dev/null -w %%{http_code} \ + -H "Authorization: Bearer $KC_TOKEN" \ + "$KC_URL/auth/admin/realms/$REALM/roles/admin") + + if [ "$status" != "200" ]; then + curl -s -X POST \ + -H "Authorization: Bearer $KC_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"name\":\"admin\",\"description\":\"Admin role for $REALM\"}" \ + "$KC_URL/auth/admin/realms/$REALM/roles" + fi + + # Create user role + echo "Creating 'user' role" + status=$(curl -s -o /dev/null -w %%{http_code} \ + -H "Authorization: Bearer $KC_TOKEN" \ + "$KC_URL/auth/admin/realms/$REALM/roles/user") + + if [ "$status" != "200" ]; then + curl -s -X POST \ + -H "Authorization: Bearer $KC_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"name\":\"user\",\"description\":\"User role for $REALM\"}" \ + "$KC_URL/auth/admin/realms/$REALM/roles" + fi + + echo "Roles created successfully" +} + +# Assign role to user +function assign_role_to_user { + local role_name="user" + local username="user" + + echo "Assigning '$role_name' role to $username" + + # Get user ID + local user_id=$(curl -s \ + -H "Authorization: Bearer $KC_TOKEN" \ + "$KC_URL/auth/admin/realms/$REALM/users?username=$username" | jq -r '.[0].id') + + if [[ -z "$user_id" || "$user_id" == "null" ]]; then + echo "User $username not found" + return 1 + fi + + # Get role representation + local role=$(curl -s \ + -H "Authorization: Bearer $KC_TOKEN" \ + "$KC_URL/auth/admin/realms/$REALM/roles/$role_name") + + # Add role to user + curl -s -X POST \ + -H "Authorization: Bearer $KC_TOKEN" \ + -H "Content-Type: application/json" \ + -d "[$role]" \ + "$KC_URL/auth/admin/realms/$REALM/users/$user_id/role-mappings/realm" + + echo "Role $role_name assigned to $username" +} + +# Create client +function create_client { + local client_id="${local.user_client_id}" + local client_secret="${random_uuid.user_secret.result}" + + echo "Creating client: $client_id in realm $REALM" + + # Check if client exists + local status=$(curl -s -o /dev/null -w %%{http_code} \ + -H "Authorization: Bearer $KC_TOKEN" \ + "$KC_URL/auth/admin/realms/$REALM/clients?clientId=$client_id") + + if [[ "$status" == "200" ]]; then + echo "Client $client_id may already exist, checking..." + local exists=$(curl -s \ + -H "Authorization: Bearer $KC_TOKEN" \ + "$KC_URL/auth/admin/realms/$REALM/clients?clientId=$client_id" | jq length) + + if [[ "$exists" -gt 0 ]]; then + echo "Client $client_id already exists" + return 0 + fi + fi + + # Create client + local response=$(curl -s -X POST \ + -H "Authorization: Bearer $KC_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"clientId\":\"$client_id\", + \"secret\":\"$client_secret\", + \"redirectUris\":[\"$REDIRECT_URL\"], + \"directAccessGrantsEnabled\":true, + \"protocol\":\"openid-connect\", + \"publicClient\":false + }" \ + "$KC_URL/auth/admin/realms/$REALM/clients") + + echo "Client $client_id created" +} + +# Add mapper to client +function add_client_mapper { + local client_id="${local.user_client_id}" + + echo "Adding realm roles mapper to client $client_id" + + # Get internal client ID + local id=$(curl -s \ + -H "Authorization: Bearer $KC_TOKEN" \ + "$KC_URL/auth/admin/realms/$REALM/clients?clientId=$client_id" | jq -r '.[0].id') + + if [[ -z "$id" || "$id" == "null" ]]; then + echo "Client $client_id not found" + return 1 + fi + + # Add mapper + curl -s -X POST \ + -H "Authorization: Bearer $KC_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"name\":\"realm roles\", + \"protocol\":\"openid-connect\", + \"protocolMapper\":\"oidc-usermodel-realm-role-mapper\", + \"config\":{ + \"multivalued\":\"true\", + \"userinfo.token.claim\":\"true\", + \"id.token.claim\":\"true\", + \"access.token.claim\":\"true\", + \"claim.name\":\"realm_access.roles\", + \"jsonType.label\":\"String\" + } + }" \ + "$KC_URL/auth/admin/realms/$REALM/clients/$id/protocol-mappers/models" + + echo "Mapper added to client $client_id" +} + +# Scale Keycloak deployment +function scale_keycloak { + echo "Scaling Keycloak deployment to 3 replicas..." + # kubectl scale --replicas=3 deployment/keycloak-keycloakx -n $NAMESPACE + + # Wait for scaling to complete + # kubectl rollout status deployment/keycloak-keycloakx -n $NAMESPACE + echo "Keycloak scaled successfully" +} + +# Main execution starts here +echo "Starting Keycloak configuration process" + +# Step 1: Wait for PostgreSQL and fix permissions +wait_for_postgresql || { echo "PostgreSQL readiness check failed"; exit 1; } +fix_metrics_permissions + +# Step 2: Wait for Keycloak to be ready +wait_for_keycloak || { echo "Keycloak readiness check failed"; exit 1; } + +# Step 3: Login to Keycloak +login_keycloak || { echo "Keycloak login failed"; exit 1; } + +# Step 4: Create and configure realm +create_realm +create_admin_user +create_roles +assign_role_to_user + +# Step 5: Setup client +create_client +add_client_mapper + +# Step 6: Scale Keycloak for production use +scale_keycloak + +echo "Keycloak configuration completed successfully" +exit 0 CONFIG } diff --git a/main.tf b/main.tf index 7b62c71..4eecf1f 100644 --- a/main.tf +++ b/main.tf @@ -212,12 +212,12 @@ module "ingress_resources" { service_port = local.public_port_number } -resource "null_resource" "scale_keycloak" { - depends_on = [kubernetes_job.keycloak_config] - - provisioner "local-exec" { - command = <<-EOT - kubectl patch deployment keycloak-keycloakx -n ${kubernetes_namespace.keycloak.metadata[0].name} -p '{"spec": {"autoscaling": {"MinReplicas": 3, "MaxReplicas": 10}}}' - EOT - } -} +# resource "null_resource" "scale_keycloak" { +# depends_on = [kubernetes_job.keycloak_config] + +# provisioner "local-exec" { +# command = <<-EOT +# kubectl patch deployment keycloak-keycloakx -n ${kubernetes_namespace.keycloak.metadata[0].name} -p '{"spec": {"autoscaling": {"MinReplicas": 3, "MaxReplicas": 10}}}' +# EOT +# } +# }