diff --git a/build.gradle b/build.gradle index 20ffd5b..11f13de 100644 --- a/build.gradle +++ b/build.gradle @@ -38,6 +38,14 @@ dependencies { implementation 'org.json:json:20240303' + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + + implementation 'org.bouncycastle:bcprov-jdk18on:1.79' + + implementation 'org.kohsuke:github-api:1.326' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' diff --git a/infrastructure/lib/constructs/eventDataLakeSns.ts b/infrastructure/lib/constructs/eventDataLakeSns.ts new file mode 100644 index 0000000..e506ea1 --- /dev/null +++ b/infrastructure/lib/constructs/eventDataLakeSns.ts @@ -0,0 +1,62 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +import {Alarm, ComparisonOperator, MathExpression, Metric, TreatMissingData} from "aws-cdk-lib/aws-cloudwatch"; +import { Construct } from "constructs"; +import { SnsMonitors, SnsMonitorsProps } from "./snsMonitor"; +import {Duration} from "aws-cdk-lib"; + +interface eventDataLakeSnsProps extends SnsMonitorsProps { + readonly eventDataLakeSnsAlarms: Array<{ alertName: string }>; +} + +export class EventDataLakeSns extends SnsMonitors { + private readonly eventDataLakeSnsAlarms: Array<{ alertName: string }>; + constructor(scope: Construct, id: string, props: eventDataLakeSnsProps) { + super(scope, id, props); + this.eventDataLakeSnsAlarms = props.eventDataLakeSnsAlarms; + this.eventDataLakeSnsAlarms.forEach(({ alertName }) => { + const alarm = this.eventDataLakeAppFailed(alertName); + this.map[alarm[1]] = alarm[0]; + }); + this.createTopic(); + } + + private eventDataLakeAppFailed(alertName: string): [Alarm, string] { + const metricPeriod = Duration.minutes(10); + + const eventDataLakeAppFailedMetric = new Metric({ + namespace: this.alarmNameSpace, + metricName: "LabelCanaryEvent", + statistic: "Sum", + period: metricPeriod, + }); + + const filledEventDataLakeAppFailedMetric = new MathExpression({ + expression: "FILL(metric, 0)", + usingMetrics: { + metric: eventDataLakeAppFailedMetric, + }, + period: metricPeriod, + }); + + const alarmObject = new Alarm(this, `error_alarm_${alertName}`, { + metric: filledEventDataLakeAppFailedMetric, + threshold: 1, + evaluationPeriods: 1, + comparisonOperator: ComparisonOperator.LESS_THAN_THRESHOLD, + datapointsToAlarm: 1, + treatMissingData: TreatMissingData.BREACHING, + alarmDescription: "Detect GitHub Event Data Lake App failure", + alarmName: alertName, + }); + return [alarmObject, alertName]; + } +} + diff --git a/infrastructure/lib/enums/project.ts b/infrastructure/lib/enums/project.ts index 468f1aa..39b5909 100644 --- a/infrastructure/lib/enums/project.ts +++ b/infrastructure/lib/enums/project.ts @@ -18,6 +18,8 @@ enum Project { RESTRICTED_PREFIX = '', LAMBDA_PACKAGE = 'opensearch-metrics-1.0.zip', EC2_AMI_SSM = '', - SNS_ALERT_EMAIL = 'insert@test.mail' + SNS_ALERT_EMAIL = 'insert@test.mail', + EVENT_CANARY_OWNER_TARGET = '', + EVENT_CANARY_REPO_TARGET = '', } export default Project; diff --git a/infrastructure/lib/infrastructure-stack.ts b/infrastructure/lib/infrastructure-stack.ts index 185cb2a..36ab79e 100644 --- a/infrastructure/lib/infrastructure-stack.ts +++ b/infrastructure/lib/infrastructure-stack.ts @@ -25,6 +25,7 @@ import { OpenSearchWAF } from "./stacks/waf"; import { GitHubWorkflowMonitorAlarms } from "./stacks/gitHubWorkflowMonitorAlarms"; import { OpenSearchS3EventIndexWorkflowStack } from "./stacks/s3EventIndexWorkflow"; import { OpenSearchMaintainerInactivityWorkflowStack } from "./stacks/maintainerInactivityWorkflow"; +import {OpenSearchEventCanaryWorkflowStack} from "./stacks/eventCanaryWorkflow"; export class InfrastructureStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { @@ -113,6 +114,16 @@ export class InfrastructureStack extends Stack { secretName: 'metrics-creds' }); + // Create OpenSearch Event Canary Lambda setup + const openSearchEventCanaryWorkflowStack = new OpenSearchEventCanaryWorkflowStack(app, 'OpenSearchEventCanary-Workflow', { + vpcStack: vpcStack, + lambdaPackage: Project.LAMBDA_PACKAGE, + gitHubOwnerTarget: Project.EVENT_CANARY_OWNER_TARGET, + gitHubRepoTarget: Project.EVENT_CANARY_REPO_TARGET, + gitHubAppSecret: openSearchMetricsSecretsStack.secret, + }) + openSearchEventCanaryWorkflowStack.node.addDependency(vpcStack); + // Create Monitoring Dashboard const openSearchMetricsMonitoringStack = new OpenSearchMetricsMonitoringStack(app, "OpenSearchMetrics-Monitoring", { diff --git a/infrastructure/lib/stacks/eventCanaryWorkflow.ts b/infrastructure/lib/stacks/eventCanaryWorkflow.ts new file mode 100644 index 0000000..4412527 --- /dev/null +++ b/infrastructure/lib/stacks/eventCanaryWorkflow.ts @@ -0,0 +1,103 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +import { Duration, Stack, StackProps } from "aws-cdk-lib"; +import { Rule, RuleTargetInput, Schedule } from "aws-cdk-lib/aws-events"; +import { SfnStateMachine } from "aws-cdk-lib/aws-events-targets"; +import { Bucket } from "aws-cdk-lib/aws-s3"; +import { JsonPath, StateMachine, TaskInput } from "aws-cdk-lib/aws-stepfunctions"; +import { LambdaInvoke } from "aws-cdk-lib/aws-stepfunctions-tasks"; +import { Construct } from 'constructs'; +import { OpenSearchLambda } from "../constructs/lambda"; +import { OpenSearchDomainStack } from "./opensearch"; +import { VpcStack } from "./vpc"; +import {Effect, ManagedPolicy, PolicyDocument, PolicyStatement, Role, ServicePrincipal} from "aws-cdk-lib/aws-iam"; +import {Secret} from "aws-cdk-lib/aws-secretsmanager"; + +export interface OpenSearchEventCanaryWorkflowStackProps extends StackProps { + readonly vpcStack: VpcStack; + readonly lambdaPackage: string; + readonly gitHubOwnerTarget: string; + readonly gitHubRepoTarget: string; + readonly gitHubAppSecret: Secret; +} + +export interface WorkflowComponent { + opensearchEventCanaryWorkflowStateMachineName: string +} + +export class OpenSearchEventCanaryWorkflowStack extends Stack { + public readonly workflowComponent: WorkflowComponent; + constructor(scope: Construct, id: string, props: OpenSearchEventCanaryWorkflowStackProps) { + super(scope, id, props); + + const eventCanaryTask = this.createEventCanaryTask(this, + props.vpcStack, + props.lambdaPackage, + props.gitHubOwnerTarget, + props.gitHubRepoTarget, + props.gitHubAppSecret, + ); + + const opensearchEventCanaryWorkflow = new StateMachine(this, 'OpenSearchEventCanaryWorkflow', { + definition: eventCanaryTask, + timeout: Duration.minutes(15), + stateMachineName: 'OpenSearchEventCanaryWorkflow' + }) + + new Rule(this, 'OpenSearchEventCanaryWorkflow-Every-10mins', { + schedule: Schedule.expression('cron(0/10 * * * ? *)'), + targets: [new SfnStateMachine(opensearchEventCanaryWorkflow)], + }); + + this.workflowComponent = { + opensearchEventCanaryWorkflowStateMachineName: opensearchEventCanaryWorkflow.stateMachineName + } + } + + private createEventCanaryTask(scope: Construct, vpcStack: VpcStack, lambdaPackage: string, gitHubOwnerTarget: string, gitHubRepoTarget: string, gitHubAppSecret: Secret) { + const eventCanaryLambdaRole = new Role(this, 'OpenSearchEventCanaryLambdaRole', { + assumedBy: new ServicePrincipal('lambda.amazonaws.com'), + description: "OpenSearch Metrics Event Canary Lambda Execution Role", + roleName: "OpenSearchEventCanaryLambdaRole", + managedPolicies: [ + ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'), + ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaVPCAccessExecutionRole'), + ] + }); + + eventCanaryLambdaRole.addToPolicy( + new PolicyStatement({ + effect: Effect.ALLOW, + actions: ["secretsmanager:GetSecretValue"], + resources: [`${gitHubAppSecret.secretFullArn}`], + }), + ); + + const eventCanaryLambda = new OpenSearchLambda(this, "OpenSearchMetricsEventCanaryLambdaFunction", { + lambdaNameBase: "OpenSearchMetricsEventCanary", + handler: "org.opensearchmetrics.lambda.EventCanaryLambda", + lambdaZipPath: `../../../build/distributions/${lambdaPackage}`, + vpc: vpcStack.vpc, + securityGroup: vpcStack.securityGroup, + role: eventCanaryLambdaRole, + environment: { + GITHUB_OWNER_TARGET: gitHubOwnerTarget, + GITHUB_REPO_TARGET: gitHubRepoTarget, + API_CREDENTIALS_SECRETS: gitHubAppSecret.secretName, + SECRETS_MANAGER_REGION: gitHubAppSecret.env.region, + } + }).lambda; + return new LambdaInvoke(scope, 'Event Canary Lambda', { + lambdaFunction: eventCanaryLambda, + resultPath: JsonPath.DISCARD, + timeout: Duration.minutes(15) + }).addRetry(); + } +} diff --git a/infrastructure/lib/stacks/monitoringDashboard.ts b/infrastructure/lib/stacks/monitoringDashboard.ts index e20f29d..33e4c34 100644 --- a/infrastructure/lib/stacks/monitoringDashboard.ts +++ b/infrastructure/lib/stacks/monitoringDashboard.ts @@ -18,6 +18,7 @@ import { OpenSearchLambda } from "../constructs/lambda"; import { StepFunctionSns } from "../constructs/stepFunctionSns"; import Project from "../enums/project"; import { VpcStack } from "./vpc"; +import {EventDataLakeSns} from "../constructs/eventDataLakeSns"; interface OpenSearchMetricsMonitoringStackProps extends StackProps { @@ -56,12 +57,13 @@ export class OpenSearchMetricsMonitoringStack extends Stack { lambdaZipPath: `../../../build/distributions/${props.lambdaPackage}`, role: slackLambdaRole, environment: { - SLACK_CREDENTIALS_SECRETS: props.secrets.secretName, + API_CREDENTIALS_SECRETS: props.secrets.secretName, SECRETS_MANAGER_REGION: props.secrets.env.region } }); this.snsMonitorStepFunctionExecutionsFailed(); this.snsMonitorCanaryFailed('metrics_heartbeat', `https://${Project.METRICS_HOSTED_ZONE}`, props.vpcStack); + this.snsMonitorEventDataLakeAppFailed(); } /** @@ -117,5 +119,23 @@ export class OpenSearchMetricsMonitoringStack extends Stack { slackLambda: this.slackLambda }); } + + /** + * Create SNS alarms for if the GitHub Event Data Lake App goes down. + */ + private snsMonitorEventDataLakeAppFailed(): void { + const eventDataLakeSnsAlarms = [ + { alertName: 'Event_data_lake_app_failed'}, + ]; + + new EventDataLakeSns(this, "SnsMonitors-EventDataLakeAppFailed", { + region: this.props.region, + accountId: this.props.account, + eventDataLakeSnsAlarms: eventDataLakeSnsAlarms, + alarmNameSpace: "GitHubCanary", + snsTopicName: "EventDataLakeAppFailed", + slackLambda: this.slackLambda + }); + } } diff --git a/infrastructure/test/event-canary-workflow-stack.test.ts b/infrastructure/test/event-canary-workflow-stack.test.ts new file mode 100644 index 0000000..f33324f --- /dev/null +++ b/infrastructure/test/event-canary-workflow-stack.test.ts @@ -0,0 +1,90 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +import { App } from "aws-cdk-lib"; +import { Template } from "aws-cdk-lib/assertions"; +import Project from "../lib/enums/project"; +import { VpcStack } from "../lib/stacks/vpc"; +import {OpenSearchEventCanaryWorkflowStack} from "../lib/stacks/eventCanaryWorkflow"; +import {OpenSearchMetricsSecretsStack} from "../lib/stacks/secrets"; + +test('Event Canary Workflow Stack Test', () => { + const app = new App(); + const vpcStack = new VpcStack(app, 'Test-OpenSearchHealth-VPC', {}); + + // Create Secret Manager for the metrics project + const openSearchMetricsSecretsStack = new OpenSearchMetricsSecretsStack(app, "OpenSearchMetrics-Secrets", { + secretName: 'metrics-creds' + }); + + const openSearchEventCanaryWorkflowStack = new OpenSearchEventCanaryWorkflowStack(app, 'OpenSearchEventCanary-Workflow', { + vpcStack: vpcStack, + lambdaPackage: Project.LAMBDA_PACKAGE, + gitHubOwnerTarget: Project.EVENT_CANARY_OWNER_TARGET, + gitHubRepoTarget: Project.EVENT_CANARY_REPO_TARGET, + gitHubAppSecret: openSearchMetricsSecretsStack.secret, + }) + + openSearchEventCanaryWorkflowStack.node.addDependency(vpcStack); + const template = Template.fromStack(openSearchEventCanaryWorkflowStack); + template.resourceCountIs('AWS::IAM::Role', 3); + template.resourceCountIs('AWS::Lambda::Function', 1); + template.hasResourceProperties('AWS::Lambda::Function', { + "FunctionName": "OpenSearchMetricsEventCanaryLambda", + "Handler": "org.opensearchmetrics.lambda.EventCanaryLambda" + }); + template.resourceCountIs('AWS::StepFunctions::StateMachine', 1); + template.hasResourceProperties('AWS::StepFunctions::StateMachine', { + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"Event Canary Lambda\",\"States\":{\"Event Canary Lambda\":{\"End\":true,\"Retry\":[{\"ErrorEquals\":[\"Lambda.ClientExecutionTimeoutException\",\"Lambda.ServiceException\",\"Lambda.AWSLambdaException\",\"Lambda.SdkClientException\"],\"IntervalSeconds\":2,\"MaxAttempts\":6,\"BackoffRate\":2},{\"ErrorEquals\":[\"States.ALL\"]}],\"Type\":\"Task\",\"TimeoutSeconds\":900,\"ResultPath\":null,\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::lambda:invoke\",\"Parameters\":{\"FunctionName\":\"", + { + "Fn::GetAtt": [ + "OpenSearchMetricsEventCanaryLambda358BAA07", + "Arn" + ] + }, + "\",\"Payload.$\":\"$\"}}},\"TimeoutSeconds\":900}" + ] + ] + }, + "RoleArn": { + "Fn::GetAtt": [ + "OpenSearchEventCanaryWorkflowRoleDC920D0E", + "Arn" + ] + }, + "StateMachineName": "OpenSearchEventCanaryWorkflow" + }); + template.resourceCountIs('AWS::Events::Rule', 1); + template.hasResourceProperties('AWS::Events::Rule', { + "ScheduleExpression": "cron(0/10 * * * ? *)", + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Ref": "OpenSearchEventCanaryWorkflowEB1017B7" + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "OpenSearchEventCanaryWorkflowEventsRoleA5644829", + "Arn" + ] + } + } + ] + }); +}); diff --git a/infrastructure/test/monitoring-stack.test.ts b/infrastructure/test/monitoring-stack.test.ts index 822f868..201f7f0 100644 --- a/infrastructure/test/monitoring-stack.test.ts +++ b/infrastructure/test/monitoring-stack.test.ts @@ -72,8 +72,8 @@ test('Monitoring Stack Test', () => { const template = Template.fromStack(openSearchMetricsMonitoringStack); template.resourceCountIs('AWS::IAM::Role', 2); template.resourceCountIs('AWS::IAM::Policy', 1); - template.resourceCountIs('AWS::CloudWatch::Alarm', 4); - template.resourceCountIs('AWS::SNS::Topic', 2); + template.resourceCountIs('AWS::CloudWatch::Alarm', 5); + template.resourceCountIs('AWS::SNS::Topic', 3); template.resourceCountIs('AWS::Synthetics::Canary', 1); template.hasResourceProperties('AWS::IAM::Role', { "AssumeRolePolicyDocument": { @@ -330,4 +330,37 @@ test('Monitoring Stack Test', () => { "Threshold": 0, "TreatMissingData": "notBreaching" }); + + template.hasResourceProperties('AWS::CloudWatch::Alarm', { + "AlarmActions": [ + { + "Ref": "SnsMonitorsEventDataLakeAppFailedOpenSearchMetricsAlarmEventDataLakeAppFailed556D56F1" + } + ], + "AlarmDescription": "Detect GitHub Event Data Lake App failure", + "AlarmName": "Event_data_lake_app_failed", + "ComparisonOperator": "LessThanThreshold", + "DatapointsToAlarm": 1, + "EvaluationPeriods": 1, + "Metrics": [ + { + "Expression": "FILL(metric, 0)", + "Id": "expr_1" + }, + { + "Id": "metric", + "MetricStat": { + "Metric": { + "MetricName": "LabelCanaryEvent", + "Namespace": "GitHubCanary" + }, + "Period": 600, + "Stat": "Sum" + }, + "ReturnData": false + } + ], + "Threshold": 1, + "TreatMissingData": "breaching" + }); }); diff --git a/src/main/java/org/opensearchmetrics/datasource/DataSourceType.java b/src/main/java/org/opensearchmetrics/datasource/DataSourceType.java index 21f6129..cdad834 100644 --- a/src/main/java/org/opensearchmetrics/datasource/DataSourceType.java +++ b/src/main/java/org/opensearchmetrics/datasource/DataSourceType.java @@ -11,5 +11,8 @@ public enum DataSourceType { SLACK_WEBHOOK_URL, SLACK_CHANNEL, - SLACK_USERNAME + SLACK_USERNAME, + GITHUB_APP_KEY, + GITHUB_APP_ID, + GITHUB_APP_INSTALL_ID, } diff --git a/src/main/java/org/opensearchmetrics/github/GhAppClient.java b/src/main/java/org/opensearchmetrics/github/GhAppClient.java new file mode 100644 index 0000000..ffcd78a --- /dev/null +++ b/src/main/java/org/opensearchmetrics/github/GhAppClient.java @@ -0,0 +1,148 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearchmetrics.github; + +import org.opensearchmetrics.datasource.DataSourceType; +import org.opensearchmetrics.model.github.GhAppAccessToken; +import org.opensearchmetrics.util.SecretsManagerUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.annotations.VisibleForTesting; +import io.jsonwebtoken.Jwts; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.codec.binary.Base64; +import org.apache.http.Header; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.BasicResponseHandler; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.message.BasicHeader; +import org.bouncycastle.asn1.*; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; + +import java.io.IOException; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +@Slf4j +@VisibleForTesting +public class GhAppClient { + + protected static final String GH_ISSUE_BASE_URL = "https://api.github.com"; + private final SecretsManagerUtil secretsManagerUtil; + private final ObjectMapper objectMapper; + + public GhAppClient(SecretsManagerUtil secretsManagerUtil, ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + this.secretsManagerUtil = secretsManagerUtil; + } + + protected HttpResponse executeGet(HttpGet request, CloseableHttpClient client) { + try { + return client.execute(request); + } catch (IOException e) { + throw new RuntimeException("Error while making HTTP call", e); + } + } + + @VisibleForTesting + CloseableHttpClient createJwtClient(String token) { + final HttpClientBuilder httpClientBuilder = HttpClientBuilder.create(); + try { + Header contentTypeHeader = new BasicHeader(HttpHeaders.ACCEPT, "application/vnd.github+json"); + Header authorizationHeader = new BasicHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token); + List
headers = Arrays.asList(contentTypeHeader, authorizationHeader); + httpClientBuilder.setDefaultHeaders(headers); + } catch (Exception ex) { + log.info("Unable to get GitHub credentials, making non-authenticated API calls", ex); + } + return httpClientBuilder.build(); + } + + @VisibleForTesting + protected CloseableHttpClient createGhClient(String token) { + final HttpClientBuilder httpClientBuilder = HttpClientBuilder.create().disableRedirectHandling(); + try { + Header contentTypeHeader = new BasicHeader(HttpHeaders.CONTENT_TYPE, " application/json"); + Header authorizationHeader = new BasicHeader(HttpHeaders.AUTHORIZATION, "token " + token); + List
headers = Arrays.asList(contentTypeHeader, authorizationHeader); + httpClientBuilder.setDefaultHeaders(headers); + } catch (Exception ex) { + log.info("Unable to get GitHub credentials, making non-authenticated API calls", ex); + } + return httpClientBuilder.build(); + } + + HttpResponse executePost(HttpPost request, CloseableHttpClient client) { + try { + return client.execute(request); + } catch (IOException e) { + throw new RuntimeException("Error while making HTTP call", e); + } + } + + /* + Private key of the GH App (.pem format) -> DER (byte conversion) -> JWT -> access tokens as ghs_fmdsknfefefefdehdedfehdF + */ + + // TOKEN:2 Method used to get the access token from JWT token + @VisibleForTesting + public String createAccessToken() throws Exception { + String privateKeyDER = secretsManagerUtil.getGitHubAppCredentials(DataSourceType.GITHUB_APP_KEY).get(); + String ghAppJWT = createJWT(secretsManagerUtil.getGitHubAppCredentials(DataSourceType.GITHUB_APP_ID).get(), + privateKeyDER); + String accessTokenUrl = GH_ISSUE_BASE_URL + "/app/installations/" + + secretsManagerUtil.getGitHubAppCredentials(DataSourceType.GITHUB_APP_INSTALL_ID).get() + + "/access_tokens"; + HttpPost request = new HttpPost(accessTokenUrl); + CloseableHttpClient createJwtClient = createJwtClient(ghAppJWT); + HttpResponse response = executePost(request, createJwtClient(ghAppJWT)); + BasicResponseHandler basicResponseHandler = new BasicResponseHandler(); + String data = basicResponseHandler.handleResponse(response); + GhAppAccessToken accessToken = objectMapper.readValue(data, GhAppAccessToken.class); + createJwtClient.close(); + return accessToken.getToken(); + } + + // TOKEN:1 method used to convert the private key to DER format and outputs the JWT token + @VisibleForTesting + String createJWT(String issuer, String privateKeyDER) throws Exception { + byte[] data = Base64.decodeBase64(privateKeyDER); + ASN1EncodableVector asn1EncodableVectorV1 = new ASN1EncodableVector(); + asn1EncodableVectorV1.add(new ASN1Integer(0)); + ASN1EncodableVector asn1EncodableVectorV2 = new ASN1EncodableVector(); + asn1EncodableVectorV2.add(new ASN1ObjectIdentifier(PKCSObjectIdentifiers.rsaEncryption.getId())); + asn1EncodableVectorV2.add(DERNull.INSTANCE); + asn1EncodableVectorV1.add(new DERSequence(asn1EncodableVectorV2)); + asn1EncodableVectorV1.add(new DEROctetString(data)); + ASN1Sequence asn1Sequence = new DERSequence(asn1EncodableVectorV1); + byte[] privKey = asn1Sequence.getEncoded("DER"); + PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privKey); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec); + long currentTimeMillis = System.currentTimeMillis(); + Date issueDate = new Date(currentTimeMillis); + long expiryTimeMillis = currentTimeMillis + 600000; + Date expDate = new Date(expiryTimeMillis); + String compactJws = Jwts.builder() + .subject("GhIssueDelete") + .issuer(issuer) + .issuedAt(issueDate) + .expiration(expDate) + .signWith(privateKey, Jwts.SIG.RS256) + .compact(); + return compactJws; + } +} diff --git a/src/main/java/org/opensearchmetrics/lambda/EventCanaryLambda.java b/src/main/java/org/opensearchmetrics/lambda/EventCanaryLambda.java new file mode 100644 index 0000000..446558e --- /dev/null +++ b/src/main/java/org/opensearchmetrics/lambda/EventCanaryLambda.java @@ -0,0 +1,62 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearchmetrics.lambda; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.annotations.VisibleForTesting; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.kohsuke.github.GHLabel; +import org.kohsuke.github.GHRepository; +import org.opensearchmetrics.dagger.DaggerServiceComponent; +import org.opensearchmetrics.dagger.ServiceComponent; +import org.opensearchmetrics.util.SecretsManagerUtil; +import org.opensearchmetrics.github.GhAppClient; +import org.kohsuke.github.GitHub; +import org.kohsuke.github.GitHubBuilder; + +@Slf4j +public class EventCanaryLambda extends GhAppClient implements RequestHandler { + private static final ServiceComponent COMPONENT = DaggerServiceComponent.create(); + private final String GITHUB_OWNER_TARGET = System.getenv("GITHUB_OWNER_TARGET"); + private final String GITHUB_REPO_TARGET = System.getenv("GITHUB_REPO_TARGET"); + private final String LABEL_NAME = "s3-data-lake-app-canary-label"; + + public EventCanaryLambda() { + this(COMPONENT.getSecretsManagerUtil(), COMPONENT.getObjectMapper()); + } + + @VisibleForTesting + EventCanaryLambda(@NonNull SecretsManagerUtil secretsManagerUtil, @NonNull ObjectMapper mapper) { + super(secretsManagerUtil, mapper); + } + + @Override + public Void handleRequest(Void input, Context context) { + try { + String accessToken = createAccessToken(); + GitHub gitHub = new GitHubBuilder().withOAuthToken(accessToken).build(); + GHRepository repository = gitHub.getRepository(GITHUB_OWNER_TARGET + "/" + GITHUB_REPO_TARGET); + GHLabel label = repository.createLabel( + LABEL_NAME, + "0366d6", + "Canary label to test s3 data lake app" + ); + System.out.println("Label created successfully"); + label.delete(); + System.out.println("Label deleted successfully"); + } catch (Exception e) { + System.out.println("Label canary FAILED"); + throw new RuntimeException("Failed to run label canary", e); + } + return null; + } +} diff --git a/src/main/java/org/opensearchmetrics/metrics/release/ReleaseInputs.java b/src/main/java/org/opensearchmetrics/metrics/release/ReleaseInputs.java index bb7dc4a..37012c8 100644 --- a/src/main/java/org/opensearchmetrics/metrics/release/ReleaseInputs.java +++ b/src/main/java/org/opensearchmetrics/metrics/release/ReleaseInputs.java @@ -21,7 +21,8 @@ public enum ReleaseInputs { VERSION_1_3_16("1.3.16", "closed", "1.3", false), VERSION_1_3_17("1.3.17", "closed", "1.3", false), VERSION_1_3_18("1.3.18", "closed", "1.3", true), - VERSION_1_3_19("1.3.19", "closed", "1.3", true); + VERSION_1_3_19("1.3.19", "closed", "1.3", true), + VERSION_1_3_20("1.3.20", "open", "1.3", true); private final String version; private final String state; diff --git a/src/main/java/org/opensearchmetrics/model/github/GhAppAccessToken.java b/src/main/java/org/opensearchmetrics/model/github/GhAppAccessToken.java new file mode 100644 index 0000000..9cd042f --- /dev/null +++ b/src/main/java/org/opensearchmetrics/model/github/GhAppAccessToken.java @@ -0,0 +1,30 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearchmetrics.model.github; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +/* + * This class is a data model for serializing and deserializing the Gh App token response and get the access token. + * Used in class GhAppClient.java + * */ + +@JsonIgnoreProperties(ignoreUnknown = true) +@Data +@RequiredArgsConstructor +@AllArgsConstructor +public class GhAppAccessToken { + + @JsonProperty("token") + private String token; +} diff --git a/src/main/java/org/opensearchmetrics/util/SecretsManagerUtil.java b/src/main/java/org/opensearchmetrics/util/SecretsManagerUtil.java index fa50c99..f0f1fba 100644 --- a/src/main/java/org/opensearchmetrics/util/SecretsManagerUtil.java +++ b/src/main/java/org/opensearchmetrics/util/SecretsManagerUtil.java @@ -24,7 +24,7 @@ @Slf4j public class SecretsManagerUtil { - private static final String SLACK_CREDENTIALS_SECRETS = "SLACK_CREDENTIALS_SECRETS"; + private static final String API_CREDENTIALS_SECRETS = "API_CREDENTIALS_SECRETS"; private final AWSSecretsManager secretsManager; private final ObjectMapper mapper; @@ -45,8 +45,20 @@ public static class SlackCredentials { private String slackUsername; } + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + @Data + public static class GitHubAppCredentials { + @JsonProperty("githubAppKey") + private String githubAppKey; + @JsonProperty("githubAppId") + private String githubAppId; + @JsonProperty("githubAppInstallId") + private String githubAppInstallId; + } + public Optional getSlackCredentials(DataSourceType datasourceType) throws IOException { - String secretName = System.getenv(SLACK_CREDENTIALS_SECRETS); + String secretName = System.getenv(API_CREDENTIALS_SECRETS); log.info("Retrieving secrets value from secrets = {} ", secretName); GetSecretValueResult getSecretValueResult = secretsManager.getSecretValue(new GetSecretValueRequest().withSecretId(secretName)); @@ -64,4 +76,24 @@ public Optional getSlackCredentials(DataSourceType datasourceType) throw return Optional.empty(); } } + + public Optional getGitHubAppCredentials(DataSourceType datasourceType) throws IOException { + String secretName = System.getenv(API_CREDENTIALS_SECRETS); + log.info("Retrieving secrets value from secrets = {} ", secretName); + GetSecretValueResult getSecretValueResult = + secretsManager.getSecretValue(new GetSecretValueRequest().withSecretId(secretName)); + log.info("Successfully retrieved secrets for data source credentials"); + GitHubAppCredentials credentials = + mapper.readValue(getSecretValueResult.getSecretString(), GitHubAppCredentials.class); + switch (datasourceType) { + case GITHUB_APP_KEY: + return Optional.of(credentials.getGithubAppKey()); + case GITHUB_APP_ID: + return Optional.of(credentials.getGithubAppId()); + case GITHUB_APP_INSTALL_ID: + return Optional.of(credentials.getGithubAppInstallId()); + default: + return Optional.empty(); + } + } } diff --git a/src/test/java/org/opensearchmetrics/github/GhAppClientTest.java b/src/test/java/org/opensearchmetrics/github/GhAppClientTest.java new file mode 100644 index 0000000..a2dbb95 --- /dev/null +++ b/src/test/java/org/opensearchmetrics/github/GhAppClientTest.java @@ -0,0 +1,146 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearchmetrics.github; + +import org.apache.http.StatusLine; +import org.apache.http.entity.StringEntity; +import org.opensearchmetrics.datasource.DataSourceType; +import org.opensearchmetrics.model.github.GhAppAccessToken; +import org.opensearchmetrics.util.SecretsManagerUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +public class GhAppClientTest { + + private GhAppClient ghAppClient; + @Mock + private SecretsManagerUtil secretsManagerUtil; + @Mock + private ObjectMapper objectMapper; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + this.ghAppClient = new GhAppClient(secretsManagerUtil, objectMapper); + } + + @Test + void WHEN_executeGet_THEN_return_CloseableHttpClient() { + HttpGet httpGet = mock(HttpGet.class); + HttpResponse closeableHttpResponse = mock(CloseableHttpResponse.class); + CloseableHttpClient closeableHttpClient = mock(CloseableHttpClient.class); + when(ghAppClient.executeGet(httpGet, closeableHttpClient)).thenReturn(closeableHttpResponse); + } + + @Test + void WHEN_executePost_THEN_return_CloseableHttpClient() { + HttpPost httpPost = mock(HttpPost.class); + HttpResponse closeableHttpResponse = mock(CloseableHttpResponse.class); + CloseableHttpClient closeableHttpClient = mock(CloseableHttpClient.class); + when(ghAppClient.executePost(httpPost, closeableHttpClient)).thenReturn(closeableHttpResponse); + } + + @Test + public void testCreateJwtClient() throws NoSuchFieldException, IllegalAccessException { + String testToken = "test-jwt-token"; + CloseableHttpClient client = ghAppClient.createJwtClient(testToken); + assertNotNull(client); + } + + @Test + public void WHEN_createAccessToken_THEN_return_accessToken() throws Exception { + // Mock dependencies + CloseableHttpClient mockHttpClient = mock(CloseableHttpClient.class); + HttpResponse mockResponse = mock(HttpResponse.class); + StatusLine mockStatusLine = mock(StatusLine.class); + + // Setup test data + String testPrivateKey = "test-private-key"; + String testAppId = "test-app-id"; + String testInstallId = "test-install-id"; + String testJwt = "test-jwt"; + String testResponseData = "{\"token\": \"test-access-token\"}"; + GhAppAccessToken testAccessToken = new GhAppAccessToken(); + testAccessToken.setToken("test-access-token"); + + // Setup mock behaviors + when(secretsManagerUtil.getGitHubAppCredentials(DataSourceType.GITHUB_APP_KEY)) + .thenReturn(Optional.of(testPrivateKey)); + when(secretsManagerUtil.getGitHubAppCredentials(DataSourceType.GITHUB_APP_ID)) + .thenReturn(Optional.of(testAppId)); + when(secretsManagerUtil.getGitHubAppCredentials(DataSourceType.GITHUB_APP_INSTALL_ID)) + .thenReturn(Optional.of(testInstallId)); + + GhAppClient spyGhAppClient = spy(new GhAppClient(secretsManagerUtil, objectMapper)); + doReturn(testJwt).when(spyGhAppClient).createJWT(anyString(), anyString()); + doReturn(mockHttpClient).when(spyGhAppClient).createJwtClient(anyString()); + doReturn(mockResponse).when(spyGhAppClient).executePost(any(HttpPost.class), any(CloseableHttpClient.class)); + + when(mockResponse.getStatusLine()).thenReturn(mockStatusLine); + + when(mockResponse.getEntity()).thenReturn(new StringEntity(testResponseData)); + when(objectMapper.readValue(testResponseData, GhAppAccessToken.class)) + .thenReturn(testAccessToken); + + // Execute the method + String result = spyGhAppClient.createAccessToken(); + + // Verify the result and interactions + assertEquals("test-access-token", result); + verify(secretsManagerUtil).getGitHubAppCredentials(DataSourceType.GITHUB_APP_KEY); + verify(secretsManagerUtil).getGitHubAppCredentials(DataSourceType.GITHUB_APP_ID); + verify(secretsManagerUtil).getGitHubAppCredentials(DataSourceType.GITHUB_APP_INSTALL_ID); + verify(spyGhAppClient).createJWT(testAppId, testPrivateKey); + verify(mockHttpClient).close(); + } + + @Test + void WHEN_createJWT_THEN_return_jwt() throws Exception { + String testKey = "MIIEpAIBAAKCAQEAtoRdC4mwHyGm3ZnhEnU5x1FqnrP61epGW00lXx9UA4yOXxjV\n" + + "F5FZ/8m5i4oS5mv4q3lWEteXmML/b17fopYvUjkWiFdJf94kpxWcVvm4I0xyeqYE\n" + + "9ASjpN2jNjQ8s/u03WpytHewsp5/aWoiZtAGBTfBvVyVPXFS31/cS/jY+0Z33P4n\n" + + "MKBbqb3CbdPz4rd/VWDWOI1LhXBexkcKZcyPROebF3RejxzkxgNhzeC3huErT6mX\n" + + "nJ3NGXWydMCQG+SVjg54g9A9i8yJ0ue+RgiIxnu5LLjncDiXZZpy/UdcABedejB3\n" + + "TgJGzX5Za2W+k1rTVF10FliIOJ2QIWUuIrEsyQIDAQABAoIBAQCNLtBmp2hUfIx+\n" + + "aJTg2UsLcmA+SUykAmfQIlnhPfOYFzbeOvBDHc13foyHcxPxp92gjuhVBO4gXd6H\n" + + "QOVO+Eu8l6plZtfVEHpbwOzBnsOgkncPhrLYK2qGkme4+ylltDQQ/lGiZd+KG+7F\n" + + "FTNtQkcV7C5yk1ZiQ/HuFlHrdqAppedkqUunT23ohw+R10AOMV20laVpf/nrOfO3\n" + + "lfoXmr/NUIj83rziC8dJ15+2HqpEmTHO+w7aUka14ZG9OnLUVwpD/dHuLkNrk0Me\n" + + "KffbxDb3163k03JkeL8lWQs19PQgcjpxF4JZk7r/kPQ/TwP8inedsN1FeMIP8aZS\n" + + "LEjihE+BAoGBAN9Li9AG3HuIL1+bCXz5oxvjszYHkbJa5Ksi+kTbOEILFhk8aMcB\n" + + "+bSWVLkgF7EMc+W2407nL/YY7dCnbqBs7rRbQ0+sVKYeL8LglVnMWaZYH/FnKQ52\n" + + "QF+Dv9PXynd1w/PQujAKn1MMGSvxPEW16sGnXrH1E4AJlheBUEUOllYZAoGBANE/\n" + + "1+P5VkndOjcgvuQPUI1ogD4+maUjZVVj/z5a48S4GOPKITtOvRrMFI5heH0wEjYw\n" + + "XYqwBSMRCDrYp+tGqZLzlGUPiN/g6pDgSd5Id6QUsNxgn+ekOSFlHNjZwAEXBdNY\n" + + "tKvrrQ9r/jz+cggE6OpKoKUuybAOU/1VzQ50AoIxAoGAYqhiUbt2Vy5IoBlEC+/Q\n" + + "XVYxrEGT4hW+ys5dfWbOaH+1d9j1AlihF2UEcfb4AMXbvzcbH5WN31IMYRBZFJCM\n" + + "tytLhjxB+lOEDrpjwpVDVvfAxUwrG7SrpIf1jYfecQGbXnJukSNgWbUSuhOP6c0C\n" + + "uCVW9ZGu1/dkVWZRLPHRAqECgYEArGGyE0dHhNZRrTS2zd6n97bNX3nmzZqZUn1s\n" + + "uwvZdChNqOrN8bPuKfNSQ/Gcd1Vwy1+Q0D4uHTNc2k2+GB9Ad6Ve7NqdYgJCe1Oq\n" + + "xwpgNbYt9X9MfGJYBmDsIOFSQhObYv9C6BbhnUDUU58yhdS1pL4SFcKzuOw02REk\n" + + "OvHrVyECgYButEdNKoDE47nXa46dtWKW1b5BiWd3HhGUttsResF86O/QExJ0lA0h\n" + + "5g1hCwej9ZihKazQTWk1cIA6c0/HwLm+KYlqQy8xGKOXtMqyu8y63Ga65QtVdM5c\n" + + "Bik2ujDXFSRKb5sKGSE1t5I9PWinfMF894apWbB1x9/Zf0CvcUmJ7Q=="; + String createJWT = ghAppClient.createJWT("1234", String.valueOf(testKey)); + assertNotNull(createJWT); + } +} diff --git a/src/test/java/org/opensearchmetrics/lambda/EventCanaryLambdaTest.java b/src/test/java/org/opensearchmetrics/lambda/EventCanaryLambdaTest.java new file mode 100644 index 0000000..7270989 --- /dev/null +++ b/src/test/java/org/opensearchmetrics/lambda/EventCanaryLambdaTest.java @@ -0,0 +1,68 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearchmetrics.lambda; + +import com.amazonaws.services.lambda.runtime.Context; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.kohsuke.github.GHLabel; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GitHub; +import org.kohsuke.github.GitHubBuilder; +import org.mockito.Mock; +import org.mockito.MockedConstruction; +import org.mockito.MockitoAnnotations; +import org.opensearchmetrics.util.SecretsManagerUtil; + + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +public class EventCanaryLambdaTest { + + @Mock + private SecretsManagerUtil secretsManagerUtil; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void testHandleRequest() throws Exception { + ObjectMapper realMapper = new ObjectMapper(); + EventCanaryLambda eventCanaryLambda = spy(new EventCanaryLambda(secretsManagerUtil, realMapper)); + doReturn("mock-token").when(eventCanaryLambda).createAccessToken(); + + GitHub mockGitHub = mock(GitHub.class); + GHRepository mockRepo = mock(GHRepository.class); + GHLabel mockLabel = mock(GHLabel.class); + + try (MockedConstruction gitHubBuilder = mockConstruction( + GitHubBuilder.class, + (builderMock, context) -> { + when(builderMock.withOAuthToken(any())).thenReturn(builderMock); + when(builderMock.build()).thenReturn(mockGitHub); + })) { + when(mockGitHub.getRepository(anyString())).thenReturn(mockRepo); + when(mockRepo.createLabel(anyString(), anyString(), anyString())).thenReturn(mockLabel); + + eventCanaryLambda.handleRequest(null, mock(Context.class)); + + verify(eventCanaryLambda).createAccessToken(); + verify(mockRepo).createLabel( + eq("s3-data-lake-app-canary-label"), + eq("0366d6"), + eq("Canary label to test s3 data lake app") + ); + verify(mockLabel).delete(); + } + } +} diff --git a/src/test/java/org/opensearchmetrics/metrics/release/ReleaseInputsTest.java b/src/test/java/org/opensearchmetrics/metrics/release/ReleaseInputsTest.java index fef8a25..623e0d0 100644 --- a/src/test/java/org/opensearchmetrics/metrics/release/ReleaseInputsTest.java +++ b/src/test/java/org/opensearchmetrics/metrics/release/ReleaseInputsTest.java @@ -30,6 +30,7 @@ public void testGetVersion() { assertEquals("1.3.17", ReleaseInputs.VERSION_1_3_17.getVersion()); assertEquals("1.3.18", ReleaseInputs.VERSION_1_3_18.getVersion()); assertEquals("1.3.19", ReleaseInputs.VERSION_1_3_19.getVersion()); + assertEquals("1.3.20", ReleaseInputs.VERSION_1_3_20.getVersion()); } @Test @@ -47,6 +48,7 @@ public void testGetState() { assertEquals("closed", ReleaseInputs.VERSION_1_3_17.getState()); assertEquals("closed", ReleaseInputs.VERSION_1_3_18.getState()); assertEquals("closed", ReleaseInputs.VERSION_1_3_19.getState()); + assertEquals("open", ReleaseInputs.VERSION_1_3_20.getState()); } @Test @@ -64,6 +66,7 @@ public void testGetBranch() { assertEquals("1.3", ReleaseInputs.VERSION_1_3_17.getBranch()); assertEquals("1.3", ReleaseInputs.VERSION_1_3_18.getBranch()); assertEquals("1.3", ReleaseInputs.VERSION_1_3_19.getBranch()); + assertEquals("1.3", ReleaseInputs.VERSION_1_3_20.getBranch()); } @Test @@ -81,12 +84,13 @@ public void testGetTrack() { assertEquals(false, ReleaseInputs.VERSION_1_3_17.getTrack()); assertEquals(true, ReleaseInputs.VERSION_1_3_18.getTrack()); assertEquals(true, ReleaseInputs.VERSION_1_3_19.getTrack()); + assertEquals(true, ReleaseInputs.VERSION_1_3_20.getTrack()); } @Test public void testGetAllReleaseInputs() { ReleaseInputs[] releaseInputs = ReleaseInputs.getAllReleaseInputs(); - assertEquals(13, releaseInputs.length); + assertEquals(14, releaseInputs.length); assertEquals(ReleaseInputs.VERSION_3_0_0, releaseInputs[0]); assertEquals(ReleaseInputs.VERSION_2_12_0, releaseInputs[1]); assertEquals(ReleaseInputs.VERSION_2_13_0, releaseInputs[2]); @@ -100,6 +104,7 @@ public void testGetAllReleaseInputs() { assertEquals(ReleaseInputs.VERSION_1_3_17, releaseInputs[10]); assertEquals(ReleaseInputs.VERSION_1_3_18, releaseInputs[11]); assertEquals(ReleaseInputs.VERSION_1_3_19, releaseInputs[12]); + assertEquals(ReleaseInputs.VERSION_1_3_20, releaseInputs[13]); } } diff --git a/src/test/java/org/opensearchmetrics/util/SecretsManagerUtilTest.java b/src/test/java/org/opensearchmetrics/util/SecretsManagerUtilTest.java index 7191e73..c7fe654 100644 --- a/src/test/java/org/opensearchmetrics/util/SecretsManagerUtilTest.java +++ b/src/test/java/org/opensearchmetrics/util/SecretsManagerUtilTest.java @@ -71,4 +71,35 @@ void testGetSlackCredentials() throws IOException { assertEquals(slackChannel, channelResult.get()); assertEquals(slackUsername, usernameResult.get()); } + + @Test + void testGetGitHubApiCredentials() throws IOException { + String githubAppKey = "github-app-key"; + String githubAppId = "github-app-id"; + String githubAppInstallId = "github-app-install-id"; + SecretsManagerUtil.GitHubAppCredentials githubAppCredentials = new SecretsManagerUtil.GitHubAppCredentials(); + githubAppCredentials.setGithubAppKey(githubAppKey); + githubAppCredentials.setGithubAppId(githubAppId); + githubAppCredentials.setGithubAppInstallId(githubAppInstallId); + + String secretString = "secret-string-with-github-credentials"; + GetSecretValueResult getSecretValueResult = new GetSecretValueResult(); + getSecretValueResult.setSecretString(secretString); + + when(secretsManager.getSecretValue(any(GetSecretValueRequest.class))) + .thenReturn(getSecretValueResult); + when(mapper.readValue(eq(secretString), eq(SecretsManagerUtil.GitHubAppCredentials.class))) + .thenReturn(githubAppCredentials); + + Optional appKeyResult = secretsManagerUtil.getGitHubAppCredentials(DataSourceType.GITHUB_APP_KEY); + Optional appIdResult = secretsManagerUtil.getGitHubAppCredentials(DataSourceType.GITHUB_APP_ID); + Optional appInstallIdResult = secretsManagerUtil.getGitHubAppCredentials(DataSourceType.GITHUB_APP_INSTALL_ID); + + assertTrue(appKeyResult.isPresent()); + assertTrue(appIdResult.isPresent()); + assertTrue(appInstallIdResult.isPresent()); + assertEquals(githubAppKey, appKeyResult.get()); + assertEquals(githubAppId, appIdResult.get()); + assertEquals(githubAppInstallId, appInstallIdResult.get()); + } }