diff --git a/.gitignore b/.gitignore index 91d7c24..56276da 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea .gradle -build/* \ No newline at end of file +build/* +*/build/* \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..922169d --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,96 @@ +pipeline { + environment { + ARTIFACTORY_URL = credentials('ARTIFACTORY_URL') + } + + agent { + label 'build' + } + + tools { + gradle "gradle-7.2" + jdk 'adoptjdk13' + } + + stages { + + stage ('Build Lambdas') { + steps { + withCredentials([usernamePassword(credentialsId: 'artifactoryuserpass', usernameVariable: 'ARTIFACTORY_USER', passwordVariable: 'ARTIFACTORY_PASSWORD')]) { + sh './gradlew -b build.gradle ' + } + } + } + stage ('Test Lambdas') { + steps { + withCredentials([usernamePassword(credentialsId: 'artifactoryuserpass', usernameVariable: 'ARTIFACTORY_USER', passwordVariable: 'ARTIFACTORY_PASSWORD')]) { + sh './gradlew clean test --info -b build.gradle' + } + } + } + + stage ('Build Jars') { + steps { + withCredentials([usernamePassword(credentialsId: 'artifactoryuserpass', usernameVariable: 'ARTIFACTORY_USER', passwordVariable: 'ARTIFACTORY_PASSWORD')]) { + sh './gradlew buildZip --info -b build.gradle' + } + } + } + // stage('SonarQube Analysis') { + // steps { + // withCredentials([usernamePassword(credentialsId: 'artifactoryuserpass', usernameVariable: 'ARTIFACTORY_USER', passwordVariable: 'ARTIFACTORY_PASSWORD')]) { + // // Automatically saves the an id for the SonarQube build + // withSonarQubeEnv('CMSSonar') { + // sh './gradlew sonarqube -Dsonar.projectKey=ab2d-lambdas-project -Dsonar.host.url=https://sonarqube.cloud.cms.gov' + // } + // } + // } + //} + // stage("Quality Gate") { + // options { + // timeout(time: 10, unit: 'MINUTES') + // } + // steps { + // Parameter indicates whether to set pipeline to UNSTABLE if Quality Gate fails + // true = set pipeline to UNSTABLE, false = don't + // waitForQualityGate abortPipeline: true + // } + // }*/ + + stage ('Publish Lambdas') { + //when { + // branch 'main' + // } + steps { + withCredentials([usernamePassword(credentialsId: 'artifactoryuserpass', usernameVariable: 'ARTIFACTORY_USER', passwordVariable: 'ARTIFACTORY_PASSWORD')]) { + script { + def deployScript = ''; + + //Calls a gradle task to check if the version of each build has already been deployed + //Each build is broken up by '''. + def versionPublishedList = sh( + script: './gradlew -q lookForArtifacts', + returnStdout: true + ).trim().split("'''") + + //First value represents the build name and the second value is if the version is already deployed. + for (int i = 1; i < versionPublishedList.size(); i++) { + def artifactoryInfo = versionPublishedList[i].split(":") + if (artifactoryInfo[1] == 'false') { + echo "Deploying ${artifactoryInfo[0]}" + deployScript += "${artifactoryInfo[0]}:artifactoryPublish " + } + } + + //deployScript represents what we are publishing. Insert it into the gradle command to do the publishing. + //ex. ab2d-fhir:artifactoryPublish" + //If nothing is there, skip publishing + if(deployScript != '') { + sh "./gradlew ${deployScript} -b build.gradle" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 4899770..f20434d 100644 --- a/build.gradle +++ b/build.gradle @@ -14,8 +14,17 @@ ext { : System.getenv()['ARTIFACTORY_USER'] artifactory_password = project.hasProperty('artifactory_password') ? project.artifactory_password : System.getenv()['ARTIFACTORY_PASSWORD'] + + metricsVersion = '1.0.0' + fetcherVersion = '1.0.0' + + + sourcesRepo = 'ab2d-maven-repo' + deployerRepo = 'ab2d-main' + resolverRepo = 'ab2d-main' } + repositories { mavenCentral() maven { @@ -27,33 +36,112 @@ repositories { } } -dependencies { - compileOnly 'org.projectlombok:lombok:1.18.24' - implementation 'com.amazonaws:aws-lambda-java-core:1.2.1' - implementation 'com.amazonaws:aws-lambda-java-events:3.11.0' - implementation 'org.springframework:spring-context:5.3.20' - implementation 'com.newrelic.agent.java:newrelic-api:7.6.0' - implementation 'ca.uhn.hapi.fhir:hapi-fhir-structures-dstu3:5.7.2' - implementation 'ca.uhn.hapi.fhir:hapi-fhir-structures-r4:5.7.2' - implementation 'gov.cms.ab2d:ab2d-aggregator:1.2.1' - implementation 'gov.cms.ab2d:ab2d-bfd:1.3.1' - implementation 'gov.cms.ab2d:ab2d-events-client:1.3.1' - implementation 'gov.cms.ab2d:ab2d-fhir:1.1.1' - implementation 'gov.cms.ab2d:ab2d-filters:1.6.1' - implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.13.3' - annotationProcessor "org.projectlombok:lombok:1.18.24" -} - test { useJUnitPlatform() } -task buildZip(type: Zip) { - from compileJava - from processResources - into('lib') { - from configurations.runtimeClasspath +subprojects { + apply plugin: 'com.jfrog.artifactory' + apply plugin: 'maven-publish' + publishing { + publications { + mavenJava(MavenPublication) { + artifact file("build/distributions/${project.name}.zip") + } + } + } + + sourceCompatibility = 11 + targetCompatibility = 11 + + group 'gov.cms.ab2d' + + + artifactory { + contextUrl = project.artifactoryLoc + + publish { + repository { + repoKey = "${deployerRepo}" + username = project.artifactory_user + password = project.artifactory_password + maven = true + } + defaults { + publications('mavenJava') + publishArtifacts = true + publishBuildInfo = false + } + } + resolve { + repository { + repoKey = "${resolverRepo}" + username = project.artifactory_user + password = project.artifactory_password + maven = true + } + } + } +} + +allprojects { + + task lookForArtifacts { + doLast { + //This is the parent build where nothing gets published. Skip it. + if (project.name != 'ab2d-lambdas') + //Set path to repository that might exist if previous published on this version. + //Sets project name and if it exists in repository. Print out so jenkins can take it. + System.out.print("'''" + project.name + ":" + urlExists("${artifactoryLoc}/${deployerRepo}/gov/cms/ab2d/${project.name}/${project.version}/${project.name}-${project.version}.zip")) + } } + + repositories { + maven { + url = "${artifactoryLoc}/${sourcesRepo}" + credentials { + username = project.artifactory_user + password = project.artifactory_password + } + } + mavenCentral() + } + + +} + +def getBase64EncodedCredentials() { + def s = "$artifactory_user" + ":" + "$artifactory_password" + return s.bytes.encodeBase64().toString() +} + +//Check if url exist in the repository +def urlExists(repositoryUrl) { + try { + def connection = (HttpURLConnection) new URL(repositoryUrl).openConnection() + + connection.setRequestProperty("Authorization", "Basic " + getBase64EncodedCredentials()) + connection.setConnectTimeout(10000) + connection.setReadTimeout(10000) + connection.setRequestMethod("HEAD") + + def responseCode = connection.getResponseCode() + + if (responseCode == 401) { + throw new RuntimeException("Unauthorized MavenUser user. Please provide valid username and password.") + } + + return (200 == responseCode) + + } catch (IOException ignored) { + println(ignored) + return false + } +} + +task buildZip(type: Zip) { + dependsOn ':metrics-lambda:buildZip' + dependsOn ':eob-fetcher:buildZip' } java { @@ -62,3 +150,5 @@ java { } build.dependsOn buildZip + + diff --git a/eob-fetcher/build.gradle b/eob-fetcher/build.gradle new file mode 100644 index 0000000..db63577 --- /dev/null +++ b/eob-fetcher/build.gradle @@ -0,0 +1,69 @@ +plugins { + id 'java' +} + +configurations { + all.collect { configuration -> + configuration.exclude group: 'org.springframework.boot' + configuration.exclude group: 'com.slack.api' + configuration.exclude group: 'org.jetbrains.kotlin' + configuration.exclude group: 'org.postgresql' + configuration.exclude group: 'com.squareup.okhttp3' + configuration.exclude group: 'commons-codec' + configuration.exclude group: 'com.squareup.okio' + configuration.exclude group: 'org.springframework.cloud' + configuration.exclude group: 'org.mock-server' + configuration.exclude group: 'software.amazon.awssdk:netty-nio-client' + configuration.exclude group: 'io.netty' + configuration.exclude group: 'org.testcontainers:testcontainers' + + } + +} + +version "$fetcherVersion" + +dependencies { + compileOnly 'org.projectlombok:lombok:1.18.24' + implementation 'com.amazonaws:aws-lambda-java-core:1.2.1' + implementation 'com.amazonaws:aws-lambda-java-events:3.11.0' + implementation 'org.springframework:spring-context:5.3.23' + implementation 'com.newrelic.agent.java:newrelic-api:7.10.0' + implementation 'ca.uhn.hapi.fhir:hapi-fhir-structures-dstu3:6.1.1' + implementation 'ca.uhn.hapi.fhir:hapi-fhir-structures-r4:6.1.1' + implementation 'gov.cms.ab2d:ab2d-aggregator:1.2.1' + implementation 'gov.cms.ab2d:ab2d-bfd:1.3.1' + implementation 'gov.cms.ab2d:ab2d-events-client:1.3.1' + implementation 'gov.cms.ab2d:ab2d-fhir:1.1.1' + implementation 'gov.cms.ab2d:ab2d-filters:1.6.1' + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.13.3' + annotationProcessor "org.projectlombok:lombok:1.18.24" +} + +test { + useJUnitPlatform() +} + +task buildZip(type: Zip) { + from compileJava + from processResources + into('lib') { + from configurations.runtimeClasspath + } + archiveFileName = 'eob-fetcher.zip' +} + +task wrapper(type: Wrapper){ + gradleVersion = '7.5' +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +repositories { + mavenCentral() +} + +build.dependsOn buildZip diff --git a/src/main/java/gov/cms/ab2d/fetcher/config/FetcherConfig.java b/eob-fetcher/src/main/java/gov/cms/ab2d/fetcher/config/FetcherConfig.java similarity index 100% rename from src/main/java/gov/cms/ab2d/fetcher/config/FetcherConfig.java rename to eob-fetcher/src/main/java/gov/cms/ab2d/fetcher/config/FetcherConfig.java diff --git a/src/main/java/gov/cms/ab2d/fetcher/messages/KinesisFetcherHandler.java b/eob-fetcher/src/main/java/gov/cms/ab2d/fetcher/messages/KinesisFetcherHandler.java similarity index 100% rename from src/main/java/gov/cms/ab2d/fetcher/messages/KinesisFetcherHandler.java rename to eob-fetcher/src/main/java/gov/cms/ab2d/fetcher/messages/KinesisFetcherHandler.java diff --git a/src/main/java/gov/cms/ab2d/fetcher/model/JobFetchPayload.java b/eob-fetcher/src/main/java/gov/cms/ab2d/fetcher/model/JobFetchPayload.java similarity index 100% rename from src/main/java/gov/cms/ab2d/fetcher/model/JobFetchPayload.java rename to eob-fetcher/src/main/java/gov/cms/ab2d/fetcher/model/JobFetchPayload.java diff --git a/src/main/java/gov/cms/ab2d/fetcher/model/PatientCoverage.java b/eob-fetcher/src/main/java/gov/cms/ab2d/fetcher/model/PatientCoverage.java similarity index 100% rename from src/main/java/gov/cms/ab2d/fetcher/model/PatientCoverage.java rename to eob-fetcher/src/main/java/gov/cms/ab2d/fetcher/model/PatientCoverage.java diff --git a/src/main/java/gov/cms/ab2d/worker/processor/MockEventClient.java b/eob-fetcher/src/main/java/gov/cms/ab2d/worker/processor/MockEventClient.java similarity index 100% rename from src/main/java/gov/cms/ab2d/worker/processor/MockEventClient.java rename to eob-fetcher/src/main/java/gov/cms/ab2d/worker/processor/MockEventClient.java diff --git a/src/main/java/gov/cms/ab2d/worker/processor/PatientClaimsFilter.java b/eob-fetcher/src/main/java/gov/cms/ab2d/worker/processor/PatientClaimsFilter.java similarity index 100% rename from src/main/java/gov/cms/ab2d/worker/processor/PatientClaimsFilter.java rename to eob-fetcher/src/main/java/gov/cms/ab2d/worker/processor/PatientClaimsFilter.java diff --git a/src/main/java/gov/cms/ab2d/worker/processor/PatientClaimsProcessorImpl.java b/eob-fetcher/src/main/java/gov/cms/ab2d/worker/processor/PatientClaimsProcessorImpl.java similarity index 100% rename from src/main/java/gov/cms/ab2d/worker/processor/PatientClaimsProcessorImpl.java rename to eob-fetcher/src/main/java/gov/cms/ab2d/worker/processor/PatientClaimsProcessorImpl.java diff --git a/src/main/java/gov/cms/ab2d/worker/processor/ProgressTrackerUpdate.java b/eob-fetcher/src/main/java/gov/cms/ab2d/worker/processor/ProgressTrackerUpdate.java similarity index 100% rename from src/main/java/gov/cms/ab2d/worker/processor/ProgressTrackerUpdate.java rename to eob-fetcher/src/main/java/gov/cms/ab2d/worker/processor/ProgressTrackerUpdate.java diff --git a/src/main/resources/fetcher.properties b/eob-fetcher/src/main/resources/fetcher.properties similarity index 100% rename from src/main/resources/fetcher.properties rename to eob-fetcher/src/main/resources/fetcher.properties diff --git a/src/main/resources/logback.xml b/eob-fetcher/src/main/resources/logback.xml similarity index 100% rename from src/main/resources/logback.xml rename to eob-fetcher/src/main/resources/logback.xml diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 69a9715..ae04661 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/gradlew.bat b/gradlew.bat index 107acd3..ac1b06f 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,89 +1,89 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/logback.xml b/logback.xml new file mode 100644 index 0000000..e67af38 --- /dev/null +++ b/logback.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/metrics-lambda/README.md b/metrics-lambda/README.md new file mode 100644 index 0000000..c5eb67b --- /dev/null +++ b/metrics-lambda/README.md @@ -0,0 +1,12 @@ +A simple lambda that converts Cloudwatch events to messages the event service can accept. + +## Build + +AWS lambdas need to be zipped. The follow command will build the code and zip the resulting jar. +``` +gradle buildZip +``` + +## Deploy + +For the time being this process is manual. Log into AWS, find the metric lambda, and upload the zip from the build process. \ No newline at end of file diff --git a/metrics-lambda/build.gradle b/metrics-lambda/build.gradle new file mode 100644 index 0000000..d9ecc16 --- /dev/null +++ b/metrics-lambda/build.gradle @@ -0,0 +1,70 @@ +plugins { + id 'java' +} + +version "$metricsVersion" + +configurations { + all.collect { configuration -> + configuration.exclude group: 'org.springframework' + configuration.exclude group: 'org.springframework.boot' + configuration.exclude group: 'com.slack.api' + configuration.exclude group: 'org.jetbrains.kotlin' + configuration.exclude group: 'org.postgresql' + configuration.exclude group: 'com.squareup.okhttp3' + configuration.exclude group: 'commons-codec' + configuration.exclude group: 'com.squareup.okio' + configuration.exclude group: 'org.springframework.cloud' + configuration.exclude group: 'org.mock-server' + configuration.exclude group: 'software.amazon.awssdk:netty-nio-client' + configuration.exclude group: 'io.netty' + configuration.exclude group: 'org.testcontainers:testcontainers' + // Lombok is amazing but its jar is 2 MBs which is 20% of the total fat jar. + // Even when set as compile and annotationProcessor only gradle still adds it to the fat jar + configuration.exclude group: 'org.projectlombok' + } + +} + +repositories { + mavenCentral() + mavenLocal() +} + +dependencies { + implementation 'gov.cms.ab2d:ab2d-events-client:1.6' + implementation 'com.amazonaws:aws-lambda-java-core:1.2.1' + implementation 'com.amazonaws:aws-lambda-java-events:3.11.0' + implementation 'software.amazon.awssdk:cloudwatch:2.17.290' + implementation 'com.amazonaws:aws-java-sdk-sqs:1.12.319' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-joda:2.13.4' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.4' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.1' + testImplementation 'org.mockito:mockito-core:4.8.0' +} + +test { + useJUnitPlatform() +} + +task buildZip(type: Zip) { + from compileJava + from processResources + into('lib') { + from configurations.runtimeClasspath + } + archiveFileName = 'metrics-lambda.zip' +} + +task wrapper(type: Wrapper){ + gradleVersion = '7.5' +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +build.dependsOn buildZip + diff --git a/metrics-lambda/docker-compose.yml b/metrics-lambda/docker-compose.yml new file mode 100644 index 0000000..c06c4be --- /dev/null +++ b/metrics-lambda/docker-compose.yml @@ -0,0 +1,16 @@ +version: '3' + +services: + localstack: + image: localstack/localstack:1.1.0 + entrypoint: [ "/tmp/setup/localstack-setup.sh" ,"docker-entrypoint.sh" ] + environment: + - AWS_DEFAULT_REGION=us-east-1 + - EDGE_PORT=4566 + - SERVICES=sqs,sns,lambda + - LS_LOG=error + - DATA_DIR="/tmp/localstack" + ports: + - '4566:4566' + volumes: + - ./:/tmp/setup \ No newline at end of file diff --git a/metrics-lambda/localstack-setup.sh b/metrics-lambda/localstack-setup.sh new file mode 100755 index 0000000..8427633 --- /dev/null +++ b/metrics-lambda/localstack-setup.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +function setup(){ + until grep -q '^Ready.' /tmp/localstack_infra.log >/dev/null 2>&1 ; do + echo "Waiting for all LocalStack services to be ready" + sleep 7 + done + + awslocal lambda create-function --function-name CloudwatchEventHandler --zip-file fileb:///tmp/setup/build/distributions/ab2d-metrics.zip --handler gov.cms.ab2d.metrics.CloudwatchEventHandler --environment="Variables={com.amazonaws.sdk.disableCertChecking=true,IS_LOCALSTACK=true}" --runtime java11 --timeout 900 --role="" + awslocal sqs create-queue --queue-name ab2d-events +} + +setup & + +$@ \ No newline at end of file diff --git a/metrics-lambda/src/main/java/gov/cms/ab2d/metrics/CloudwatchEventHandler.java b/metrics-lambda/src/main/java/gov/cms/ab2d/metrics/CloudwatchEventHandler.java new file mode 100644 index 0000000..7a4ddf1 --- /dev/null +++ b/metrics-lambda/src/main/java/gov/cms/ab2d/metrics/CloudwatchEventHandler.java @@ -0,0 +1,135 @@ +package gov.cms.ab2d.metrics; + +import com.amazonaws.SDKGlobalConfiguration; +import com.amazonaws.client.builder.AwsClientBuilder; +import com.amazonaws.regions.Regions; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.LambdaLogger; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.SNSEvent; +import com.amazonaws.services.sqs.AmazonSQS; +import com.amazonaws.services.sqs.AmazonSQSAsyncClientBuilder; +import com.amazonaws.services.sqs.model.SendMessageRequest; +import com.amazonaws.util.StringUtils; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; +import com.fasterxml.jackson.datatype.joda.JodaModule; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import gov.cms.ab2d.eventclient.events.MetricsEvent; +import gov.cms.ab2d.eventclient.messages.GeneralSQSMessage; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import static gov.cms.ab2d.eventclient.events.MetricsEvent.State.END; +import static gov.cms.ab2d.eventclient.events.MetricsEvent.State.START; +import static software.amazon.awssdk.services.cloudwatch.model.StateValue.ALARM; +import static software.amazon.awssdk.services.cloudwatch.model.StateValue.OK; + + +// Catches cloudwatch alerts, extracts what we care about, then send an event to the ab2d-event sqs queue +public class CloudwatchEventHandler implements RequestHandler { + private static AmazonSQS amazonSQS; + + private final String environment = Optional.ofNullable(System.getenv("PARENT_ENV")) + .orElse("local") + "-"; + + // AWS sends an object that's not wrapped with type info. The event service expects the wrapper. + // Since there's not an easy way to enable/disable type wrapper just have 2 mappers. + private final ObjectMapper inputMapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .registerModule(new JodaModule()) + .registerModule(new JavaTimeModule()); + + private final ObjectMapper outputMapper = new ObjectMapper() + .activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY) + .registerModule(new JodaModule()) + .registerModule(new JavaTimeModule()); + + static { + amazonSQS = setup(); + } + + private static AmazonSQS setup() { + if (!StringUtils.isNullOrEmpty(System.getenv("IS_LOCALSTACK"))) { + System.setProperty(SDKGlobalConfiguration.DISABLE_CERT_CHECKING_SYSTEM_PROPERTY, "true"); + return AmazonSQSAsyncClientBuilder.standard() + .withEndpointConfiguration(new AwsClientBuilder + .EndpointConfiguration("https://localhost:4566", + Regions.US_EAST_1.getName())) + .build(); + } else { + return AmazonSQSAsyncClientBuilder.standard() + .build(); + } + } + + @Override + public String handleRequest(SNSEvent snsEvent, Context context) { + final LambdaLogger log = context.getLogger(); + snsEvent.getRecords() + .forEach(snsRecord -> sendMetric(snsRecord, log)); + return "OK"; + } + + private void sendMetric(SNSEvent.SNSRecord snsRecord, LambdaLogger log) { + final SendMessageRequest request = new SendMessageRequest(); + final String queue = environment + "events-sqs"; + final String service; + try { + final MetricAlarm alarm = inputMapper.readValue(Optional.ofNullable(snsRecord.getSNS()) + .orElse(new SNSEvent.SNS()) + .getMessage(), MetricAlarm.class); + OffsetDateTime time = alarm.getStateChangeTime() != null + ? OffsetDateTime.parse(fixDate(alarm.getStateChangeTime())) + : OffsetDateTime.now(); + request.setQueueUrl(amazonSQS.getQueueUrl(queue) + .getQueueUrl()); + service = Optional.ofNullable(alarm.getAlarmName()) + .orElseThrow(() -> new EventDataException("AlarmName is null")) + .replace(environment, ""); + request.setMessageBody(outputMapper.writeValueAsString(new GeneralSQSMessage(MetricsEvent.builder() + .service(service) + .eventDescription(alarm.getAlarmDescription()) + .timeOfEvent(time) + //This might need more work later if AWS is sending unknown states regularly + .stateType(from(alarm.getNewStateValue())) + .build()))); + } catch (Exception e) { + log.log(String.format("Handling lambda failed %s", exceptionToString(e))); + return; + } + log.log(String.format("Sending %s to %s", service, queue)); + amazonSQS.sendMessage(request); + } + + private String exceptionToString(Exception exception) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + exception.printStackTrace(pw); + return sw.toString(); + } + + + private String fixDate(String date) { + StringBuilder builder = new StringBuilder(date); + return builder.insert(builder.length() - 2, ":") + .toString(); + } + + + private MetricsEvent.State from(String stateValue) { + return Stream.of(stateValue) + .filter(value1 -> List.of(OK.toString(), ALARM.toString()) + .contains(value1)) + .map(state -> stateValue.equals(OK.toString()) ? END : START) + .findFirst() + .orElseThrow(() -> new EventDataException(String.format("AWS provided Unknown State %s", stateValue))); + } +} diff --git a/metrics-lambda/src/main/java/gov/cms/ab2d/metrics/Dimensions.java b/metrics-lambda/src/main/java/gov/cms/ab2d/metrics/Dimensions.java new file mode 100644 index 0000000..a302adb --- /dev/null +++ b/metrics-lambda/src/main/java/gov/cms/ab2d/metrics/Dimensions.java @@ -0,0 +1,31 @@ +package gov.cms.ab2d.metrics; + +public class Dimensions { + private String value; + private String name; + + public Dimensions(String value, String name) { + this.value = value; + this.name = name; + } + + public Dimensions() { + //default constructor for Jackson + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/metrics-lambda/src/main/java/gov/cms/ab2d/metrics/EventDataException.java b/metrics-lambda/src/main/java/gov/cms/ab2d/metrics/EventDataException.java new file mode 100644 index 0000000..2198e23 --- /dev/null +++ b/metrics-lambda/src/main/java/gov/cms/ab2d/metrics/EventDataException.java @@ -0,0 +1,7 @@ +package gov.cms.ab2d.metrics; + +public class EventDataException extends RuntimeException { + public EventDataException(String error) { + super(error); + } +} diff --git a/metrics-lambda/src/main/java/gov/cms/ab2d/metrics/MetricAlarm.java b/metrics-lambda/src/main/java/gov/cms/ab2d/metrics/MetricAlarm.java new file mode 100644 index 0000000..249d761 --- /dev/null +++ b/metrics-lambda/src/main/java/gov/cms/ab2d/metrics/MetricAlarm.java @@ -0,0 +1,160 @@ +package gov.cms.ab2d.metrics; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; +import java.util.Optional; + +public class MetricAlarm { + @JsonProperty("AlarmName") + private String alarmName; + @JsonProperty("AlarmDescription") + private String alarmDescription; + @JsonProperty("AWSAccountId") + private String awsAccountId; + @JsonProperty("AlarmConfigurationUpdatedTimestamp") + private String alarmConfigurationUpdatedTimestamp; + @JsonProperty("NewStateValue") + private String newStateValue; + @JsonProperty("NewStateReason") + private String newStateReason; + @JsonProperty("StateChangeTime") + private String stateChangeTime; + @JsonProperty("Region") + private String region; + @JsonProperty("AlarmArn") + private String alarmArn; + @JsonProperty("OldStateValue") + private String oldStateValue; + @JsonProperty("OKActions") + private List okActions; + @JsonProperty("AlarmActions") + private List alarmActions; + @JsonProperty("InsufficientDataActions") + private List insufficientDataActions; + @JsonProperty("Trigger") + private Trigger trigger; + + public MetricAlarm() { + //default constructor for Jackson + } + + @JsonProperty("Namespace") + public String getNamespace() { + return Optional.ofNullable(trigger) + .orElse(new Trigger()) + .getNamespace(); + } + + public String getAlarmName() { + return alarmName; + } + + public void setAlarmName(String alarmName) { + this.alarmName = alarmName; + } + + public String getAlarmDescription() { + return alarmDescription; + } + + public void setAlarmDescription(String alarmDescription) { + this.alarmDescription = alarmDescription; + } + + public String getAwsAccountId() { + return awsAccountId; + } + + public void setAwsAccountId(String awsAccountId) { + this.awsAccountId = awsAccountId; + } + + public String getAlarmConfigurationUpdatedTimestamp() { + return alarmConfigurationUpdatedTimestamp; + } + + public void setAlarmConfigurationUpdatedTimestamp(String alarmConfigurationUpdatedTimestamp) { + this.alarmConfigurationUpdatedTimestamp = alarmConfigurationUpdatedTimestamp; + } + + public String getNewStateValue() { + return newStateValue; + } + + public void setNewStateValue(String newStateValue) { + this.newStateValue = newStateValue; + } + + public String getNewStateReason() { + return newStateReason; + } + + public void setNewStateReason(String newStateReason) { + this.newStateReason = newStateReason; + } + + public String getStateChangeTime() { + return stateChangeTime; + } + + public void setStateChangeTime(String stateChangeTime) { + this.stateChangeTime = stateChangeTime; + } + + public String getRegion() { + return region; + } + + public void setRegion(String region) { + this.region = region; + } + + public String getAlarmArn() { + return alarmArn; + } + + public void setAlarmArn(String alarmArn) { + this.alarmArn = alarmArn; + } + + public String getOldStateValue() { + return oldStateValue; + } + + public void setOldStateValue(String oldStateValue) { + this.oldStateValue = oldStateValue; + } + + public List getOkActions() { + return okActions; + } + + public void setOkActions(List okActions) { + this.okActions = okActions; + } + + public List getAlarmActions() { + return alarmActions; + } + + public void setAlarmActions(List alarmActions) { + this.alarmActions = alarmActions; + } + + public List getInsufficientDataActions() { + return insufficientDataActions; + } + + public void setInsufficientDataActions(List insufficientDataActions) { + this.insufficientDataActions = insufficientDataActions; + } + + public Trigger getTrigger() { + return trigger; + } + + public void setTrigger(Trigger trigger) { + this.trigger = trigger; + } +} diff --git a/metrics-lambda/src/main/java/gov/cms/ab2d/metrics/Trigger.java b/metrics-lambda/src/main/java/gov/cms/ab2d/metrics/Trigger.java new file mode 100644 index 0000000..b2e3216 --- /dev/null +++ b/metrics-lambda/src/main/java/gov/cms/ab2d/metrics/Trigger.java @@ -0,0 +1,130 @@ +package gov.cms.ab2d.metrics; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Trigger { + @JsonProperty("Dimensions") + private Dimensions[] dimensions; + @JsonProperty("MetricName") + private String metricName; + @JsonProperty("Namespace") + private String namespace; + @JsonProperty("StatisticType") + private String statisticType; + @JsonProperty("Statistic") + private String statistic; + @JsonProperty("Unit") + private String unit; + @JsonProperty("Period") + private int period; + @JsonProperty("EvaluationPeriods") + private String evaluationPeriods; + @JsonProperty("ComparisonOperator") + private String comparisonOperator; + @JsonProperty("Threshold") + private int threshold; + @JsonProperty("TreatMissingData") + private String treatMissingData; + @JsonProperty("EvaluateLowSampleCountPercentile") + private String evaluateLowSampleCountPercentile; + + public Trigger() { + //default constructor for Jackson + } + + public Dimensions[] getDimensions() { + return dimensions; + } + + public void setDimensions(Dimensions[] dimensions) { + this.dimensions = dimensions; + } + + public String getMetricName() { + return metricName; + } + + public void setMetricName(String metricName) { + this.metricName = metricName; + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public String getStatisticType() { + return statisticType; + } + + public void setStatisticType(String statisticType) { + this.statisticType = statisticType; + } + + public String getStatistic() { + return statistic; + } + + public void setStatistic(String statistic) { + this.statistic = statistic; + } + + public String getUnit() { + return unit; + } + + public void setUnit(String unit) { + this.unit = unit; + } + + public int getPeriod() { + return period; + } + + public void setPeriod(int period) { + this.period = period; + } + + public String getEvaluationPeriods() { + return evaluationPeriods; + } + + public void setEvaluationPeriods(String evaluationPeriods) { + this.evaluationPeriods = evaluationPeriods; + } + + public String getComparisonOperator() { + return comparisonOperator; + } + + public void setComparisonOperator(String comparisonOperator) { + this.comparisonOperator = comparisonOperator; + } + + public int getThreshold() { + return threshold; + } + + public void setThreshold(int threshold) { + this.threshold = threshold; + } + + public String getTreatMissingData() { + return treatMissingData; + } + + public void setTreatMissingData(String treatMissingData) { + this.treatMissingData = treatMissingData; + } + + public String getEvaluateLowSampleCountPercentile() { + return evaluateLowSampleCountPercentile; + } + + public void setEvaluateLowSampleCountPercentile(String evaluateLowSampleCountPercentile) { + this.evaluateLowSampleCountPercentile = evaluateLowSampleCountPercentile; + } +} diff --git a/metrics-lambda/src/main/resources/log4j2.xml b/metrics-lambda/src/main/resources/log4j2.xml new file mode 100644 index 0000000..72d0175 --- /dev/null +++ b/metrics-lambda/src/main/resources/log4j2.xml @@ -0,0 +1,16 @@ + + + + + %d{yyyy-MM-dd HH:mm:ss} %X{AWSRequestId} %-5p %c{1} - %m%n + + + + + + + + + + + \ No newline at end of file diff --git a/metrics-lambda/src/test/java/gov/cms/ab2d/metrics/DtoTest.java b/metrics-lambda/src/test/java/gov/cms/ab2d/metrics/DtoTest.java new file mode 100644 index 0000000..efe57bf --- /dev/null +++ b/metrics-lambda/src/test/java/gov/cms/ab2d/metrics/DtoTest.java @@ -0,0 +1,104 @@ +package gov.cms.ab2d.metrics; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class DtoTest { + @Test + void DimensionsAllArgTest() { + Dimensions dimensions = new Dimensions("a", "a"); + assertEquals("a", dimensions.getName()); + assertEquals("a", dimensions.getValue()); + } + + @Test + void DimensionsTest() { + Dimensions dimensions = new Dimensions(); + dimensions.setName("a"); + dimensions.setValue("a"); + assertEquals("a", dimensions.getName()); + assertEquals("a", dimensions.getValue()); + } + + @Test + void MetricAlarmTest() { + Trigger trigger = new Trigger(); + MetricAlarm metricAlarm = new MetricAlarm(); + + metricAlarm.setAlarmName(""); + metricAlarm.setAlarmDescription(""); + metricAlarm.setAwsAccountId(""); + metricAlarm.setAlarmConfigurationUpdatedTimestamp(""); + metricAlarm.setNewStateValue(""); + metricAlarm.setNewStateReason(""); + metricAlarm.setStateChangeTime(""); + metricAlarm.setRegion(""); + metricAlarm.setAlarmArn(""); + metricAlarm.setOldStateValue(""); + metricAlarm.setAlarmActions(new ArrayList<>()); + metricAlarm.setOkActions(new ArrayList<>()); + metricAlarm.setInsufficientDataActions(new ArrayList<>()); + metricAlarm.setTrigger(trigger); + + assertEquals("", metricAlarm.getAlarmName()); + assertEquals("", metricAlarm.getAlarmDescription()); + assertEquals("", metricAlarm.getAwsAccountId()); + assertEquals("", metricAlarm.getAlarmConfigurationUpdatedTimestamp()); + assertEquals("", metricAlarm.getNewStateValue()); + assertEquals("", metricAlarm.getNewStateReason()); + assertEquals("", metricAlarm.getStateChangeTime()); + assertEquals("", metricAlarm.getRegion()); + assertEquals("", metricAlarm.getAlarmArn()); + assertEquals("", metricAlarm.getOldStateValue()); + assertEquals(new ArrayList<>(), metricAlarm.getAlarmActions()); + assertEquals(new ArrayList<>(), metricAlarm.getOkActions()); + assertEquals(new ArrayList<>(), metricAlarm.getInsufficientDataActions()); + assertEquals(trigger, metricAlarm.getTrigger()); + } + + @Test + void TriggerTest() { + Dimensions[] dimensions = new Dimensions[]{}; + Trigger trigger = new Trigger(); + + trigger.setDimensions(dimensions); + trigger.setMetricName("test"); + trigger.setStatistic("test"); + trigger.setStatisticType("test"); + trigger.setUnit("test"); + trigger.setPeriod(1); + trigger.setEvaluationPeriods("test"); + trigger.setComparisonOperator("test"); + trigger.setThreshold(1); + trigger.setTreatMissingData("test"); + trigger.setEvaluateLowSampleCountPercentile("test"); + trigger.setNamespace("test"); + trigger.setComparisonOperator("test"); + + assertEquals(dimensions, trigger.getDimensions()); + assertEquals("test", trigger.getMetricName()); + assertEquals("test", trigger.getStatistic()); + assertEquals("test", trigger.getStatisticType()); + assertEquals("test", trigger.getUnit()); + assertEquals(1, trigger.getPeriod()); + assertEquals("test", trigger.getEvaluationPeriods()); + assertEquals("test", trigger.getComparisonOperator()); + assertEquals(1, trigger.getThreshold()); + assertEquals("test", trigger.getTreatMissingData()); + assertEquals("test", trigger.getEvaluateLowSampleCountPercentile()); + assertEquals("test", trigger.getNamespace()); + assertEquals("test", trigger.getComparisonOperator()); + } + + @Test + void eventDataException() { + assertThrows(EventDataException.class, () -> { + throw new EventDataException("test"); + }); + } + +} diff --git a/metrics-lambda/src/test/java/gov/cms/ab2d/metrics/InvokeTest.java b/metrics-lambda/src/test/java/gov/cms/ab2d/metrics/InvokeTest.java new file mode 100644 index 0000000..2047bde --- /dev/null +++ b/metrics-lambda/src/test/java/gov/cms/ab2d/metrics/InvokeTest.java @@ -0,0 +1,129 @@ +package gov.cms.ab2d.metrics; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.SNSEvent; +import com.amazonaws.services.sqs.AmazonSQSClient; +import com.amazonaws.services.sqs.model.GetQueueUrlResult; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.joda.JodaModule; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.platform.commons.util.ReflectionUtils; +import org.mockito.Mockito; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.anyString; + +class InvokeTest { + + ObjectMapper objectMapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .registerModule(new JodaModule()) + .registerModule(new JavaTimeModule()); + + @BeforeEach + public void before() { + setEnv("IS_LOCALSTACK", ""); + } + + @Test + void invokeTestOk() throws Exception { + invoke("ab2d-dev-test","OK", "2022-09-14T19:03:51.523+0100"); + } + + @Test + void invokeTestAlarm() throws Exception { + invoke("ab2d-dev-test","ALARM", "2022-09-14T19:03:51.523+0100"); + } + + @Test + void invokeTestAlarmNameFail() throws Exception { + invoke(null,"OK", "2022-09-14T19:03:51.523+0100"); + } + + @Test + void invokeTest() throws Exception { + invoke("test","OK", "2022-09-14T19:03:51.523+0100"); + } + + @Test + void invokeTestFail() throws Exception { + invoke("test",null, null); + } + + + private void invoke(String alarmName, String state, String time) throws IllegalAccessException, JsonProcessingException { + MetricAlarm metricAlarm = new MetricAlarm(); + metricAlarm.setAlarmName(alarmName); + metricAlarm.setStateChangeTime(time); + metricAlarm.setNewStateValue(state); + Trigger trigger = new Trigger(); + trigger.setNamespace("test"); + metricAlarm.setTrigger(trigger); + SNSEvent event = new SNSEvent(); + SNSEvent.SNSRecord record = new SNSEvent.SNSRecord(); + SNSEvent.SNS sns = new SNSEvent.SNS(); + sns.setMessage(objectMapper.writeValueAsString(metricAlarm)); + record.setSns(sns); + event.setRecords(List.of(record)); + Context context = new TestContext(); + Field sqs = ReflectionUtils.findFields(CloudwatchEventHandler.class, (f) -> f.getName() + .equals("amazonSQS"), ReflectionUtils.HierarchyTraversalMode.TOP_DOWN) + .stream() + .findFirst() + .orElseThrow(() -> new RuntimeException("Field not found")); + CloudwatchEventHandler handler = new CloudwatchEventHandler(); + sqs.setAccessible(true); + AmazonSQSClient mockedSQS = Mockito.mock(AmazonSQSClient.class); + sqs.set(handler, mockedSQS); + Mockito.when(mockedSQS.getQueueUrl(anyString())) + .thenReturn(new GetQueueUrlResult()); + String result = handler.handleRequest(event, context); + assertEquals( "OK", result); + } + + @Test + void setupTest() throws NoSuchMethodException { + CloudwatchEventHandler handler = new CloudwatchEventHandler(); + Method setup = ReflectionUtils.makeAccessible(CloudwatchEventHandler.class.getDeclaredMethod("setup")); + assertDoesNotThrow(() -> { + ReflectionUtils.invokeMethod(setup, handler); + }); + } + + @Test + void setupTestLocalstack() throws NoSuchMethodException { + setEnv("IS_LOCALSTACK", "true"); + assertEquals( "true", System.getenv("IS_LOCALSTACK")); + CloudwatchEventHandler handler = new CloudwatchEventHandler(); + Method setup = ReflectionUtils.makeAccessible(CloudwatchEventHandler.class.getDeclaredMethod("setup")); + assertDoesNotThrow(() -> { + ReflectionUtils.invokeMethod(setup, handler); + }); + } + + public static void setEnv(String key, String value) { + try { + Map env = System.getenv(); + Class cl = env.getClass(); + Field field = cl.getDeclaredField("m"); + field.setAccessible(true); + Map writableEnv = (Map) field.get(env); + writableEnv.put(key, value); + } catch (Exception e) { + throw new IllegalStateException("Failed to set environment variable", e); + } + } + + + +} diff --git a/metrics-lambda/src/test/java/gov/cms/ab2d/metrics/TestContext.java b/metrics-lambda/src/test/java/gov/cms/ab2d/metrics/TestContext.java new file mode 100644 index 0000000..721b862 --- /dev/null +++ b/metrics-lambda/src/test/java/gov/cms/ab2d/metrics/TestContext.java @@ -0,0 +1,68 @@ +package gov.cms.ab2d.metrics; + +import com.amazonaws.services.lambda.runtime.ClientContext; +import com.amazonaws.services.lambda.runtime.CognitoIdentity; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.LambdaLogger; +import org.mockito.Mockito; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; + + +public class TestContext implements Context { + + public TestContext() { + } + + public String getAwsRequestId() { + return "495b12a8-xmpl-4eca-8168-160484189f99"; + } + + public String getLogGroupName() { + return "/aws/lambda/my-function"; + } + + public String getLogStreamName() { + return "2020/02/26/[$LATEST]704f8dxmpla04097b9134246b8438f1a"; + } + + public String getFunctionName() { + return "my-function"; + } + + public String getFunctionVersion() { + return "$LATEST"; + } + + public String getInvokedFunctionArn() { + return "arn:aws:lambda:us-east-2:123456789012:function:my-function"; + } + + public CognitoIdentity getIdentity() { + return null; + } + + public ClientContext getClientContext() { + return null; + } + + public int getRemainingTimeInMillis() { + return 300000; + } + + public int getMemoryLimitInMB() { + return 512; + } + + public LambdaLogger getLogger() { + LambdaLogger logger = Mockito.mock(LambdaLogger.class); + doAnswer(call -> { + System.out.println((String) call.getArgument(0)); + return null; + }).when(logger) + .log(anyString()); + return logger; + } + +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..5f2de51 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'ab2d-lambdas' +include 'eob-fetcher', 'metrics-lambda' \ No newline at end of file