diff --git a/.github/workflows/build-deploy-apim.yaml b/.github/workflows/build-deploy-apim.yaml new file mode 100644 index 0000000000..a0a99d878b --- /dev/null +++ b/.github/workflows/build-deploy-apim.yaml @@ -0,0 +1,174 @@ +name: APIM Build & Deploy + +permissions: + id-token: write + contents: read + +on: + push: + branches: + - main + - 'task/DOSIS*' + +jobs: + metadata: + name: "Get CI/CD metadata" + uses: ./.github/workflows/metadata.yaml + + build-and-deploy: + name: "Build and Deploy API" + runs-on: ubuntu-latest + needs: metadata + env: + ENVIRONMENT: internal-dev + API_NAME: dos-search + FHIR_VERSION: FHIR_R4 + WORKSPACE: ${{ needs.metadata.outputs.workspace }} # branch name or PR ref + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + cache: 'pip' + + - name: Set up yq + run: | + sudo wget https://github.com/mikefarah/yq/releases/download/v4.44.1/yq_linux_amd64 -O /usr/local/bin/yq + sudo chmod +x /usr/local/bin/yq + + - name: Install proxygen CLI + run: | + pip install proxygen-cli + echo "$HOME/.local/bin" >> $GITHUB_PATH + echo "$HOME/.proxygen/bin" >> $GITHUB_PATH + + - name: Verify workspace variable + run: | + echo "Workspace: ${{ env.WORKSPACE }}" + echo "Environment: ${{ env.ENVIRONMENT }}" + echo "API Name: ${{ env.API_NAME }}" + echo "FHIR Version: ${{ env.FHIR_VERSION }}" + + - name: Backup original OAS file + run: | + cp docs/specification/dos-search.yaml docs/specification/dos-search.yaml.backup + + - name: Preprocess OAS file + run: | + # Prefix title with workspace tag + yq eval '.info.title = "[" + "${{ env.WORKSPACE }}" + "] " + .info.title' \ + docs/specification/dos-search.yaml.backup > docs/specification/dos-search.yaml + + - name: Display modified OAS info + run: | + echo "Modified OAS title:" + yq eval '.info.title' docs/specification/dos-search.yaml + + # - name: Validate OpenAPI spec + # run: | + # npx @redocly/cli lint docs/specification/dos-search.yaml --format=stylish + + # - name: Security scan OpenAPI spec + # run: | + # # Optional: Add security scanning + # echo "Running security checks..." + # # npx @42crunch/api-security-audit docs/specification/dos-search.yaml || true + + - name: Deploy to APIM + run: | + API_INSTANCE_NAME="${{ env.WORKSPACE }}-${{ env.API_NAME }}_${{ env.FHIR_VERSION }}" + echo "Deploying API instance: ${API_INSTANCE_NAME}" + + proxygen instance deploy \ + "${{ env.ENVIRONMENT }}" \ + "${API_INSTANCE_NAME}" \ + "docs/specification/dos-search.yaml" + + - name: Post-deployment verification + run: | + API_INSTANCE_NAME="${{ env.WORKSPACE }}-${{ env.API_NAME }}_${{ env.FHIR_VERSION }}" + echo "Verifying deployment of: ${API_INSTANCE_NAME}" + + # List instances and check if our API exists + if proxygen instance list "${{ env.ENVIRONMENT }}" | grep -q "${API_INSTANCE_NAME}"; then + echo "✅ API instance deployed successfully: ${API_INSTANCE_NAME}" + else + echo "❌ API instance not found: ${API_INSTANCE_NAME}" + echo "Available instances:" + proxygen instance list "${{ env.ENVIRONMENT }}" + exit 1 + fi + + - name: Generate deployment report + if: always() + run: | + cat > deployment-report.md << EOF + # Deployment Report + + ## Details + - **Environment**: ${{ env.ENVIRONMENT }} + - **API Name**: ${{ env.API_NAME }} + - **FHIR Version**: ${{ env.FHIR_VERSION }} + - **Workspace**: ${{ env.WORKSPACE }} + - **Instance Name**: ${{ env.WORKSPACE }}-${{ env.API_NAME }}_${{ env.FHIR_VERSION }} + - **Commit**: ${{ github.sha }} + - **Branch**: ${{ github.ref_name }} + - **Actor**: ${{ github.actor }} + - **Timestamp**: $(date -u +"%Y-%m-%d %H:%M:%S UTC") + + ## Status + - Validation: ${{ job.status == 'success' && '✅ Passed' || '❌ Failed' }} + - Deployment: ${{ job.status == 'success' && '✅ Successful' || '❌ Failed' }} + + ## Links + - [Workflow Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) + - [Commit](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}) + EOF + + - name: Upload deployment artifacts + uses: actions/upload-artifact@v4 + if: always() + with: + name: deployment-artifacts-${{ env.WORKSPACE }} + path: | + docs/specification/dos-search.yaml + docs/specification/dos-search.yaml.backup + deployment-report.md + retention-days: 30 + + - name: Cleanup on failure + if: failure() + run: | + echo "Deployment failed, attempting cleanup..." + API_INSTANCE_NAME="${{ env.WORKSPACE }}-${{ env.API_NAME }}_${{ env.FHIR_VERSION }}" + + # Optional: Remove failed deployment + # proxygen instance delete "${{ env.ENVIRONMENT }}" "${API_INSTANCE_NAME}" || true + + echo "Cleanup completed" + + notify: + name: "Notify deployment status" + runs-on: ubuntu-latest + needs: [metadata, build-and-deploy] + if: always() + + steps: + - name: Notify on success + if: needs.build-and-deploy.result == 'success' + run: | + echo "🎉 Deployment successful!" + echo "Environment: internal-dev" + echo "API Instance: ${{ needs.metadata.outputs.workspace }}-dos-search_FHIR_R4" + # Add notification logic here (Slack, Teams, email, etc.) + + - name: Notify on failure + if: needs.build-and-deploy.result == 'failure' + run: | + echo "💥 Deployment failed!" + echo "Check the workflow logs for details" + # Add error notification logic here diff --git a/infrastructure/stacks/gp_search/s3.tf b/infrastructure/stacks/gp_search/s3.tf index 1d9601f04e..3e517cbdbc 100644 --- a/infrastructure/stacks/gp_search/s3.tf +++ b/infrastructure/stacks/gp_search/s3.tf @@ -4,3 +4,10 @@ module "s3" { force_destroy = true s3_logging_bucket = local.s3_logging_bucket } + +module "proxygen-s3" { + source = "../../modules/s3" + bucket_name = "${local.resource_prefix}-${var.s3_bucket_name}-proxygen" + force_destroy = true + s3_logging_bucket = local.s3_logging_bucket +} diff --git a/infrastructure/stacks/gp_search/secrets.tf b/infrastructure/stacks/gp_search/secrets.tf new file mode 100644 index 0000000000..f8d5f61961 --- /dev/null +++ b/infrastructure/stacks/gp_search/secrets.tf @@ -0,0 +1,27 @@ +resource "aws_secretsmanager_secret" "proxygen_private_key" { + # checkov:skip=CKV2_AWS_57: TODO https://nhsd-jira.digital.nhs.uk/browse/FDOS-405 + # checkov:skip=CKV_AWS_149: TODO https://nhsd-jira.digital.nhs.uk/browse/FDOS-405 + name = "/${var.project}/${var.environment}/proxygen-private-key" + description = "Private key for proxygen" +} + +resource "aws_secretsmanager_secret" "proxygen_public_key" { + # checkov:skip=CKV2_AWS_57: TODO https://nhsd-jira.digital.nhs.uk/browse/FDOS-405 + # checkov:skip=CKV_AWS_149: TODO https://nhsd-jira.digital.nhs.uk/browse/FDOS-405 + name = "/${var.project}/${var.environment}/proxygen-public-key" + description = "Public key for proxygen" +} + +resource "aws_secretsmanager_secret" "proxygen_key_id" { + # checkov:skip=CKV2_AWS_57: TODO https://nhsd-jira.digital.nhs.uk/browse/FDOS-405 + # checkov:skip=CKV_AWS_149: TODO https://nhsd-jira.digital.nhs.uk/browse/FDOS-405 + name = "/${var.project}/${var.environment}/proxygen-key-id" + description = "Key id for proxygen" +} + +resource "aws_secretsmanager_secret" "proxygen_client_id" { + # checkov:skip=CKV2_AWS_57: TODO https://nhsd-jira.digital.nhs.uk/browse/FDOS-405 + # checkov:skip=CKV_AWS_149: TODO https://nhsd-jira.digital.nhs.uk/browse/FDOS-405 + name = "/${var.project}/${var.environment}/proxygen-client-id" + description = "Client id for proxygen" +} diff --git a/scripts/workflow/deploy-apim-proxy.sh b/scripts/workflow/deploy-apim-proxy.sh new file mode 100644 index 0000000000..f6b9db4386 --- /dev/null +++ b/scripts/workflow/deploy-apim-proxy.sh @@ -0,0 +1,292 @@ +#!/bin/bash + +# AWS Secrets Manager retrieval and Proxygen deployment script for GitHub Actions +# This script fetches secrets, retrieves private key from S3, sets up Proxygen credentials, and deploys + +set -euo pipefail + +# Define the secret paths (excluding private key which will come from S3) +SECRETS=( + "/ftrs-dos/dev/proxygen-key-id" + "/ftrs-dos/dev/proxygen-client-id" + "/ftrs-dos/dev/proxygen-public-key" + "/temp/dev/api-ca-cert" + "/temp/dev/api-ca-pk" +) + +# Define corresponding output variable names +OUTPUT_VARS=( + "PROXYGEN_KEY_ID" + "PROXYGEN_CLIENT_ID" + "PROXYGEN_PUBLIC_KEY" + "API_CA_CERT" + "API_CA_PK" +) + +# S3 configuration for private key +S3_BUCKET="" +S3_PRIVATE_KEY_PATH="ftrs-dos/dev/proxygen-private-key" +PROXYGEN_PRIVATE_KEY_FILE="/tmp/proxygen-private-key.pem" + +# Proxygen configuration +PROXYGEN_INSTANCE="internal-dev" +PROXYGEN_SERVICE_NAME="ftrs-search-" # not sure how to handle workspace here +OASPEC_FILE="docs/specification/dos-search.yaml" +PROXYGEN_CONFIG_FILE="/tmp/proxygen-config.json" + +echo "=== Starting Proxygen Deployment Process ===" + +# Check if required CLI tools are available +check_dependencies() { + local deps=("aws" "proxygen" "yq") + for dep in "${deps[@]}"; do + if ! command -v "$dep" &> /dev/null; then + echo "Error: $dep is not installed or not in PATH" + exit 1 + fi + done + echo "✓ All dependencies available (including Proxygen CLI and yq)" +} + +# Function to retrieve and output secret +retrieve_secret() { + local secret_path="$1" + local output_var="$2" + + echo "Retrieving secret: $secret_path" + + # Retrieve the secret value + local secret_value + if secret_value=$(aws secretsmanager get-secret-value --secret-id "$secret_path" --query 'SecretString' --output text 2>/dev/null); then + # Export as environment variable + export "$output_var"="$secret_value" + + # Mask the secret in GitHub Actions logs + echo "::add-mask::$secret_value" + echo "$output_var=***MASKED***" >> $GITHUB_ENV + + echo "✓ Successfully retrieved $output_var" + else + echo "Error: Failed to retrieve secret $secret_path" + exit 1 + fi +} + +# Function to retrieve private key from S3 +retrieve_private_key_from_s3() { + echo "Retrieving private key from S3: s3://$S3_BUCKET/$S3_PRIVATE_KEY_PATH" + + if aws s3 cp "s3://$S3_BUCKET/$S3_PRIVATE_KEY_PATH" "$PROXYGEN_PRIVATE_KEY_FILE" 2>/dev/null; then + # Set proper permissions for private key + chmod 600 "$PROXYGEN_PRIVATE_KEY_FILE" + + # Export path as environment variable + export PROXYGEN_PRIVATE_KEY="$PROXYGEN_PRIVATE_KEY_FILE" + echo "PROXYGEN_PRIVATE_KEY=$PROXYGEN_PRIVATE_KEY_FILE" >> $GITHUB_ENV + + echo "✓ Successfully retrieved private key from S3" + else + echo "Error: Failed to retrieve private key from S3" + exit 1 + fi +} + +# Function to set up Proxygen CLI credentials +setup_proxygen_auth() { + echo "Setting up Proxygen CLI credentials..." + + # Set Proxygen credentials using the credentials set command + echo "Running: proxygen credentials set private_key_path $PROXYGEN_PRIVATE_KEY key_id $PROXYGEN_KEY_ID client_id $PROXYGEN_CLIENT_ID" + + if proxygen credentials set \ + private_key_path "$PROXYGEN_PRIVATE_KEY" \ + key_id "$PROXYGEN_KEY_ID" \ + client_id "$PROXYGEN_CLIENT_ID"; then + echo "✓ Proxygen CLI credentials configured successfully" + + # Verify credentials are set + echo "Verifying Proxygen credentials..." + if proxygen credentials show 2>/dev/null; then + echo "✓ Credentials verification successful" + else + echo "⚠ Could not verify credentials, but set command succeeded" + fi + else + echo "Error: Failed to set Proxygen CLI credentials" + echo "Command failed: proxygen credentials set private_key_path $PROXYGEN_PRIVATE_KEY key_id $PROXYGEN_KEY_ID client_id $PROXYGEN_CLIENT_ID" + exit 1 + fi +} + +# Function to preprocess OAS specification file +preprocess_oaspec() { + echo "Preprocessing OpenAPI specification file..." + + local workspace="${WORKSPACE:-dev}" + local backup_file="${OASPEC_FILE}.backup" + + # Create backup of original file + if [[ -f "$OASPEC_FILE" ]]; then + echo "Creating backup: $backup_file" + cp "$OASPEC_FILE" "$backup_file" + else + echo "Error: OpenAPI spec file '$OASPEC_FILE' not found" + exit 1 + fi + + # Preprocess the OAS file - prefix title with workspace tag + echo "Prefixing title with workspace tag: [$workspace]" + + if yq eval ".info.title = \"[\" + \"$workspace\" + \"] \" + .info.title" "$backup_file" > "$OASPEC_FILE"; then + echo "✓ Successfully preprocessed OAS file" + + # Show the updated title for verification + local new_title + if new_title=$(yq eval '.info.title' "$OASPEC_FILE" 2>/dev/null); then + echo "Updated title: $new_title" + fi + else + echo "Error: Failed to preprocess OAS file" + echo "Restoring original file..." + mv "$backup_file" "$OASPEC_FILE" + exit 1 + fi + + # Optionally show other info that was modified + echo "OAS file preprocessing completed successfully" +} + +# Function to validate OpenAPI spec file +validate_oaspec() { + echo "Validating OpenAPI specification file..." + + if [[ ! -f "$OASPEC_FILE" ]]; then + echo "Error: OpenAPI spec file '$OASPEC_FILE' not found" + echo "Please ensure the oaspec.yaml file exists in the current directory" + exit 1 + fi + + # Basic validation of YAML format + if ! yq eval '.' "$OASPEC_FILE" > /dev/null 2>&1; then + echo "Error: OpenAPI spec file has YAML syntax issues" + exit 1 + fi + + # Validate that required OpenAPI fields exist + local title version + title=$(yq eval '.info.title // ""' "$OASPEC_FILE" 2>/dev/null) + version=$(yq eval '.info.version // ""' "$OASPEC_FILE" 2>/dev/null) + + if [[ -z "$title" ]]; then + echo "Warning: OpenAPI spec missing info.title" + fi + + if [[ -z "$version" ]]; then + echo "Warning: OpenAPI spec missing info.version" + fi + + echo "✓ OpenAPI specification file validated: $OASPEC_FILE" + [[ -n "$title" ]] && echo " Title: $title" + [[ -n "$version" ]] && echo " Version: $version" +} + +# Function to deploy to Proxygen using CLI +deploy_to_proxygen() { + echo "Starting deployment to Proxygen using CLI..." + + # Expand workspace variable if it exists + local service_name="$PROXYGEN_SERVICE_NAME" + if [[ "$service_name" == *""* ]]; then + # Replace with actual workspace value if available + local workspace="${WORKSPACE:-dev}" + service_name="${service_name//$workspace}" + echo "Expanded service name: $service_name" + fi + + echo "Deploying with parameters:" + echo " Instance: $PROXYGEN_INSTANCE" + echo " Service: $service_name" + echo " Spec file: $OASPEC_FILE" + + # Execute the Proxygen deployment command + echo "Running: proxygen instance deploy $PROXYGEN_INSTANCE $service_name $OASPEC_FILE" + + if proxygen instance deploy "$PROXYGEN_INSTANCE" "$service_name" "$OASPEC_FILE"; then + echo "✅ Proxygen deployment completed successfully!" + + # Get deployment status + echo "Checking deployment status..." + if proxygen instance status "$PROXYGEN_INSTANCE" "$service_name"; then + echo "✓ Deployment status retrieved successfully" + else + echo "⚠ Could not retrieve deployment status, but deployment command succeeded" + fi + + else + echo "❌ Proxygen deployment failed!" + echo "Command: proxygen instance deploy $PROXYGEN_INSTANCE $service_name $OASPEC_FILE" + exit 1 + fi +} + +# Function to list Proxygen deployments (optional verification) +list_proxygen_deployments() { + echo "Listing current Proxygen deployments for verification..." + + if proxygen instance list "$PROXYGEN_INSTANCE"; then + echo "✓ Successfully listed deployments" + else + echo "⚠ Could not list deployments, but this is non-critical" + fi +} + +# Cleanup function +cleanup() { + echo "Cleaning up temporary files..." + rm -f "$PROXYGEN_PRIVATE_KEY_FILE" "$PROXYGEN_CONFIG_FILE" + + # Clean up OAS backup file + local backup_file="${OASPEC_FILE}.backup" + if [[ -f "$backup_file" ]]; then + rm -f "$backup_file" + echo "Removed OAS backup file: $backup_file" + fi +} + +# Set up cleanup trap +trap cleanup EXIT + +# Main execution flow +main() { + echo "=== Phase 1: Dependency Check ===" + check_dependencies + + echo -e "\n=== Phase 2: Retrieving Secrets ===" + for i in "${!SECRETS[@]}"; do + retrieve_secret "${SECRETS[$i]}" "${OUTPUT_VARS[$i]}" + done + + echo -e "\n=== Phase 3: Retrieving Private Key from S3 ===" + retrieve_private_key_from_s3 + + echo -e "\n=== Phase 4: Preprocessing OpenAPI Specification ===" + preprocess_oaspec + + echo -e "\n=== Phase 5: Validating OpenAPI Specification ===" + validate_oaspec + + echo -e "\n=== Phase 7: Setting Proxygen CLI Credentials ===" + setup_proxygen_auth + + echo -e "\n=== Phase 8: Deploying to Proxygen ===" + deploy_to_proxygen + + echo -e "\n=== Phase 9: Verification ===" + list_proxygen_deployments + + echo -e "\n🎉 Proxygen deployment process completed successfully!" + echo "Proxy is now deployed and ready to handle requests." +} + +# Run main function +main "$@"