diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml new file mode 100644 index 0000000..c796205 --- /dev/null +++ b/.github/workflows/integration.yaml @@ -0,0 +1,107 @@ +name: Integration Tests + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + integration: + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Checkout agari-helm + uses: actions/checkout@v4 + with: + repository: jbothma/agari-helm + ref: integration-tested + path: agari-helm + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Start services + env: + AGARI_HELM_PATH: ${{ github.workspace }}/agari-helm + run: docker compose up -d + + - name: Wait for services to be healthy + timeout-minutes: 5 + run: | + echo "Waiting for all services to become healthy (excluding zookeeper)..." + END=$((SECONDS+120)) + + while [ $SECONDS -lt $END ]; do + # Get list of unhealthy services (excluding NAME header, already healthy services, and zookeeper) + UNHEALTHY=$(docker compose ps --status running | egrep -v '(NAME|healthy|zookeeper)' || true) + + if [ -z "$UNHEALTHY" ]; then + echo "All services are healthy!" + docker compose ps + exit 0 + fi + + echo "Still waiting for services to become healthy..." + echo "$UNHEALTHY" + sleep 5 + done + + echo "Timeout: Services did not become healthy within 2 minutes" + docker compose ps + exit 1 + + - name: Show service status + if: success() + run: docker compose ps + + - name: Test authentication + run: | + echo "Testing Keycloak authentication as system-admin..." + RESPONSE=$(curl --silent --request POST \ + --url http://localhost:8080/realms/agari/protocol/openid-connect/token \ + --header 'content-type: application/x-www-form-urlencoded' \ + --data username=system.admin@agari.tech \ + --data password=pass123 \ + --data grant_type=password \ + --data client_id=dms \ + --data client_secret=VDyLEjGR3xDQvoQlrHq5AB6OwbW0Refc) + + SYSADMIN_ACCESS_TOKEN=$(echo "$RESPONSE" | jq -r '.access_token') + + if [ "$SYSADMIN_ACCESS_TOKEN" = "null" ] || [ -z "$SYSADMIN_ACCESS_TOKEN" ]; then + echo "Failed to get access token" + echo "Response: $RESPONSE" + exit 1 + fi + + echo "Successfully obtained access token for system-admin" + echo "SYSADMIN_ACCESS_TOKEN=${SYSADMIN_ACCESS_TOKEN}" >> $GITHUB_ENV + + - name: Test Folio whoami endpoint + run: | + echo "Testing Folio /info/whoami endpoint..." + RESPONSE=$(curl --silent --request GET \ + --url http://localhost:8000/info/whoami \ + --header "authorization: Bearer ${SYSADMIN_ACCESS_TOKEN}") + + echo "Response: $RESPONSE" + + if echo "$RESPONSE" | grep -q "system.admin@agari.tech"; then + echo "✓ Successfully verified user identity: system.admin@agari.tech" + exit 0 + else + echo "✗ Failed to find system.admin@agari.tech in response" + exit 1 + fi + + - name: Print docker compose logs + if: always() + run: docker compose logs + + - name: Clean up + if: always() + run: docker compose down -v diff --git a/Dockerfile b/Dockerfile index 7af929f..3966fd6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,7 @@ WORKDIR /app # Install system dependencies RUN apt-get update && apt-get install -y \ gcc \ + curl \ && rm -rf /var/lib/apt/lists/* # Copy requirements and install Python dependencies diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..00e0cd1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,302 @@ +services: + # Keycloak Database + keycloak-db: + image: postgres:14 + container_name: keycloak-db + environment: + POSTGRES_DB: keycloak_db + POSTGRES_USER: admin + POSTGRES_PASSWORD: keycloak-db-pass-123 + ports: + - "5433:5432" + volumes: + - keycloak-db-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U admin -d keycloak_db"] + interval: 10s + timeout: 5s + retries: 5 + + # Keycloak + keycloak: + image: quay.io/phasetwo/phasetwo-keycloak:26.2.5 + container_name: keycloak + environment: + KC_DB: postgres + KC_DB_URL: jdbc:postgresql://keycloak-db:5432/keycloak_db + KC_DB_USERNAME: admin + KC_DB_PASSWORD: keycloak-db-pass-123 + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: admin123 + KC_HOSTNAME_STRICT: "false" + KC_HOSTNAME_STRICT_HTTPS: "false" + KC_PROXY: edge + KC_FEATURES: preview + KC_CORS_ORIGINS: "*" + JAVA_OPTS_APPEND: "-Djava.awt.headless=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.net.preferIPv4Stack=true -server -Xms512m -Xmx768m" + command: + - start-dev + # "If a realm already exists in the server, the import operation is skipped." + - --import-realm + volumes: + - ${AGARI_HELM_PATH}/helm/keycloak/configs/agari-realm-rbac.json:/opt/keycloak/data/import/agari-realm-rbac.json:ro + ports: + - "8080:8080" + depends_on: + keycloak-db: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/8080;echo -e 'GET /health/ready HTTP/1.1\r\nhost: 127.0.0.1:8080\r\nConnection: close\r\n\r\n' >&3;if [ $? -eq 0 ]; then exit 0;else exit 1;fi;exec 3<&-;exec 3>&-"] + interval: 30s + timeout: 15s + retries: 12 + start_period: 120s + + # Song Database + song-db: + image: postgres:14 + container_name: song-db + environment: + POSTGRES_DB: song_db + POSTGRES_USER: admin + POSTGRES_PASSWORD: song-db-pass-123 + ports: + - "5434:5432" + volumes: + - song-db-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U admin -d song_db"] + interval: 10s + timeout: 5s + retries: 5 + + # Elasticsearch + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:7.17.15 + container_name: elasticsearch + environment: + - discovery.type=single-node + - cluster.name=workflow.elasticsearch + - xpack.security.enabled=false + - "ES_JAVA_OPTS=-Xms300m -Xmx300m" + - network.host=0.0.0.0 + ports: + - "9200:9200" + - "9300:9300" + volumes: + - elasticsearch-data:/usr/share/elasticsearch/data + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:9200/_cluster/health || exit 1"] + interval: 15s + timeout: 10s + retries: 6 + start_period: 60s + + # Zookeeper (required for Kafka) + zookeeper: + image: quay.io/strimzi/kafka:0.43.0-kafka-3.8.0 + container_name: zookeeper + command: [ + "sh", "-c", + "bin/zookeeper-server-start.sh config/zookeeper.properties" + ] + ports: + - "2181:2181" + environment: + LOG_DIR: /tmp/logs + volumes: + - zookeeper-data:/var/lib/zookeeper/data + - zookeeper-logs:/var/lib/zookeeper/log + + # Kafka + kafka: + image: quay.io/strimzi/kafka:0.43.0-kafka-3.8.0 + container_name: kafka + command: [ + "sh", "-c", + "bin/kafka-server-start.sh config/server.properties --override listeners=$${KAFKA_LISTENERS} --override advertised.listeners=$${KAFKA_ADVERTISED_LISTENERS} --override zookeeper.connect=$${KAFKA_ZOOKEEPER_CONNECT} --override listener.security.protocol.map=$${KAFKA_LISTENER_SECURITY_PROTOCOL_MAP} --override inter.broker.listener.name=$${KAFKA_INTER_BROKER_LISTENER_NAME} --override offsets.topic.replication.factor=$${KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR} --override transaction.state.log.replication.factor=$${KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR} --override transaction.state.log.min.isr=$${KAFKA_TRANSACTION_STATE_LOG_MIN_ISR} --override default.replication.factor=$${KAFKA_DEFAULT_REPLICATION_FACTOR} --override min.insync.replicas=$${KAFKA_MIN_INSYNC_REPLICAS}" + ] + ports: + - "9092:9092" + environment: + LOG_DIR: /tmp/logs + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_LISTENERS: PLAINTEXT://:9092 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_DEFAULT_REPLICATION_FACTOR: 1 + KAFKA_MIN_INSYNC_REPLICAS: 1 + depends_on: + - zookeeper + volumes: + - kafka-data:/var/lib/kafka/data + healthcheck: + test: ["CMD-SHELL", "bin/kafka-broker-api-versions.sh --bootstrap-server localhost:9092 || exit 1"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 30s + + # Song + song: + image: ghcr.io/overture-stack/song-server:edge + container_name: song + environment: + SPRING_PROFILES_ACTIVE: "prod,secure,kafka,s3,score-client-cred" + SPRING_FLYWAY_ENABLED: "true" + SPRING_DATASOURCE_URL: "jdbc:postgresql://song-db:5432/song_db?stringtype=unspecified" + SPRING_DATASOURCE_USERNAME: "admin" + SPRING_DATASOURCE_PASSWORD: "song-db-pass-123" + AUTH_SERVER_PROVIDER: "keycloak" + AUTH_SERVER_KEYCLOAK_HOST: "http://keycloak:8080" + AUTH_SERVER_KEYCLOAK_REALM: "agari" + AUTH_SERVER_CLIENTID: "dms" + AUTH_SERVER_CLIENTSECRET: "dms-secret" + AUTH_SERVER_TOKENNAME: "apiKey" + AUTH_SERVER_SCOPE_STUDY_PREFIX: "STUDY." + AUTH_SERVER_SCOPE_STUDY_SUFFIX: ".WRITE" + AUTH_SERVER_SCOPE_SYSTEM: "song.WRITE" + SCORE_URL: "http://score:8087" + SCORE_CLIENTCREDENTIALS_ID: "dms" + SCORE_CLIENTCREDENTIALS_SECRET: "dms-secret" + SCORE_CLIENTCREDENTIALS_TOKENURL: "http://keycloak:8080/realms/agari/protocol/openid-connect/token" + SCORE_CLIENTCREDENTIALS_SYSTEMSCOPE: "openid" + SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK-SET-URI: "http://keycloak:8080/realms/agari/protocol/openid-connect/certs" + SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER-URI: "http://keycloak:8080/realms/agari" + KAFKA_BOOTSTRAPSERVERS: "kafka:9092" + KAFKA_TEMPLATE_DEFAULTTOPIC: "song-analysis" + ID_USELOCAL: "true" + SCHEMAS_ENFORCELATEST: "true" + CLIENT_STUDY_MAXUPLOADFILESIZE: "10737418240" + CLIENT_STUDY_MAXNUMBEROFFILESPERUPLOAD: "20" + SWAGGER_ALTERNATEURL: "/swagger-api" + ports: + - "8081:8080" + depends_on: + song-db: + condition: service_healthy + keycloak: + condition: service_healthy + kafka: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8080/isAlive || exit 1"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 60s + + # Folio Database + folio-db: + image: postgres:14 + container_name: folio-db + environment: + POSTGRES_DB: folio + POSTGRES_USER: admin + POSTGRES_PASSWORD: folio-db-pass-123 + ports: + - "5435:5432" + volumes: + - folio-db-data:/var/lib/postgresql/data + - ${AGARI_HELM_PATH}/helm/folio-db/sql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U admin -d folio"] + interval: 10s + timeout: 5s + retries: 5 + + # Folio + folio: + build: + context: . + dockerfile: Dockerfile + container_name: folio + environment: + KEYCLOAK_HOST: "http://keycloak:8080" + KEYCLOAK_REALM: "agari" + KEYCLOAK_ISSUER: "http://keycloak:8080/realms/agari" + KEYCLOAK_CLIENT_ID: "dms" + KEYCLOAK_CLIENT_SECRET: "VDyLEjGR3xDQvoQlrHq5AB6OwbW0Refc" + DB_HOST: "folio-db" + DB_PORT: "5432" + DB_NAME: "folio" + DB_USER: "admin" + DB_PASSWORD: "folio-db-pass-123" + SCORE_URL: "http://score:8087" + SONG_URL: "http://song:8080" + ports: + - "8000:8000" + depends_on: + folio-db: + condition: service_healthy + keycloak: + condition: service_healthy + song: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8000/info/health/db || exit 1"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 30s + + # Maestro + maestro: + image: ghcr.io/overture-stack/maestro:4.3.0 + container_name: maestro + environment: + MAESTRO_FAILURELOG_ENABLED: "true" + MAESTRO_FAILURELOG_DIR: "app/logs/maestro" + MAESTRO_LOGGING_LEVEL_ROOT: "INFO" + MAESTRO_NOTIFICATIONS_SLACK_ENABLED: "false" + MAESTRO_REPOSITORIES_0_CODE: "song.overture" + MAESTRO_REPOSITORIES_0_URL: "http://song:8080" + MAESTRO_REPOSITORIES_0_NAME: "Overture" + MAESTRO_REPOSITORIES_0_ORGANIZATION: "Overture" + MAESTRO_REPOSITORIES_0_COUNTRY: "CA" + MAESTRO_REPOSITORIES_0_AUTH_TYPE: "oauth2" + MAESTRO_REPOSITORIES_0_AUTH_CLIENTID: "dms" + MAESTRO_REPOSITORIES_0_AUTH_CLIENTSECRET: "dms-secret" + MAESTRO_REPOSITORIES_0_AUTH_TOKENURL: "http://keycloak:8080/realms/agari/protocol/openid-connect/token" + MAESTRO_REPOSITORIES_0_AUTH_SCOPE: "song.READ" + MAESTRO_ELASTICSEARCH_CLUSTER_NODES: "http://elasticsearch:9200" + MAESTRO_ELASTICSEARCH_CLIENT_BASICAUTH_ENABLED: "false" + MAESTRO_ELASTICSEARCH_CLIENT_TRUSTSELFSIGNCERT: "true" + MAESTRO_ELASTICSEARCH_INDEXES_ANALYSISCENTRIC_ENABLED: "false" + MAESTRO_ELASTICSEARCH_INDEXES_FILECENTRIC_ENABLED: "true" + MAESTRO_ELASTICSEARCH_INDEXES_FILECENTRIC_NAME: "agari-index" + MAESTRO_ELASTICSEARCH_INDEXES_FILECENTRIC_ALIAS: "file_centric" + SPRING_MVC_ASYNC_REQUESTTIMEOUT: "-1" + SPRING_CLOUD_STREAM_KAFKA_BINDER_BROKERS: "kafka:9092" + SPRING_CLOUD_STREAM_BINDINGS_SONGINPUT_DESTINATION: "song-analysis" + SPRINGDOC_SWAGGERUI_PATH: "/swagger-api" + MANAGEMENT_HEALTH_ELASTICSEARCH_ENABLED: "false" + ports: + - "11235:11235" + depends_on: + elasticsearch: + condition: service_healthy + kafka: + condition: service_healthy + song: + condition: service_healthy + healthcheck: + test: ["CMD", "jrunscript", "-e", "var url = new java.net.URL('http://localhost:11235/'); url.openConnection().getResponseCode() == 200 ? java.lang.System.exit(0) : java.lang.System.exit(1);"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 90s + +volumes: + keycloak-db-data: + song-db-data: + folio-db-data: + elasticsearch-data: + zookeeper-data: + zookeeper-logs: + kafka-data: