diff --git a/pom.xml b/pom.xml index 8d6c8317da1c8ef7ddad77b2d371afa6cff7774d..eef38efd9dc660066d903e17802171631c6f969e 100644 --- a/pom.xml +++ b/pom.xml @@ -136,6 +136,7 @@ <module>provider/indexer-azure</module> <module>provider/indexer-gcp</module> <module>provider/indexer-ibm</module> + <module>provider/indexer-reference</module> </modules> <repositories> diff --git a/provider/indexer-reference/Dockerfile b/provider/indexer-reference/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..1e6901f365e2e9c7049c5019b4037ade931b65f0 --- /dev/null +++ b/provider/indexer-reference/Dockerfile @@ -0,0 +1,5 @@ +FROM openjdk:8-slim +WORKDIR /app +COPY target/indexer-reference-0.0.1-SNAPSHOT-spring-boot.jar indexer-reference.jar +# Run the web service on container startup. +CMD java -Djava.security.egd=file:/dev/./urandom -Dserver.port=8080 -jar /app/indexer-reference.jar diff --git a/provider/indexer-reference/docker-compose.yml b/provider/indexer-reference/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..a6838447de241e658325e275434293b2d4e2be09 --- /dev/null +++ b/provider/indexer-reference/docker-compose.yml @@ -0,0 +1,12 @@ +version: "3" +services: + os-storage-app: + build: + args: + JAR_FILE: target/indexer-reference-0.0.1-SNAPSHOT-spring-boot.jar + context: "" + dockerfile: ../Dockerfile + image: us.gcr.io/osdu-anthos-02/os-indexer/anthos-indexer-reference + ports: + - "8080:8080" + diff --git a/provider/indexer-reference/kubernetes/deployments/deployment-os-indexer-service.yml b/provider/indexer-reference/kubernetes/deployments/deployment-os-indexer-service.yml new file mode 100644 index 0000000000000000000000000000000000000000..0157d9875389754f1c640f0d186e02b2ea0020d8 --- /dev/null +++ b/provider/indexer-reference/kubernetes/deployments/deployment-os-indexer-service.yml @@ -0,0 +1,179 @@ +apiVersion: v1 +data: + AUTHORIZE_API: ${AUTHORIZE_API} + MONGO_DB_URL: ${MONGO_DB_URL} + MONGO_DB_USER: ${MONGO_DB_USER} + MONGO_DB_NAME: ${MONGO_DB_NAME} + REGION: ${REGION} + LEGALTAG_API: ${LEGALTAG_API} + INDEXER_HOST: ${INDEXER_HOST} + GOOGLE_CLOUD_PROJECT: ${GOOGLE_CLOUD_PROJECT} + STORAGE_HOSTNAME: ${STORAGE_HOSTNAME} + STORAGE_SCHEMA_HOST: ${STORAGE_SCHEMA_HOST} + STORAGE_QUERY_RECORD_HOST: ${STORAGE_QUERY_RECORD_HOST} + STORAGE_QUERY_RECORD_FOR_CONVERSION_HOST: ${STORAGE_QUERY_RECORD_FOR_CONVERSION_HOST} + INDEXER_QUEUE_HOST: ${INDEXER_QUEUE_HOST} + CRS_API: ${CRS_API} + REDIS_GROUP_HOST: ${REDIS_GROUP_HOST} + REDIS_SEARCH_HOST: ${REDIS_SEARCH_HOST} + GOOGLE_AUDIENCES: ${GOOGLE_AUDIENCES} + KEY_RING: ${KEY_RING} + KMS_KEY: ${KMS_KEY} +kind: ConfigMap +metadata: + labels: + app: indexer-reference + name: indexer-config + namespace: default +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + generateName: indexer-reference-anthos + labels: + app: indexer-reference + name: indexer-reference + namespace: default +spec: + selector: + matchLabels: + app: indexer-reference + replicas: 1 + template: + metadata: + labels: + app: indexer-reference + spec: + containers: + - env: + - name: AUTHORIZE_API + valueFrom: + configMapKeyRef: + key: AUTHORIZE_API + name: indexer-config + - name: MONGO_DB_URL + valueFrom: + configMapKeyRef: + key: MONGO_DB_URL + name: indexer-config + - name: MONGO_DB_USER + valueFrom: + configMapKeyRef: + key: MONGO_DB_USER + name: indexer-config + - name: MONGO_DB_PASSWORD + valueFrom: + secretKeyRef: + name: indexer-secret + key: mongo.db.password + - name: MONGO_DB_NAME + valueFrom: + configMapKeyRef: + key: MONGO_DB_NAME + name: indexer-config + - name: MB_RABBITMQ_URI + valueFrom: + secretKeyRef: + name: indexer-secret + key: mb.rabbitmq.uri + - name: REGION + valueFrom: + configMapKeyRef: + key: REGION + name: indexer-config + - name: LEGALTAG_API + valueFrom: + configMapKeyRef: + key: LEGALTAG_API + name: indexer-config + - name: INDEXER_HOST + valueFrom: + configMapKeyRef: + key: INDEXER_HOST + name: indexer-config + - name: GOOGLE_CLOUD_PROJECT + valueFrom: + configMapKeyRef: + key: GOOGLE_CLOUD_PROJECT + name: indexer-config + - name: STORAGE_HOSTNAME + valueFrom: + configMapKeyRef: + key: STORAGE_HOSTNAME + name: indexer-config + - name: STORAGE_SCHEMA_HOST + valueFrom: + configMapKeyRef: + key: STORAGE_SCHEMA_HOST + name: indexer-config + - name: STORAGE_QUERY_RECORD_HOST + valueFrom: + configMapKeyRef: + key: STORAGE_QUERY_RECORD_HOST + name: indexer-config + - name: STORAGE_QUERY_RECORD_FOR_CONVERSION_HOST + valueFrom: + configMapKeyRef: + key: STORAGE_QUERY_RECORD_FOR_CONVERSION_HOST + name: indexer-config + - name: INDEXER_QUEUE_HOST + valueFrom: + configMapKeyRef: + key: INDEXER_QUEUE_HOST + name: indexer-config + - name: CRS_API + valueFrom: + configMapKeyRef: + key: CRS_API + name: indexer-config + - name: REDIS_GROUP_HOST + valueFrom: + configMapKeyRef: + key: REDIS_GROUP_HOST + name: indexer-config + - name: REDIS_SEARCH_HOST + valueFrom: + configMapKeyRef: + key: REDIS_SEARCH_HOST + name: indexer-config + - name: GOOGLE_AUDIENCES + valueFrom: + configMapKeyRef: + key: GOOGLE_AUDIENCES + name: indexer-config + - name: KEY_RING + valueFrom: + configMapKeyRef: + key: KEY_RING + name: indexer-config + - name: KMS_KEY + valueFrom: + configMapKeyRef: + key: KMS_KEY + name: indexer-config + image: us.gcr.io/osdu-anthos-02/os-indexer/anthos-indexer-reference:9966597-dirty + name: indexer-reference +--- +apiVersion: v1 +kind: Service +metadata: + name: indexer-reference + namespace: default +spec: + ports: + - protocol: TCP + port: 80 + targetPort: 8080 + selector: + app: indexer-reference + type: LoadBalancer +--- +apiVersion: v1 +data: + mongo.db.password: ${mongo.db.password} + mb.rabbitmq.uri: ${mb.rabbitmq.uri} +kind: Secret +metadata: + name: indexer-secret + namespace: default +type: Opaque \ No newline at end of file diff --git a/provider/indexer-reference/pom.xml b/provider/indexer-reference/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..99d01082c723b55da8f969a693d301daf9378570 --- /dev/null +++ b/provider/indexer-reference/pom.xml @@ -0,0 +1,197 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Copyright 2020 Google LLC + ~ Copyright 2020 EPAM Systems, Inc + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ https://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.opengroup.osdu.indexer</groupId> + <artifactId>indexer-service</artifactId> + <version>1.0.4-SNAPSHOT</version> + <relativePath>../../pom.xml</relativePath> + </parent> + + <artifactId>indexer-reference</artifactId> + <version>0.0.1-SNAPSHOT</version> + <name>indexer-reference</name> + <description>Indexer Service GCP Anthos</description> + <packaging>jar</packaging> + + <dependencies> + <dependency> + <groupId>org.opengroup.osdu.indexer</groupId> + <artifactId>indexer-core</artifactId> + <version>1.0.6-SNAPSHOT</version> + </dependency> + + <dependency> + <groupId>org.opengroup.osdu</groupId> + <artifactId>core-lib-gcp</artifactId> + <version>0.1.17</version> + </dependency> + + <dependency> + <groupId>org.opengroup.osdu</groupId> + <artifactId>os-core-common</artifactId> + <version>0.0.18</version> + </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-data-mongodb</artifactId> + </dependency> + + <dependency> + <groupId>com.google.cloud</groupId> + <artifactId>google-cloud-logging</artifactId> + <version>1.72.0</version> + </dependency> + <dependency> + <groupId>com.google.apis</groupId> + <artifactId>google-api-services-storage</artifactId> + <version>v1-rev150-1.25.0</version> + <exclusions> + <exclusion> + <groupId>com.google.guava</groupId> + <artifactId>guava-jdk5</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <groupId>com.google.apis</groupId> + <artifactId>google-api-services-cloudkms</artifactId> + <version>v1-rev81-1.25.0</version> + </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-amqp</artifactId> + </dependency> + <dependency> + <groupId>org.elasticsearch</groupId> + <artifactId>elasticsearch</artifactId> + <version>6.6.2</version> + </dependency> + <dependency> + <groupId>org.elasticsearch.client</groupId> + <artifactId>elasticsearch-rest-client</artifactId> + <version>6.6.2</version> + </dependency> + <dependency> + <groupId>org.elasticsearch.client</groupId> + <artifactId>elasticsearch-rest-high-level-client</artifactId> + <version>6.6.2</version> + </dependency> + + <!-- Test Dependencies --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.springframework.security</groupId> + <artifactId>spring-security-test</artifactId> + <scope>test</scope> + </dependency> + + <!-- Test Dependencies --> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>4.12</version> + <scope>test</scope> + </dependency> + <!-- https://mvnrepository.com/artifact/org.powermock/powermock-api-mockito2 --> + <dependency> + <groupId>org.powermock</groupId> + <artifactId>powermock-api-mockito2</artifactId> + <version>2.0.2</version> + <scope>test</scope> + </dependency> + <!-- https://mvnrepository.com/artifact/org.powermock/powermock-module-junit4 --> + <dependency> + <groupId>org.powermock</groupId> + <artifactId>powermock-module-junit4</artifactId> + <version>2.0.2</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <version>2.26.0</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.codehaus.mojo</groupId> + <artifactId>cobertura-maven-plugin</artifactId> + <version>2.7</version> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>com.auth0</groupId> + <artifactId>java-jwt</artifactId> + <version>3.8.1</version> + </dependency> + + <!-- https://mvnrepository.com/artifact/com.github.stefanbirkner/system-rules --> + <dependency> + <groupId>com.github.stefanbirkner</groupId> + <artifactId>system-rules</artifactId> + <version>1.2.0</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.google.api.grpc</groupId> + <artifactId>proto-google-iam-v1</artifactId> + <version>0.12.0</version> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-actuator-autoconfigure</artifactId> + </dependency> + + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-maven-plugin</artifactId> + <executions> + <execution> + <goals> + <goal>repackage</goal> + </goals> + <configuration> + <classifier>spring-boot</classifier> + <mainClass> + org.opengroup.osdu.indexer.IndexerAnthosApplication + </mainClass> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + + +</project> diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/IndexerAnthosApplication.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/IndexerAnthosApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..d2c3e68f06d2da43a8988aa470b58d2002f36888 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/IndexerAnthosApplication.java @@ -0,0 +1,41 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer; + +import org.opengroup.osdu.core.gcp.multitenancy.TenantFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; + +@SpringBootApplication(exclude = {MongoAutoConfiguration.class, SecurityAutoConfiguration.class, + ManagementWebSecurityAutoConfiguration.class}) +@Configuration +@ComponentScan(value = {"org.opengroup.osdu"}, excludeFilters = { + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = TenantFactory.class)}) +public class IndexerAnthosApplication { + + public static void main(String[] args) { + SpringApplication.run(IndexerAnthosApplication.class, args); + } + +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/ServletInitializer.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/ServletInitializer.java new file mode 100644 index 0000000000000000000000000000000000000000..ec6996fe06ba02a1675486c9c22d341389736d32 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/ServletInitializer.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer; + +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; + +public class ServletInitializer extends SpringBootServletInitializer { + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + return application.sources(IndexerAnthosApplication.class); + } +} + diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/AttributesCache.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/AttributesCache.java new file mode 100644 index 0000000000000000000000000000000000000000..b60b4d14405fbadaaa0e25977b81e08fb66fe905 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/AttributesCache.java @@ -0,0 +1,65 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.cache; + +import java.util.Set; +import org.opengroup.osdu.core.common.cache.RedisCache; +import org.opengroup.osdu.core.common.provider.interfaces.IAttributesCache; +import org.opengroup.osdu.indexer.config.IndexerConfigurationProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class AttributesCache implements IAttributesCache<String, Set>, AutoCloseable { + + private RedisCache<String, Set> cache; + + @Autowired + public AttributesCache(IndexerConfigurationProperties indexerConfigurationProperties) { + cache = new RedisCache(indexerConfigurationProperties.getRedisSearchHost(), + Integer.parseInt(indexerConfigurationProperties.getRedisSearchPort()), + indexerConfigurationProperties.getIndexCacheExpiration() * 60, + String.class, + Boolean.class); + } + + @Override + public void put(String key, Set value) { + this.cache.put(key, value); + } + + @Override + public Set get(String key) { + return this.cache.get(key); + } + + @Override + public void delete(String key) { + this.cache.delete(key); + } + + @Override + public void clearAll() { + this.cache.clearAll(); + } + + @Override + public void close() { + this.cache.close(); + } +} \ No newline at end of file diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/DatastoreCredentialCache.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/DatastoreCredentialCache.java new file mode 100644 index 0000000000000000000000000000000000000000..6e832b44671a8ac95b65556076bf15122c6990a3 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/DatastoreCredentialCache.java @@ -0,0 +1,37 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.cache; + +import com.google.auth.oauth2.AccessToken; +import org.opengroup.osdu.core.common.cache.RedisCache; +import org.opengroup.osdu.indexer.config.IndexerConfigurationProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class DatastoreCredentialCache extends RedisCache<String, AccessToken> { + + @Autowired + public DatastoreCredentialCache(IndexerConfigurationProperties indexerConfigurationProperties) { + super(indexerConfigurationProperties.getRedisSearchHost(), + Integer.parseInt(indexerConfigurationProperties.getRedisSearchPort()), + 58 * 60, + String.class, + AccessToken.class); + } +} \ No newline at end of file diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/ElasticCredentialsCache.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/ElasticCredentialsCache.java new file mode 100644 index 0000000000000000000000000000000000000000..24804bc9000a8f10298b5e30a033ed99886c90de --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/ElasticCredentialsCache.java @@ -0,0 +1,66 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.cache; + +import org.opengroup.osdu.core.common.cache.RedisCache; +import org.opengroup.osdu.core.common.model.search.ClusterSettings; +import org.opengroup.osdu.core.common.provider.interfaces.IElasticCredentialsCache; +import org.opengroup.osdu.indexer.config.IndexerConfigurationProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class ElasticCredentialsCache implements IElasticCredentialsCache<String, ClusterSettings>, + AutoCloseable { + + private RedisCache<String, ClusterSettings> cache; + + @Autowired + public ElasticCredentialsCache(IndexerConfigurationProperties indexerConfigurationProperties) { + cache = new RedisCache<>(indexerConfigurationProperties.getRedisSearchHost(), + Integer.parseInt(indexerConfigurationProperties.getRedisSearchPort()), + indexerConfigurationProperties.getIndexCacheExpiration() * 60, + String.class, + ClusterSettings.class); + } + + @Override + public void close() throws Exception { + this.cache.close(); + } + + @Override + public void put(String s, ClusterSettings o) { + this.cache.put(s, o); + } + + @Override + public ClusterSettings get(String s) { + return this.cache.get(s); + } + + @Override + public void delete(String s) { + this.cache.delete(s); + } + + @Override + public void clearAll() { + this.cache.clearAll(); + } +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/IndexCache.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/IndexCache.java new file mode 100644 index 0000000000000000000000000000000000000000..de447717623d63ded8d1d1c46efc42817d618183 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/IndexCache.java @@ -0,0 +1,64 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.cache; + +import org.opengroup.osdu.core.common.cache.RedisCache; +import org.opengroup.osdu.core.common.provider.interfaces.IIndexCache; +import org.opengroup.osdu.indexer.config.IndexerConfigurationProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class IndexCache implements IIndexCache<String, Boolean>, AutoCloseable { + + private RedisCache<String, Boolean> cache; + + @Autowired + public IndexCache(IndexerConfigurationProperties indexerConfigurationProperties) { + cache = new RedisCache<>(indexerConfigurationProperties.getRedisSearchHost(), + Integer.parseInt(indexerConfigurationProperties.getRedisSearchPort()), + indexerConfigurationProperties.getIndexCacheExpiration() * 60, + String.class, + Boolean.class); + } + + @Override + public void close() throws Exception { + this.cache.close(); + } + + @Override + public void put(String s, Boolean o) { + this.cache.put(s, o); + } + + @Override + public Boolean get(String s) { + return this.cache.get(s); + } + + @Override + public void delete(String s) { + this.cache.delete(s); + } + + @Override + public void clearAll() { + this.cache.clearAll(); + } +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/JwtCache.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/JwtCache.java new file mode 100644 index 0000000000000000000000000000000000000000..9a2a02d753dfbbe506a10bab3ecb2d6083876bef --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/JwtCache.java @@ -0,0 +1,66 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.cache; + +import org.opengroup.osdu.core.common.cache.RedisCache; +import org.opengroup.osdu.core.common.model.search.IdToken; +import org.opengroup.osdu.core.common.provider.interfaces.IJwtCache; +import org.opengroup.osdu.indexer.config.IndexerConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +public class JwtCache implements IJwtCache<String, IdToken>, AutoCloseable { + + RedisCache<String, IdToken> cache; + + // google service account id_token can be requested only for 1 hr + private final static int EXPIRED_AFTER = 59; + + public JwtCache(IndexerConfigurationProperties indexerConfigurationProperties) { + cache = new RedisCache<>(indexerConfigurationProperties.getRedisSearchHost(), + Integer.parseInt(indexerConfigurationProperties.getRedisSearchPort()), + EXPIRED_AFTER * 60, + String.class, + IdToken.class); + } + + @Override + public void close() throws Exception { + this.cache.close(); + } + + @Override + public void put(String s, IdToken o) { + this.cache.put(s, o); + } + + @Override + public IdToken get(String s) { + return this.cache.get(s); + } + + @Override + public void delete(String s) { + this.cache.delete(s); + } + + @Override + public void clearAll() { + this.cache.clearAll(); + } +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/KindsCache.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/KindsCache.java new file mode 100644 index 0000000000000000000000000000000000000000..afc2a8c39299158c3a50e1e9f5d1f5b67e5a6095 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/KindsCache.java @@ -0,0 +1,66 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.cache; + +import java.util.Set; +import org.opengroup.osdu.core.common.cache.RedisCache; +import org.opengroup.osdu.core.common.provider.interfaces.IKindsCache; +import org.opengroup.osdu.indexer.config.IndexerConfigurationProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class KindsCache implements IKindsCache<String, Set>, AutoCloseable { + + private RedisCache<String, Set> cache; + + @Autowired + public KindsCache(IndexerConfigurationProperties indexerConfigurationProperties) { + cache = new RedisCache<>(indexerConfigurationProperties.getRedisSearchHost(), + Integer.parseInt(indexerConfigurationProperties.getRedisSearchPort()), + indexerConfigurationProperties.getKindsCacheExpiration() * 60, + indexerConfigurationProperties.getKindsRedisDatabase(), + String.class, + Set.class); + } + + @Override + public void close() throws Exception { + this.cache.close(); + } + + @Override + public void put(String s, Set o) { + this.cache.put(s, o); + } + + @Override + public Set get(String s) { + return this.cache.get(s); + } + + @Override + public void delete(String s) { + this.cache.delete(s); + } + + @Override + public void clearAll() { + this.cache.clearAll(); + } +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/SchemaCache.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/SchemaCache.java new file mode 100644 index 0000000000000000000000000000000000000000..3d84e22959501decedf8dd08564bc0869bee8d50 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/SchemaCache.java @@ -0,0 +1,64 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.cache; + +import org.opengroup.osdu.core.common.cache.RedisCache; +import org.opengroup.osdu.indexer.config.IndexerConfigurationProperties; +import org.opengroup.osdu.indexer.provider.interfaces.ISchemaCache; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class SchemaCache implements ISchemaCache<String, String>, AutoCloseable { + + private RedisCache<String, String> cache; + + @Autowired + public SchemaCache(IndexerConfigurationProperties indexerConfigurationProperties) { + cache = new RedisCache<>(indexerConfigurationProperties.getRedisSearchHost(), + Integer.parseInt(indexerConfigurationProperties.getRedisSearchPort()), + indexerConfigurationProperties.getSchemaCacheExpiration() * 60, + String.class, + String.class); + } + + @Override + public void close() throws Exception { + this.cache.close(); + } + + @Override + public void put(String s, String o) { + this.cache.put(s, o); + } + + @Override + public String get(String s) { + return this.cache.get(s); + } + + @Override + public void delete(String s) { + this.cache.delete(s); + } + + @Override + public void clearAll() { + this.cache.clearAll(); + } +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/config/EntitlementsConfigProperties.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/config/EntitlementsConfigProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..1ba4425a4f2f09ff361c00cbcf4ec554811daf34 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/config/EntitlementsConfigProperties.java @@ -0,0 +1,32 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties +@Getter +@Setter +public class EntitlementsConfigProperties { + + private String AuthorizeApi; +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/config/MongoDBConfigProperties.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/config/MongoDBConfigProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..080f76dfc25465b8539e8c0789e81941bd026e07 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/config/MongoDBConfigProperties.java @@ -0,0 +1,35 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties +@Getter +@Setter +public class MongoDBConfigProperties { + + private String mongoDbUrl; + private String mongoDbUser; + private String mongoDbPassword; + private String mongoDbName; +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/config/RabbitMqConfigProperties.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/config/RabbitMqConfigProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..55efbf1a75763e976d71a840335e60b14c8a5ad4 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/config/RabbitMqConfigProperties.java @@ -0,0 +1,32 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties +@Getter +@Setter +public class RabbitMqConfigProperties { + + private String mbRabbitMqUri; +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/di/EntitlementsClientFactory.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/di/EntitlementsClientFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..45131176cd1ff35eb2dc411b0073b245c0d3b8e8 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/di/EntitlementsClientFactory.java @@ -0,0 +1,55 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.di; + +import org.opengroup.osdu.core.common.entitlements.EntitlementsAPIConfig; +import org.opengroup.osdu.core.common.entitlements.EntitlementsFactory; +import org.opengroup.osdu.core.common.entitlements.IEntitlementsFactory; +import org.opengroup.osdu.indexer.config.EntitlementsConfigProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.AbstractFactoryBean; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; +import org.springframework.web.context.annotation.RequestScope; + +@Component +@RequestScope +@Lazy +public class EntitlementsClientFactory extends AbstractFactoryBean<IEntitlementsFactory> { + + private EntitlementsConfigProperties entitlementsConfigProperties; + + @Override + protected IEntitlementsFactory createInstance() throws Exception { + return new EntitlementsFactory(EntitlementsAPIConfig + .builder() + .rootUrl(entitlementsConfigProperties.getAuthorizeApi()) + .build()); + } + + @Override + public Class<?> getObjectType() { + return IEntitlementsFactory.class; + } + + @Autowired + public void setEntitlementsConfigProperties( + EntitlementsConfigProperties entitlementsConfigProperties) { + this.entitlementsConfigProperties = entitlementsConfigProperties; + } +} \ No newline at end of file diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/di/RabbitMQFactoryImpl.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/di/RabbitMQFactoryImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..3103acc8e88eca93f51397fc921499de1fc11c35 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/di/RabbitMQFactoryImpl.java @@ -0,0 +1,87 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.di; + +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import java.io.IOException; +import java.net.URISyntaxException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.concurrent.TimeoutException; +import javax.annotation.PostConstruct; +import org.opengroup.osdu.indexer.config.RabbitMqConfigProperties; +import org.opengroup.osdu.indexer.messagebus.IMessageFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class RabbitMQFactoryImpl implements IMessageFactory { + + private static final Logger LOG = LoggerFactory.getLogger(RabbitMQFactoryImpl.class); + private final RabbitMqConfigProperties rabbitMqConfigProperties; + + private Channel channel; + + @Autowired + public RabbitMQFactoryImpl(RabbitMqConfigProperties rabbitMqConfigProperties) { + this.rabbitMqConfigProperties = rabbitMqConfigProperties; + } + + @PostConstruct + private void init() { + ConnectionFactory factory = new ConnectionFactory(); + try { + String uri = rabbitMqConfigProperties.getMbRabbitMqUri(); + LOG.debug(String.format("RabbitMQ Channel = %s", uri)); + factory.setUri(uri); + factory.setAutomaticRecoveryEnabled(true); + Connection conn = factory.newConnection(); + this.channel = conn.createChannel(); + LOG.debug("RabbitMQ Channel was created."); + for (String queue : Arrays.asList(DEFAULT_QUEUE_NAME, INDEXER_QUEUE_NAME, LEGAL_QUEUE_NAME)) { + channel.queueDeclare(queue, true, false, false, null); + LOG.debug(String.format("Queue [%s] was declared.", queue)); + } + } catch (KeyManagementException | NoSuchAlgorithmException | URISyntaxException | IOException | TimeoutException e) { + LOG.error(e.getMessage(), e); + } + + } + + @Override + public void sendMessage(String msg) { + this.sendMessage("records", msg); + } + + @Override + public void sendMessage(String queueName, String msg) { + String queueNameWithPrefix = queueName; + try { + channel.basicPublish("", queueNameWithPrefix, null, msg.getBytes()); + LOG.info(String.format("[x] Sent '%s' to queue [%s]", msg, queueNameWithPrefix)); + } catch (IOException e) { + LOG.error(String.format("Unable to publish message to [%s]", queueNameWithPrefix)); + LOG.error(e.getMessage(), e); + } + } +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/di/TenantFactoryImpl.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/di/TenantFactoryImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..b60b6e590e5ec671110e74fa49949391c219ce23 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/di/TenantFactoryImpl.java @@ -0,0 +1,102 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.di; + + +import com.google.gson.Gson; +import com.mongodb.client.FindIterable; +import com.mongodb.client.MongoCollection; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import org.bson.Document; +import org.opengroup.osdu.core.common.cache.ICache; +import org.opengroup.osdu.core.common.model.tenant.TenantInfo; +import org.opengroup.osdu.core.common.provider.interfaces.ITenantFactory; +import org.opengroup.osdu.indexer.persistence.MongoDdmsClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; + +@Primary +@Component +public class TenantFactoryImpl implements ITenantFactory { + + private static final Logger LOG = LoggerFactory.getLogger(TenantFactoryImpl.class); + + public static final String MAIN_DATABASE = "main"; + public static final String TENANT_INFO = "tenantinfo"; + + + private final MongoDdmsClient mongoClient; + + @Autowired + public TenantFactoryImpl(MongoDdmsClient mongoClient) { + this.mongoClient = mongoClient; + } + + private Map<String, TenantInfo> tenants; + + public boolean exists(String tenantName) { + if (this.tenants == null) { + initTenants(); + } + return this.tenants.containsKey(tenantName); + } + + public TenantInfo getTenantInfo(String tenantName) { + if (this.tenants == null) { + initTenants(); + } + return this.tenants.get(tenantName); + } + + public Collection<TenantInfo> listTenantInfo() { + if (this.tenants == null) { + initTenants(); + } + return this.tenants.values(); + } + + public <V> ICache<String, V> createCache(String tenantName, String host, int port, + int expireTimeSeconds, Class<V> classOfV) { + return null; + } + + public void flushCache() { + } + + private void initTenants() { + this.tenants = new HashMap<>(); + MongoCollection<Document> mongoCollection = mongoClient + .getMongoCollection(MAIN_DATABASE, TENANT_INFO); + FindIterable<Document> results = mongoCollection.find(); + if (Objects.isNull(results) && Objects.isNull(results.first())) { + LOG.error(String.format("Collection \'%s\' is empty.", results)); + } + for (Document document : results) { + TenantInfo tenantInfo = new Gson().fromJson(document.toJson(), TenantInfo.class); + this.tenants.put(tenantInfo.getName(), tenantInfo); + } + } + +} + diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/messagebus/IMessageFactory.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/messagebus/IMessageFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..21316cc68c9d5fa64615a8616f3f6a0e533f8ed4 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/messagebus/IMessageFactory.java @@ -0,0 +1,28 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.messagebus; + +public interface IMessageFactory { + String DEFAULT_QUEUE_NAME = "records"; + String LEGAL_QUEUE_NAME = "legal"; + String INDEXER_QUEUE_NAME = "indexer"; + + void sendMessage(String msg); + + void sendMessage(String queueName, String msg); +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/middleware/IndexFilter.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/middleware/IndexFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..12cffcf01340bf7c7ce763e2ba7a19202ed1a8e6 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/middleware/IndexFilter.java @@ -0,0 +1,127 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.middleware; + +import com.google.common.base.Strings; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.extern.java.Log; +import org.apache.http.HttpStatus; +import org.opengroup.osdu.core.common.http.ResponseHeaders; +import org.opengroup.osdu.core.common.model.http.AppException; +import org.opengroup.osdu.core.common.model.http.DpsHeaders; +import org.opengroup.osdu.core.common.model.search.DeploymentEnvironment; +import org.opengroup.osdu.core.common.provider.interfaces.IRequestInfo; +import org.opengroup.osdu.indexer.config.IndexerConfigurationProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; + +@Log +@Component +public class IndexFilter implements Filter { + + + private final IndexerConfigurationProperties indexerConfigurationProperties; + private final DpsHeaders dpsHeaders; + private final IRequestInfo requestInfo; + + private FilterConfig filterConfig; + + private static final String PATH_SWAGGER = "/swagger.json"; + private static final String PATH_TASK_HANDLERS = "task-handlers"; + private static final String PATH_CRON_HANDLERS = "cron-handlers"; + + @Autowired + public IndexFilter(IndexerConfigurationProperties indexerConfigurationProperties, + DpsHeaders dpsHeaders, IRequestInfo requestInfo) { + this.indexerConfigurationProperties = indexerConfigurationProperties; + this.dpsHeaders = dpsHeaders; + this.requestInfo = requestInfo; + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, + FilterChain filterChain) + throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) servletRequest; + String uri = httpRequest.getRequestURI().toLowerCase(); + + if (httpRequest.getMethod().equalsIgnoreCase(HttpMethod.POST.name()) && uri + .contains(PATH_TASK_HANDLERS)) { + if (DeploymentEnvironment.valueOf(indexerConfigurationProperties.getEnvironment()) + != DeploymentEnvironment.LOCAL) { + checkWorkerApiAccess(requestInfo); + } + } + + if (httpRequest.getMethod().equalsIgnoreCase(HttpMethod.GET.name()) && uri + .contains(PATH_CRON_HANDLERS)) { + checkWorkerApiAccess(requestInfo); + } + + filterChain.doFilter(servletRequest, servletResponse); + + HttpServletResponse httpResponse = (HttpServletResponse) servletResponse; + Map<String, List<Object>> standardHeaders = ResponseHeaders.STANDARD_RESPONSE_HEADERS; + for (Map.Entry<String, List<Object>> header : standardHeaders.entrySet()) { + httpResponse.addHeader(header.getKey(), header.getValue().toString()); + } + if (httpResponse.getHeader(DpsHeaders.CORRELATION_ID) == null) { + httpResponse.addHeader(DpsHeaders.CORRELATION_ID, dpsHeaders.getCorrelationId()); + } + } + + @Override + public void destroy() { + } + + private void checkWorkerApiAccess(IRequestInfo requestInfo) { + } + + private List<String> validateAccountId(DpsHeaders requestHeaders) { + String accountHeader = requestHeaders.getPartitionIdWithFallbackToAccountId(); + String debuggingInfo = String.format("%s:%s", DpsHeaders.DATA_PARTITION_ID, accountHeader); + + if (Strings.isNullOrEmpty(accountHeader)) { + throw new AppException(HttpStatus.SC_BAD_REQUEST, "Bad request", + "invalid or empty data partition", debuggingInfo); + } + + List<String> dataPartitions = Arrays.asList(accountHeader.trim().split("\\s*,\\s*")); + if (dataPartitions.isEmpty() || dataPartitions.size() > 1) { + throw new AppException(HttpStatus.SC_BAD_REQUEST, "Bad request", + "invalid or empty data partition", debuggingInfo); + } + return dataPartitions; + } +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/persistence/DatastoreCredential.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/persistence/DatastoreCredential.java new file mode 100644 index 0000000000000000000000000000000000000000..9b7f8b1f5edd751f35dcd850ec57073c594f838f --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/persistence/DatastoreCredential.java @@ -0,0 +1,123 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.persistence; + +import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.services.iam.v1.Iam; +import com.google.api.services.iam.v1.Iam.Projects.ServiceAccounts.SignJwt; +import com.google.api.services.iam.v1.IamScopes; +import com.google.api.services.iam.v1.model.SignJwtRequest; +import com.google.api.services.iam.v1.model.SignJwtResponse; +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.gson.JsonObject; +import java.util.Collections; +import java.util.Date; +import org.apache.commons.lang3.time.DateUtils; +import org.opengroup.osdu.core.common.model.tenant.TenantInfo; +import org.opengroup.osdu.core.common.util.Crc32c; +import org.opengroup.osdu.indexer.cache.DatastoreCredentialCache; + +public class DatastoreCredential extends GoogleCredentials { + + private static final long serialVersionUID = 8344377091688956815L; + private static final JsonFactory JSON_FACTORY = new JacksonFactory(); + private Iam iam; + + private final TenantInfo tenant; + private final DatastoreCredentialCache cache; + + protected DatastoreCredential(TenantInfo tenant, DatastoreCredentialCache cache) { + this.tenant = tenant; + this.cache = cache; + } + + @Override + public AccessToken refreshAccessToken() { + + String cacheKey = this.getCacheKey(); + + AccessToken accessToken = this.cache.get(cacheKey); + + if (accessToken != null) { + return accessToken; + } + + try { + SignJwtRequest signJwtRequest = new SignJwtRequest(); + signJwtRequest.setPayload(this.getPayload()); + + String serviceAccountName = String + .format("projects/-/serviceAccounts/%s", this.tenant.getServiceAccount()); + + SignJwt signJwt = this.getIam().projects().serviceAccounts() + .signJwt(serviceAccountName, signJwtRequest); + + SignJwtResponse signJwtResponse = signJwt.execute(); + String signedJwt = signJwtResponse.getSignedJwt(); + + accessToken = new AccessToken(signedJwt, DateUtils.addSeconds(new Date(), 3600)); + + this.cache.put(cacheKey, accessToken); + + return accessToken; + } catch (Exception e) { + throw new RuntimeException("Error creating datastore credential", e); + } + } + + private String getPayload() { + JsonObject payload = new JsonObject(); + payload.addProperty("iss", this.tenant.getServiceAccount()); + payload.addProperty("sub", this.tenant.getServiceAccount()); + payload.addProperty("aud", "https://datastore.googleapis.com/google.datastore.v1.Datastore"); + payload.addProperty("iat", System.currentTimeMillis() / 1000); + + return payload.toString(); + } + + protected void setIam(Iam iam) { + this.iam = iam; + } + + private Iam getIam() throws Exception { + if (this.iam == null) { + + GoogleCredential credential = GoogleCredential.getApplicationDefault(); + if (credential.createScopedRequired()) { + credential = credential.createScoped(Collections.singletonList(IamScopes.CLOUD_PLATFORM)); + } + + Iam.Builder builder = new Iam.Builder(GoogleNetHttpTransport.newTrustedTransport(), + JSON_FACTORY, + credential).setApplicationName("Search Service"); + + this.iam = builder.build(); + } + return this.iam; + } + + + private String getCacheKey() { + return Crc32c + .hashToBase64EncodedString(String.format("datastoreCredential:%s", this.tenant.getName())); + } +} \ No newline at end of file diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/persistence/DatastoreFactory.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/persistence/DatastoreFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..144edee0eb4c1bd99054bd1a1859588adb9be3ae --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/persistence/DatastoreFactory.java @@ -0,0 +1,75 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.persistence; + +import com.google.api.gax.retrying.RetrySettings; +import com.google.cloud.TransportOptions; +import com.google.cloud.datastore.Datastore; +import com.google.cloud.datastore.DatastoreOptions; +import com.google.cloud.http.HttpTransportOptions; +import java.util.HashMap; +import java.util.Map; +import javax.inject.Inject; +import org.opengroup.osdu.core.common.model.tenant.TenantInfo; +import org.opengroup.osdu.indexer.cache.DatastoreCredentialCache; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.threeten.bp.Duration; + +@Component +public class DatastoreFactory { + + @Inject + private DatastoreCredentialCache cache; + + @Autowired + public DatastoreFactory(DatastoreCredentialCache cache) { + this.cache = cache; + } + + private static Map<String, Datastore> DATASTORE_CLIENTS = new HashMap<>(); + + private static final RetrySettings RETRY_SETTINGS = RetrySettings.newBuilder() + .setMaxAttempts(6) + .setInitialRetryDelay(Duration.ofSeconds(10)) + .setMaxRetryDelay(Duration.ofSeconds(32)) + .setRetryDelayMultiplier(2.0) + .setTotalTimeout(Duration.ofSeconds(50)) + .setInitialRpcTimeout(Duration.ofSeconds(50)) + .setRpcTimeoutMultiplier(1.0) + .setMaxRpcTimeout(Duration.ofSeconds(50)) + .build(); + + private static final TransportOptions TRANSPORT_OPTIONS = HttpTransportOptions.newBuilder() + .setReadTimeout(30000) + .build(); + + public Datastore getDatastoreInstance(TenantInfo tenantInfo) { + if (DATASTORE_CLIENTS.get(tenantInfo.getName()) == null) { + Datastore googleDatastore = DatastoreOptions.newBuilder() + .setCredentials(new DatastoreCredential(tenantInfo, this.cache)) + .setRetrySettings(RETRY_SETTINGS) + .setTransportOptions(TRANSPORT_OPTIONS) + .setNamespace(tenantInfo.getName()) + .setProjectId(tenantInfo.getProjectId()) + .build().getService(); + DATASTORE_CLIENTS.put(tenantInfo.getName(), googleDatastore); + } + return DATASTORE_CLIENTS.get(tenantInfo.getName()); + } +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/persistence/ElasticRepositoryMongoDB.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/persistence/ElasticRepositoryMongoDB.java new file mode 100644 index 0000000000000000000000000000000000000000..1c4d4fba54520a6fe22fc1d32c129dda80309432 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/persistence/ElasticRepositoryMongoDB.java @@ -0,0 +1,87 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.persistence; + +import com.mongodb.client.FindIterable; +import com.mongodb.client.MongoCollection; +import java.util.Objects; +import org.apache.http.HttpStatus; +import org.bson.Document; +import org.opengroup.osdu.core.common.model.http.AppException; +import org.opengroup.osdu.core.common.model.search.ClusterSettings; +import org.opengroup.osdu.core.common.model.tenant.TenantInfo; +import org.opengroup.osdu.core.common.provider.interfaces.IElasticRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class ElasticRepositoryMongoDB implements IElasticRepository { + + private static final Logger LOG = LoggerFactory.getLogger(ElasticRepositoryMongoDB.class); + + private static final String SCHEMA_DATABASE = "local"; + private static final String SEARCH_SETTINGS = "SearchSettings"; + private static final String HOST = "host"; + private static final String PORT = "port"; + private static final String XPACK_RESTCLIENT_CONFIGURATION = "configuration"; + + private final MongoDdmsClient mongoClient; + + @Autowired + public ElasticRepositoryMongoDB(MongoDdmsClient mongoClient) { + this.mongoClient = mongoClient; + } + + @Override + public ClusterSettings getElasticClusterSettings(TenantInfo tenantInfo) { + MongoCollection<Document> mongoCollection = mongoClient + .getMongoCollection(SCHEMA_DATABASE, SEARCH_SETTINGS); + + FindIterable<Document> results = mongoCollection.find(); + + if (Objects.isNull(results) && Objects.isNull(results.first())) { + LOG.error(String.format("Collection \'%s\' is empty.", results)); + throw new AppException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Cluster setting fetch error", + "An error has occurred fetching cluster settings from the database."); + } + + Document document = results.first(); + + String encryptedConfiguration = document.get(XPACK_RESTCLIENT_CONFIGURATION).toString(); + String encryptedHost = document.get(HOST).toString(); + String encryptedPort = document.get(PORT).toString(); + + try { + + String host = encryptedHost;//this.kmsClient.decryptString(encryptedHost); + String portString = encryptedPort;//this.kmsClient.decryptString(encryptedPort); + String usernameAndPassword = encryptedConfiguration;//this.kmsClient.decryptString(encryptedConfiguration); + + int port = Integer.parseInt(portString); + ClusterSettings clusterSettings = new ClusterSettings(host, port, usernameAndPassword); + clusterSettings.setHttps(false); + return clusterSettings; + + } catch (Exception e) { + throw new AppException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Cluster setting fetch error", + "An error has occurred fetching cluster settings from the database.", e); + } + } +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/persistence/MongoDdmsClient.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/persistence/MongoDdmsClient.java new file mode 100644 index 0000000000000000000000000000000000000000..6cb02a3a7fbfb36d6a2ada3f3b3906a1116aaaa4 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/persistence/MongoDdmsClient.java @@ -0,0 +1,41 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.persistence; + +import com.mongodb.client.MongoCollection; +import org.bson.Document; +import org.opengroup.osdu.core.common.model.tenant.TenantInfo; +import org.opengroup.osdu.indexer.util.MongoClientHandler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class MongoDdmsClient { + + private final MongoClientHandler mongoClientHandler; + + @Autowired + public MongoDdmsClient(MongoClientHandler mongoClientHandler) { + this.mongoClientHandler = mongoClientHandler; + } + + public MongoCollection<Document> getMongoCollection(String dbName, String collectionName) { + return mongoClientHandler.getMongoClient().getDatabase(dbName) + .getCollection(collectionName); + } +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/publish/PublisherImpl.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/publish/PublisherImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a9a82d8cffb4ea3b86d56d66e210f00575f0a034 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/publish/PublisherImpl.java @@ -0,0 +1,109 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.publish; + + +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.elasticsearch.common.Strings; +import org.opengroup.osdu.core.common.model.http.DpsHeaders; +import org.opengroup.osdu.core.common.model.indexer.JobStatus; +import org.opengroup.osdu.core.common.model.indexer.RecordStatus; +import org.opengroup.osdu.core.common.model.search.RecordChangedMessages; +import org.opengroup.osdu.indexer.messagebus.IMessageFactory; +import org.opengroup.osdu.indexer.provider.interfaces.IPublisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.context.annotation.RequestScope; + +@Component +@RequestScope +public class PublisherImpl implements IPublisher { + + private static final Logger LOG = LoggerFactory.getLogger(PublisherImpl.class); + private final IMessageFactory mq; + + @Autowired + public PublisherImpl(IMessageFactory mq) { + this.mq = mq; + } + + @Override + public void publishStatusChangedTagsToTopic(DpsHeaders headers, JobStatus indexerBatchStatus) { + + String tenant = headers.getPartitionId(); + if (Strings.isNullOrEmpty(tenant)) { + tenant = headers.getAccountId(); + } + + Map<String, String> message = new HashMap<>(); + message.put(tenant, headers.getPartitionIdWithFallbackToAccountId()); + headers.addCorrelationIdIfMissing(); + message.put(DpsHeaders.CORRELATION_ID, headers.getCorrelationId()); + + RecordChangedMessages recordChangedMessages = getRecordChangedMessage(headers, + indexerBatchStatus); + message.put("data", recordChangedMessages.toString()); + + try { + LOG.info("Indexer publishes message " + headers.getCorrelationId()); + mq.sendMessage(IMessageFactory.INDEXER_QUEUE_NAME, message.toString()); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } + } + + private RecordChangedMessages getRecordChangedMessage(DpsHeaders headers, + JobStatus indexerBatchStatus) { + + Gson gson = new GsonBuilder().create(); + Map<String, String> attributesMap = new HashMap<>(); + Type listType = new TypeToken<List<RecordStatus>>() { + }.getType(); + + JsonElement statusChangedTagsJson = gson + .toJsonTree(indexerBatchStatus.getStatusesList(), listType); + String statusChangedTagsData = (statusChangedTagsJson.toString()); + + String tenant = headers.getPartitionId(); + // This code it to provide backward compatibility to slb-account-id + if (!Strings.isNullOrEmpty(tenant)) { + attributesMap + .put(DpsHeaders.DATA_PARTITION_ID, headers.getPartitionIdWithFallbackToAccountId()); + } else { + attributesMap.put(DpsHeaders.ACCOUNT_ID, headers.getPartitionIdWithFallbackToAccountId()); + } + headers.addCorrelationIdIfMissing(); + attributesMap.put(DpsHeaders.CORRELATION_ID, headers.getCorrelationId()); + + RecordChangedMessages recordChangedMessages = new RecordChangedMessages(); + // statusChangedTagsData is not ByteString but String + recordChangedMessages.setData(statusChangedTagsData); + recordChangedMessages.setAttributes(attributesMap); + + return recordChangedMessages; + } +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/security/GSuiteSecurityConfig.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/security/GSuiteSecurityConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..0db2f9e557efcc69fbd0419df57fc3a821e024de --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/security/GSuiteSecurityConfig.java @@ -0,0 +1,44 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.security; + +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class GSuiteSecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.httpBasic().disable() + .csrf().disable(); + } + + @Override + public void configure(WebSecurity web) throws Exception { + web.ignoring().antMatchers("/api-docs") + .antMatchers("/swagger"); + } + +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/service/IndexerServiceImpl.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/service/IndexerServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..558e30bcc6d7c6653f51f1d023a56b6f015a5c18 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/service/IndexerServiceImpl.java @@ -0,0 +1,614 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.service; + +import com.google.gson.Gson; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import javax.inject.Inject; +import org.apache.http.HttpStatus; +import org.elasticsearch.ElasticsearchStatusException; +import org.elasticsearch.action.bulk.BulkItemResponse; +import org.elasticsearch.action.bulk.BulkRequest; +import org.elasticsearch.action.bulk.BulkResponse; +import org.elasticsearch.action.delete.DeleteRequest; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.update.UpdateRequest; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.RestHighLevelClient; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.rest.RestStatus; +import org.opengroup.osdu.core.common.Constants; +import org.opengroup.osdu.core.common.logging.JaxRsDpsLog; +import org.opengroup.osdu.core.common.model.entitlements.Acl; +import org.opengroup.osdu.core.common.model.http.AppException; +import org.opengroup.osdu.core.common.model.http.DpsHeaders; +import org.opengroup.osdu.core.common.model.http.RequestStatus; +import org.opengroup.osdu.core.common.model.indexer.IndexSchema; +import org.opengroup.osdu.core.common.model.indexer.IndexingStatus; +import org.opengroup.osdu.core.common.model.indexer.JobStatus; +import org.opengroup.osdu.core.common.model.indexer.OperationType; +import org.opengroup.osdu.core.common.model.indexer.RecordIndexerPayload; +import org.opengroup.osdu.core.common.model.indexer.RecordInfo; +import org.opengroup.osdu.core.common.model.indexer.RecordStatus; +import org.opengroup.osdu.core.common.model.indexer.Records; +import org.opengroup.osdu.core.common.model.search.RecordChangedMessages; +import org.opengroup.osdu.core.common.model.search.RecordMetaAttribute; +import org.opengroup.osdu.core.common.provider.interfaces.IRequestInfo; +import org.opengroup.osdu.core.common.search.ElasticIndexNameResolver; +import org.opengroup.osdu.core.common.search.IndicesService; +import org.opengroup.osdu.indexer.logging.AuditLogger; +import org.opengroup.osdu.indexer.provider.interfaces.IPublisher; +import org.opengroup.osdu.indexer.util.ElasticClientHandler; +import org.opengroup.osdu.indexer.util.IndexerQueueTaskBuilder; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Service; + +@Service +@Primary +public class IndexerServiceImpl implements IndexerService { + + private static final TimeValue BULK_REQUEST_TIMEOUT = TimeValue.timeValueMinutes(1); + + private static final List<RestStatus> RETRY_ELASTIC_EXCEPTION = new ArrayList<>(Arrays + .asList(RestStatus.TOO_MANY_REQUESTS, RestStatus.BAD_GATEWAY, + RestStatus.SERVICE_UNAVAILABLE)); + + private final Gson gson = new Gson(); + + @Inject + private JaxRsDpsLog jaxRsDpsLog; + @Inject + private AuditLogger auditLogger; + @Inject + private StorageService storageService; + @Inject + private IndexSchemaService schemaService; + @Inject + private IndicesService indicesService; + @Inject + private IndexerMappingService mappingService; + @Inject + private IPublisher progressPublisher; + @Inject + private ElasticClientHandler elasticClientHandler; + @Inject + private IndexerQueueTaskBuilder indexerQueueTaskBuilder; + @Inject + private ElasticIndexNameResolver elasticIndexNameResolver; + @Inject + private StorageIndexerPayloadMapper storageIndexerPayloadMapper; + @Inject + private IRequestInfo requestInfo; + @Inject + private JobStatus jobStatus; + + private DpsHeaders headers; + + @Override + public JobStatus processRecordChangedMessages(RecordChangedMessages message, + List<RecordInfo> recordInfos) throws Exception { + + // this should not happen + if (recordInfos.size() == 0) { + return null; + } + + String errorMessage = ""; + List<String> retryRecordIds = new LinkedList<>(); + + // get auth header with service account Authorization + this.headers = this.requestInfo.getHeaders(); + String authorization = message.getAttributes().get("authorization"); + headers.put("authorization", authorization); + // initialize status for all messages. + this.jobStatus.initialize(recordInfos); + + try { + // get upsert records + Map<String, Map<String, OperationType>> upsertRecordMap = RecordInfo + .getUpsertRecordIds(recordInfos); + if (upsertRecordMap != null && !upsertRecordMap.isEmpty()) { + List<String> upsertFailureRecordIds = processUpsertRecords(upsertRecordMap); + retryRecordIds.addAll(upsertFailureRecordIds); + } + + // get delete records + Map<String, List<String>> deleteRecordMap = RecordInfo.getDeleteRecordIds(recordInfos); + if (deleteRecordMap != null && !deleteRecordMap.isEmpty()) { + List<String> deleteFailureRecordIds = processDeleteRecords(deleteRecordMap); + retryRecordIds.addAll(deleteFailureRecordIds); + } + + // process schema change messages + Map<String, OperationType> schemaMsgs = RecordInfo.getSchemaMsgs(recordInfos); + if (schemaMsgs != null && !schemaMsgs.isEmpty()) { + this.schemaService.processSchemaMessages(schemaMsgs); + } + + // process failed records + if (retryRecordIds.size() > 0) { + retryAndEnqueueFailedRecords(recordInfos, retryRecordIds, message); + } + } catch (IOException e) { + errorMessage = e.getMessage(); + throw new AppException(HttpStatus.SC_GATEWAY_TIMEOUT, "Internal communication failure", + errorMessage, e); + } catch (AppException e) { + errorMessage = e.getMessage(); + throw e; + } catch (Exception e) { + errorMessage = "error indexing records"; + throw new AppException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Unknown error", + "An unknown error has occurred.", e); + } finally { + this.jobStatus.finalizeRecordStatus(errorMessage); + this.updateAuditLog(); + this.progressPublisher.publishStatusChangedTagsToTopic(this.headers, this.jobStatus); + } + + return jobStatus; + } + + @Override + public void processSchemaMessages(List<RecordInfo> recordInfos) throws IOException { + Map<String, OperationType> schemaMsgs = RecordInfo.getSchemaMsgs(recordInfos); + if (schemaMsgs != null && !schemaMsgs.isEmpty()) { + try (RestHighLevelClient restClient = elasticClientHandler.createRestClient()) { + schemaMsgs.entrySet().forEach(msg -> { + try { + processSchemaEvents(restClient, msg); + } catch (IOException | ElasticsearchStatusException e) { + throw new AppException( + org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR.value(), + "unable to process schema delete", e.getMessage()); + } + }); + } + } + } + + private void processSchemaEvents(RestHighLevelClient restClient, + Map.Entry<String, OperationType> msg) throws IOException, ElasticsearchStatusException { + String kind = msg.getKey(); + String index = elasticIndexNameResolver.getIndexNameFromKind(kind); + + boolean indexExist = indicesService.isIndexExist(restClient, index); + if (indexExist && msg.getValue() == OperationType.purge_schema) { + indicesService.deleteIndex(restClient, index); + } + } + + private List<String> processUpsertRecords(Map<String, Map<String, OperationType>> upsertRecordMap) + throws Exception { + // get schema for kind + Map<String, IndexSchema> schemas = this.getSchema(upsertRecordMap); + + if (schemas.isEmpty()) { + return new LinkedList<>(); + } + + // get recordIds with valid upsert index-status + List<String> recordIds = this.jobStatus.getIdsByValidUpsertIndexingStatus(); + + if (recordIds.isEmpty()) { + return new LinkedList<>(); + } + + // get records via storage api + Records storageRecords = this.storageService.getStorageRecords(recordIds); + List<String> failedOrRetryRecordIds = new LinkedList<>(storageRecords.getMissingRetryRecords()); + + // map storage records to indexer payload + RecordIndexerPayload recordIndexerPayload = this + .getIndexerPayload(upsertRecordMap, schemas, storageRecords); + + jaxRsDpsLog.info(String.format( + "records change messages received : %s | valid storage bulk records: %s | valid index payload: %s", + recordIds.size(), storageRecords.getRecords().size(), + recordIndexerPayload.getRecords().size())); + + // index records + failedOrRetryRecordIds.addAll(processElasticMappingAndUpsertRecords(recordIndexerPayload)); + + return failedOrRetryRecordIds; + } + + private Map<String, IndexSchema> getSchema( + Map<String, Map<String, OperationType>> upsertRecordMap) { + + Map<String, IndexSchema> schemas = new HashMap<>(); + + try { + for (Map.Entry<String, Map<String, OperationType>> entry : upsertRecordMap.entrySet()) { + + String kind = entry.getKey(); + IndexSchema schemaObj = this.schemaService.getIndexerInputSchema(kind, false); + if (schemaObj.isDataSchemaMissing()) { + this.jobStatus.addOrUpdateRecordStatus(entry.getValue().keySet(), IndexingStatus.WARN, + HttpStatus.SC_NOT_FOUND, "schema not found", + String.format("schema not found | kind: %s", kind)); + } + + schemas.put(kind, schemaObj); + } + } catch (AppException e) { + throw e; + } catch (Exception e) { + throw new AppException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Get schema error", + "An error has occurred while getting schema", e); + } + + return schemas; + } + + private RecordIndexerPayload getIndexerPayload( + Map<String, Map<String, OperationType>> upsertRecordMap, + Map<String, IndexSchema> kindSchemaMap, Records records) { + List<Records.Entity> storageValidRecords = records.getRecords(); + List<RecordIndexerPayload.Record> indexerPayload = new ArrayList<>(); + List<IndexSchema> schemas = new ArrayList<>(); + + for (Records.Entity storageRecord : storageValidRecords) { + + Map<String, OperationType> idOperationMap = upsertRecordMap.get(storageRecord.getKind()); + + // skip if storage returned record with same id but different kind + if (idOperationMap == null) { + String message = String.format( + "storage service returned incorrect record | requested kind: %s | received kind: %s", + this.jobStatus.getRecordKindById(storageRecord.getId()), storageRecord.getKind()); + this.jobStatus.addOrUpdateRecordStatus(storageRecord.getId(), IndexingStatus.SKIP, + RequestStatus.STORAGE_CONFLICT, message, + String.format("%s | record-id: %s", message, storageRecord.getId())); + continue; + } + + IndexSchema schema = kindSchemaMap.get(storageRecord.getKind()); + schemas.add(schema); + + // skip indexing of records if data block is empty + RecordIndexerPayload.Record document = prepareIndexerPayload(schema, storageRecord, + idOperationMap); + if (document != null) { + indexerPayload.add(document); + } + } + + // this should only happen if storage service returned WRONG records with kind for all the records in the messages + if (indexerPayload.isEmpty()) { + throw new AppException(RequestStatus.STORAGE_CONFLICT, "Indexer error", + "upsert record failed, storage service returned incorrect records"); + } + + return RecordIndexerPayload.builder().records(indexerPayload).schemas(schemas).build(); + } + + private RecordIndexerPayload.Record prepareIndexerPayload(IndexSchema schemaObj, + Records.Entity storageRecord, Map<String, OperationType> idToOperationMap) { + + RecordIndexerPayload.Record document = null; + + try { + Map<String, Object> storageRecordData = storageRecord.getData(); + document = new RecordIndexerPayload.Record(); + if (storageRecordData == null || storageRecordData.isEmpty()) { + String message = "empty or null data block found in the storage record"; + this.jobStatus.addOrUpdateRecordStatus(storageRecord.getId(), IndexingStatus.WARN, + HttpStatus.SC_NOT_FOUND, message, + String.format("record-id: %s | %s", storageRecord.getId(), message)); + } else if (schemaObj.isDataSchemaMissing()) { + document.setSchemaMissing(true); + } else { + Map<String, Object> dataMap = this.storageIndexerPayloadMapper + .mapDataPayload(schemaObj, storageRecordData, storageRecord.getId()); + if (dataMap.isEmpty()) { + document.setMappingMismatch(true); + String message = String.format( + "complete schema mismatch: none of the data attribute can be mapped | data: %s", + storageRecordData); + this.jobStatus.addOrUpdateRecordStatus(storageRecord.getId(), IndexingStatus.WARN, + HttpStatus.SC_NOT_FOUND, message, + String.format("record-id: %s | %s", storageRecord.getId(), message)); + } + document.setData(dataMap); + } + } catch (AppException e) { + this.jobStatus.addOrUpdateRecordStatus(storageRecord.getId(), IndexingStatus.FAIL, + HttpStatus.SC_INTERNAL_SERVER_ERROR, e.getMessage()); + jaxRsDpsLog + .warning(String.format("record-id: %s | %s", storageRecord.getId(), e.getMessage()), e); + } catch (Exception e) { + this.jobStatus.addOrUpdateRecordStatus(storageRecord.getId(), IndexingStatus.FAIL, + HttpStatus.SC_INTERNAL_SERVER_ERROR, + String.format("error parsing records against schema, error-message: %s", e.getMessage())); + jaxRsDpsLog.error(String + .format("record-id: %s | error parsing records against schema, error-message: %s", + storageRecord.getId(), e.getMessage()), e); + } + + try { + // index individual parts of kind + String[] kindParts = storageRecord.getKind().split(":"); + + document.setKind(storageRecord.getKind()); + document.setNamespace(kindParts[0] + ":" + kindParts[1]); + document.setType(kindParts[2]); + document.setId(storageRecord.getId()); + document.setVersion(storageRecord.getVersion()); + document.setAcl(storageRecord.getAcl()); + document.setLegal(storageRecord.getLegal()); + RecordStatus recordStatus = this.jobStatus.getJobStatusByRecordId(storageRecord.getId()); + if (recordStatus.getIndexProgress().getStatusCode() == 0) { + recordStatus.getIndexProgress().setStatusCode(HttpStatus.SC_OK); + } + document.setIndexProgress(recordStatus.getIndexProgress()); + if (storageRecord.getAncestry() != null) { + document.setAncestry(storageRecord.getAncestry()); + } + document.setOperationType(idToOperationMap.get(storageRecord.getId())); + } catch (Exception e) { + this.jobStatus.addOrUpdateRecordStatus(storageRecord.getId(), IndexingStatus.FAIL, + HttpStatus.SC_INTERNAL_SERVER_ERROR, + String.format("error parsing meta data, error-message: %s", e.getMessage())); + jaxRsDpsLog.error(String.format("record-id: %s | error parsing meta data, error-message: %s", + storageRecord.getId(), e.getMessage()), e); + } + return document; + } + + private List<String> processElasticMappingAndUpsertRecords( + RecordIndexerPayload recordIndexerPayload) throws Exception { + + try (RestHighLevelClient restClient = this.elasticClientHandler.createRestClient()) { + List<IndexSchema> schemas = recordIndexerPayload.getSchemas(); + if (schemas == null || schemas.isEmpty()) { + return new LinkedList<>(); + } + + // process the schema + this.cacheOrCreateElasticMapping(schemas, restClient); + + // process the records + return this.upsertRecords(recordIndexerPayload.getRecords(), restClient); + } + } + + private void cacheOrCreateElasticMapping(List<IndexSchema> schemas, + RestHighLevelClient restClient) throws Exception { + + for (IndexSchema schema : schemas) { + String index = this.elasticIndexNameResolver.getIndexNameFromKind(schema.getKind()); + + // check if index exist + if (this.indicesService.isIndexExist(restClient, index)) { + continue; + } + + // create index + Map<String, Object> mapping = this.mappingService.getIndexMappingFromRecordSchema(schema); + if (!this.indicesService.createIndex(restClient, index, null, schema.getType(), mapping)) { + throw new AppException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Elastic error", + "Error creating index.", + String.format("Failed to get confirmation from elastic server for index: %s", index)); + } + } + } + + private List<String> upsertRecords(List<RecordIndexerPayload.Record> records, + RestHighLevelClient restClient) throws AppException { + if (records == null || records.isEmpty()) { + return new LinkedList<>(); + } + + BulkRequest bulkRequest = new BulkRequest(); + bulkRequest.timeout(BULK_REQUEST_TIMEOUT); + + for (RecordIndexerPayload.Record record : records) { + if ((record.getData() == null || record.getData().isEmpty()) && !record + .skippedDataIndexing()) { + // it will come here when schema is missing + // TODO: rollback once we know what is causing the problem + jaxRsDpsLog.warning(String.format("data not found for record: %s", record)); + } + + OperationType operation = record.getOperationType(); + Map<String, Object> sourceMap = getSourceMap(record); + String index = this.elasticIndexNameResolver.getIndexNameFromKind(record.getKind()); + + if (operation == OperationType.create) { + IndexRequest indexRequest = new IndexRequest(index, record.getType(), record.getId()) + .source(this.gson.toJson(sourceMap), XContentType.JSON); + bulkRequest.add(indexRequest); + } else if (operation == OperationType.update) { + UpdateRequest updateRequest = new UpdateRequest(index, record.getType(), record.getId()) + .upsert(this.gson.toJson(sourceMap), XContentType.JSON); + bulkRequest.add(updateRequest); + } + } + + return processBulkRequest(restClient, bulkRequest); + } + + private List<String> processDeleteRecords(Map<String, List<String>> deleteRecordMap) + throws Exception { + BulkRequest bulkRequest = new BulkRequest(); + bulkRequest.timeout(BULK_REQUEST_TIMEOUT); + + for (Map.Entry<String, List<String>> record : deleteRecordMap.entrySet()) { + + String[] kindParts = record.getKey().split(":"); + String type = kindParts[2]; + + String index = this.elasticIndexNameResolver.getIndexNameFromKind(record.getKey()); + + for (String id : record.getValue()) { + DeleteRequest deleteRequest = new DeleteRequest(index, type, id); + bulkRequest.add(deleteRequest); + } + } + + try (RestHighLevelClient restClient = this.elasticClientHandler.createRestClient()) { + return processBulkRequest(restClient, bulkRequest); + } + } + + private List<String> processBulkRequest(RestHighLevelClient restClient, BulkRequest bulkRequest) + throws AppException { + + List<String> failureRecordIds = new LinkedList<>(); + if (bulkRequest.numberOfActions() == 0) { + return failureRecordIds; + } + + try { + BulkResponse bulkResponse = restClient.bulk(bulkRequest, RequestOptions.DEFAULT); + + // log failed bulk requests + ArrayList<String> bulkFailures = new ArrayList<>(); + int succeededResponses = 0; + int failedResponses = 0; + + for (BulkItemResponse bulkItemResponse : bulkResponse.getItems()) { + if (bulkItemResponse.isFailed()) { + BulkItemResponse.Failure failure = bulkItemResponse.getFailure(); + bulkFailures.add(String.format("elasticsearch bulk service status: %s id: %s message: %s", + failure.getStatus(), failure.getId(), failure.getMessage())); + this.jobStatus.addOrUpdateRecordStatus(bulkItemResponse.getId(), IndexingStatus.FAIL, + failure.getStatus().getStatus(), bulkItemResponse.getFailureMessage()); + if (RETRY_ELASTIC_EXCEPTION.contains(bulkItemResponse.status())) { + failureRecordIds.add(bulkItemResponse.getId()); + } + failedResponses++; + } else { + succeededResponses++; + this.jobStatus.addOrUpdateRecordStatus(bulkItemResponse.getId(), IndexingStatus.SUCCESS, + HttpStatus.SC_OK, "Indexed Successfully"); + } + } + if (!bulkFailures.isEmpty()) { + this.jaxRsDpsLog.warning(bulkFailures); + } + + jaxRsDpsLog.info(String + .format("records in elasticsearch service bulk request: %s | successful: %s | failed: %s", + bulkRequest.numberOfActions(), succeededResponses, failedResponses)); + } catch (IOException e) { + // throw explicit 504 for IOException + throw new AppException(HttpStatus.SC_GATEWAY_TIMEOUT, "Elastic error", + "Request cannot be completed in specified time.", e); + } catch (ElasticsearchStatusException e) { + throw new AppException(e.status().getStatus(), "Elastic error", e.getMessage(), e); + } catch (Exception e) { + throw new AppException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Elastic error", + "Error indexing records.", e); + } + return failureRecordIds; + } + + private Map<String, Object> getSourceMap(RecordIndexerPayload.Record record) { + + Map<String, Object> indexerPayload = new HashMap<>(); + + // get the key and get the corresponding object from the individualRecord object + if (record.getData() != null) { + Map<String, Object> data = new HashMap<>(); + for (Map.Entry<String, Object> entry : record.getData().entrySet()) { + data.put(entry.getKey(), entry.getValue()); + } + indexerPayload.put(Constants.DATA, data); + } + + indexerPayload.put(RecordMetaAttribute.ID.getValue(), record.getId()); + indexerPayload.put(RecordMetaAttribute.KIND.getValue(), record.getKind()); + indexerPayload.put(RecordMetaAttribute.NAMESPACE.getValue(), record.getNamespace()); + indexerPayload.put(RecordMetaAttribute.TYPE.getValue(), record.getType()); + indexerPayload.put(RecordMetaAttribute.VERSION.getValue(), record.getVersion()); + indexerPayload.put(RecordMetaAttribute.ACL.getValue(), record.getAcl()); + indexerPayload.put(RecordMetaAttribute.X_ACL.getValue(), Acl.flattenAcl(record.getAcl())); + indexerPayload.put(RecordMetaAttribute.LEGAL.getValue(), record.getLegal()); + indexerPayload.put(RecordMetaAttribute.INDEX_STATUS.getValue(), record.getIndexProgress()); + if (record.getAncestry() != null) { + indexerPayload.put(RecordMetaAttribute.ANCESTRY.getValue(), record.getAncestry()); + } + return indexerPayload; + } + + private void retryAndEnqueueFailedRecords(List<RecordInfo> recordInfos, + List<String> failuresRecordIds, RecordChangedMessages message) throws IOException { + + jaxRsDpsLog.info(String.format( + "queuing bulk failed records back to task-queue for retry | count: %s | records: %s", + failuresRecordIds.size(), failuresRecordIds)); + List<RecordInfo> retryRecordInfos = new LinkedList<>(); + for (String recordId : failuresRecordIds) { + for (RecordInfo origMessage : recordInfos) { + if (origMessage.getId().equalsIgnoreCase(recordId)) { + retryRecordInfos.add(origMessage); + } + } + } + + RecordChangedMessages newMessage = RecordChangedMessages.builder() + .messageId(message.getMessageId()) + .publishTime(message.getPublishTime()) + .data(this.gson.toJson(retryRecordInfos)) + .attributes(message.getAttributes()).build(); + + String payLoad = this.gson.toJson(newMessage); + this.indexerQueueTaskBuilder.createWorkerTask(payLoad, this.headers); + } + + private void updateAuditLog() { + logAuditEvents(OperationType.create, this.auditLogger::indexCreateRecordSuccess, + this.auditLogger::indexCreateRecordFail); + logAuditEvents(OperationType.update, this.auditLogger::indexUpdateRecordSuccess, + this.auditLogger::indexUpdateRecordFail); + logAuditEvents(OperationType.purge, this.auditLogger::indexPurgeRecordSuccess, + this.auditLogger::indexPurgeRecordFail); + logAuditEvents(OperationType.delete, this.auditLogger::indexDeleteRecordSuccess, + this.auditLogger::indexDeleteRecordFail); + } + + private void logAuditEvents(OperationType operationType, Consumer<List<String>> successEvent, + Consumer<List<String>> failedEvent) { + List<RecordStatus> succeededRecords = this.jobStatus + .getRecordStatuses(IndexingStatus.SUCCESS, operationType); + if (!succeededRecords.isEmpty()) { + successEvent.accept(succeededRecords.stream().map(RecordStatus::succeededAuditLogMessage) + .collect(Collectors.toList())); + } + List<RecordStatus> skippedRecords = this.jobStatus + .getRecordStatuses(IndexingStatus.SKIP, operationType); + List<RecordStatus> failedRecords = this.jobStatus + .getRecordStatuses(IndexingStatus.FAIL, operationType); + failedRecords.addAll(skippedRecords); + if (!failedRecords.isEmpty()) { + failedEvent.accept(failedRecords.stream().map(RecordStatus::failedAuditLogMessage) + .collect(Collectors.toList())); + } + } +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/util/DpsHeaderFactoryGcp.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/util/DpsHeaderFactoryGcp.java new file mode 100644 index 0000000000000000000000000000000000000000..86fb43449ea75340340b0ee43de9e85304c79697 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/util/DpsHeaderFactoryGcp.java @@ -0,0 +1,57 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.util; + +import com.google.common.base.Strings; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Collectors; +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import org.opengroup.osdu.core.common.model.http.DpsHeaders; +import org.opengroup.osdu.core.gcp.model.AppEngineHeaders; +import org.opengroup.osdu.core.gcp.util.TraceIdExtractor; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; +import org.springframework.web.context.annotation.RequestScope; + +@Component +@RequestScope +@Primary +public class DpsHeaderFactoryGcp extends DpsHeaders { + + @Inject + public DpsHeaderFactoryGcp(HttpServletRequest request) { + + Map<String, String> headers = Collections + .list(request.getHeaderNames()) + .stream() + .collect(Collectors.toMap(h -> h, request::getHeader)); + + String traceContext = headers.get(AppEngineHeaders.CLOUD_TRACE_CONTEXT); + + if (!Strings.isNullOrEmpty(traceContext)) { + headers.put(AppEngineHeaders.TRACE_ID, TraceIdExtractor.getTraceId(traceContext)); + } + + this.addFromMap(headers); + + // Add Correlation ID if missing + this.addCorrelationIdIfMissing(); + } +} \ No newline at end of file diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/util/MongoClientHandler.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/util/MongoClientHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..8e53b593ec171e2962ca3b286d328e89a1a7716c --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/util/MongoClientHandler.java @@ -0,0 +1,80 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.util; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import org.apache.http.HttpStatus; +import org.opengroup.osdu.core.common.model.http.AppException; +import org.opengroup.osdu.indexer.config.MongoDBConfigProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class MongoClientHandler { + + private static final Logger LOG = LoggerFactory.getLogger( + org.opengroup.osdu.indexer.util.MongoClientHandler.class); + private static final String MONGO_PREFIX = "mongodb://"; + private static final String MONGO_OPTIONS = "retryWrites=true&w=majority&maxIdleTimeMS=10000"; + + private com.mongodb.client.MongoClient mongoClient = null; + private MongoDBConfigProperties mongoDBConfigProperties; + + private MongoClient getOrInitMongoClient() throws RuntimeException { + if (mongoClient != null) { + return mongoClient; + } + + final String connectionString = String.format("%s%s:%s@%s/?%s", + MONGO_PREFIX, + mongoDBConfigProperties.getMongoDbUser(), + mongoDBConfigProperties.getMongoDbPassword(), + mongoDBConfigProperties.getMongoDbUrl(), + MONGO_OPTIONS); + ConnectionString connString = new ConnectionString(connectionString); + MongoClientSettings settings = MongoClientSettings.builder() + .applyConnectionString(connString) + .retryWrites(true) + .build(); + try { + mongoClient = MongoClients.create(settings); + } catch (Exception ex) { + LOG.error("Error connecting MongoDB", ex); + throw new AppException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Error connecting MongoDB", + ex.getMessage(), ex); + } + return mongoClient; + } + + public MongoClient getMongoClient() { + if (mongoClient == null) { + getOrInitMongoClient(); + } + return mongoClient; + } + + @Autowired + public void setMongoDBConfigProperties(MongoDBConfigProperties mongoDBConfigProperties) { + this.mongoDBConfigProperties = mongoDBConfigProperties; + } +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/util/RequestInfoImpl.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/util/RequestInfoImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..1e3c3f244cb37470a91c6a72185a2007c2578698 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/util/RequestInfoImpl.java @@ -0,0 +1,124 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.util; + +import static org.opengroup.osdu.core.common.model.http.DpsHeaders.AUTHORIZATION; + +import com.google.common.base.Strings; +import java.util.Map; +import lombok.extern.java.Log; +import org.apache.http.HttpStatus; +import org.opengroup.osdu.core.common.Constants; +import org.opengroup.osdu.core.common.model.http.AppException; +import org.opengroup.osdu.core.common.model.http.DpsHeaders; +import org.opengroup.osdu.core.common.model.search.DeploymentEnvironment; +import org.opengroup.osdu.core.common.model.tenant.TenantInfo; +import org.opengroup.osdu.core.common.provider.interfaces.IRequestInfo; +import org.opengroup.osdu.core.common.util.IServiceAccountJwtClient; +import org.opengroup.osdu.core.gcp.model.AppEngineHeaders; +import org.opengroup.osdu.indexer.config.IndexerConfigurationProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.context.annotation.RequestScope; + +@Log +@Component +@RequestScope +public class RequestInfoImpl implements IRequestInfo { + + private final IndexerConfigurationProperties indexerConfigurationProperties; + private final TenantInfo tenantInfo; + private final IServiceAccountJwtClient serviceAccountJwtClient; + private final DpsHeaders dpsHeaders; + + @Autowired + public RequestInfoImpl(IndexerConfigurationProperties indexerConfigurationProperties, + TenantInfo tenantInfo, IServiceAccountJwtClient serviceAccountJwtClient, + DpsHeaders dpsHeaders) { + this.indexerConfigurationProperties = indexerConfigurationProperties; + this.tenantInfo = tenantInfo; + this.serviceAccountJwtClient = serviceAccountJwtClient; + this.dpsHeaders = dpsHeaders; + } + + private static final String expectedCronHeaderValue = "true"; + + @Override + public DpsHeaders getHeaders() { + + return this.dpsHeaders; + } + + @Override + public String getPartitionId() { + return this.dpsHeaders.getPartitionId(); + } + + @Override + public Map<String, String> getHeadersMap() { + return this.dpsHeaders.getHeaders(); + } + + @Override + public Map<String, String> getHeadersMapWithDwdAuthZ() { + return getHeadersWithDwdAuthZ().getHeaders(); + } + + @Override + public DpsHeaders getHeadersWithDwdAuthZ() { + // Update DpsHeaders so that service account creds are passed down + this.dpsHeaders.put(AUTHORIZATION, this.checkOrGetAuthorizationHeader()); + return this.dpsHeaders; + } + + @Override + public boolean isCronRequest() { + String appEngineCronHeader = this.dpsHeaders.getHeaders() + .getOrDefault(AppEngineHeaders.CRON_SERVICE, null); + return expectedCronHeaderValue.equalsIgnoreCase(appEngineCronHeader); + } + + @Override + public boolean isTaskQueueRequest() { + if (!this.dpsHeaders.getHeaders().containsKey(AppEngineHeaders.TASK_QUEUE_NAME)) { + return false; + } + + String queueId = this.dpsHeaders.getHeaders().get(AppEngineHeaders.TASK_QUEUE_NAME); + return queueId.endsWith(Constants.INDEXER_QUEUE_IDENTIFIER); + } + + public String checkOrGetAuthorizationHeader() { + if (DeploymentEnvironment.valueOf(indexerConfigurationProperties.getEnvironment()) + == DeploymentEnvironment.LOCAL) { + String authHeader = this.dpsHeaders.getAuthorization(); + if (Strings.isNullOrEmpty(authHeader)) { + throw new AppException(HttpStatus.SC_UNAUTHORIZED, "Invalid authorization header", + "Authorization token cannot be empty"); + } + String user = this.dpsHeaders.getUserEmail(); + if (Strings.isNullOrEmpty(user)) { + throw new AppException(HttpStatus.SC_UNAUTHORIZED, "Invalid user header", + "User header cannot be empty"); + } + return authHeader; + } else { + return "Bearer " + this.serviceAccountJwtClient.getIdToken(tenantInfo.getName()); + } + } +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/util/ServiceAccountJwtGcpClientImpl.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/util/ServiceAccountJwtGcpClientImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..98605a1509c597b4b5f7332560374ff072248d44 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/util/ServiceAccountJwtGcpClientImpl.java @@ -0,0 +1,202 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.util; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.exceptions.JWTDecodeException; +import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.services.iam.v1.Iam; +import com.google.api.services.iam.v1.IamScopes; +import com.google.api.services.iam.v1.model.SignJwtRequest; +import com.google.api.services.iam.v1.model.SignJwtResponse; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpStatus; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.opengroup.osdu.core.common.logging.JaxRsDpsLog; +import org.opengroup.osdu.core.common.model.http.AppException; +import org.opengroup.osdu.core.common.model.http.DpsHeaders; +import org.opengroup.osdu.core.common.model.search.IdToken; +import org.opengroup.osdu.core.common.model.tenant.TenantInfo; +import org.opengroup.osdu.core.common.provider.interfaces.IJwtCache; +import org.opengroup.osdu.core.common.provider.interfaces.ITenantFactory; +import org.opengroup.osdu.core.common.util.IServiceAccountJwtClient; +import org.opengroup.osdu.indexer.config.IndexerConfigurationProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.context.annotation.RequestScope; + +@Component +@RequestScope +public class ServiceAccountJwtGcpClientImpl implements IServiceAccountJwtClient { + + private static final String JWT_AUDIENCE = "https://www.googleapis.com/oauth2/v4/token"; + private static final String SERVICE_ACCOUNT_NAME_FORMAT = "projects/%s/serviceAccounts/%s"; + + private final JsonFactory JSON_FACTORY = new JacksonFactory(); + private final IndexerConfigurationProperties indexerConfigurationProperties; + private final ITenantFactory tenantInfoServiceProvider; + private final IJwtCache cacheService; + private final JaxRsDpsLog log; + private final DpsHeaders dpsHeaders; + + private Iam iam; + + @Autowired + public ServiceAccountJwtGcpClientImpl( + IndexerConfigurationProperties indexerConfigurationProperties, + ITenantFactory tenantInfoServiceProvider, + IJwtCache cacheService, JaxRsDpsLog log, DpsHeaders dpsHeaders) { + this.indexerConfigurationProperties = indexerConfigurationProperties; + this.tenantInfoServiceProvider = tenantInfoServiceProvider; + this.cacheService = cacheService; + this.log = log; + this.dpsHeaders = dpsHeaders; + } + + public String getIdToken(String tenantName) { + this.log.info("Tenant name received for auth token is: " + tenantName); + TenantInfo tenant = this.tenantInfoServiceProvider.getTenantInfo(tenantName); + if (tenant == null) { + this.log.error("Invalid tenant name receiving from pubsub"); + throw new AppException(HttpStatus.SC_BAD_REQUEST, "Invalid tenant Name", + "Invalid tenant Name from pubsub"); + } + try { + + IdToken cachedToken = (IdToken) this.cacheService.get(tenant.getServiceAccount()); + // Add the user to DpsHeaders directly + this.dpsHeaders.put(DpsHeaders.USER_EMAIL, tenant.getServiceAccount()); + + if (!IdToken.refreshToken(cachedToken)) { + return cachedToken.getTokenValue(); + } + + // Getting signed JWT + Map<String, Object> signJwtPayload = this.getJWTCreationPayload(tenant); + + SignJwtRequest signJwtRequest = new SignJwtRequest(); + signJwtRequest.setPayload(JSON_FACTORY.toString(signJwtPayload)); + + String serviceAccountName = String + .format(SERVICE_ACCOUNT_NAME_FORMAT, tenant.getProjectId(), tenant.getServiceAccount()); + + Iam.Projects.ServiceAccounts.SignJwt signJwt = this.getIam().projects().serviceAccounts() + .signJwt(serviceAccountName, signJwtRequest); + SignJwtResponse signJwtResponse = signJwt.execute(); + String signedJwt = signJwtResponse.getSignedJwt(); + + // Getting id token + List<NameValuePair> postParameters = new ArrayList<>(); + postParameters + .add(new BasicNameValuePair("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer")); + postParameters.add(new BasicNameValuePair("assertion", signedJwt)); + + HttpPost post = new HttpPost(JWT_AUDIENCE); + post.setHeader(HttpHeaders.CONTENT_TYPE, + ContentType.APPLICATION_FORM_URLENCODED.getMimeType()); + post.setEntity(new UrlEncodedFormEntity(postParameters, "UTF-8")); + + try (CloseableHttpClient httpclient = HttpClients.createDefault(); + CloseableHttpResponse httpResponse = httpclient.execute(post)) { + JsonObject jsonContent = new JsonParser() + .parse(EntityUtils.toString(httpResponse.getEntity())) + .getAsJsonObject(); + + if (!jsonContent.has("id_token")) { + log.error(String.format("Google IAM response: %s", jsonContent.toString())); + throw new AppException(HttpStatus.SC_FORBIDDEN, "Access denied", + "The user is not authorized to perform this action"); + } + + String token = jsonContent.get("id_token").getAsString(); + IdToken idToken = IdToken.builder().tokenValue(token) + .expirationTimeMillis(JWT.decode(token).getExpiresAt().getTime()).build(); + + this.cacheService.put(tenant.getServiceAccount(), idToken); + + return token; + } + } catch (JWTDecodeException e) { + throw new AppException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Persistence error", + "Invalid token, error decoding", e); + } catch (AppException e) { + throw e; + } catch (Exception e) { + throw new AppException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Persistence error", + "Error generating token", + e); + } + } + + public Iam getIam() throws Exception { + + if (this.iam == null) { + HttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport(); + + // Authenticate using Google Application Default Credentials. + GoogleCredential credential = GoogleCredential.getApplicationDefault(); + if (credential.createScopedRequired()) { + List<String> scopes = new ArrayList<>(); + // Enable full Cloud Platform scope. + scopes.add(IamScopes.CLOUD_PLATFORM); + credential = credential.createScoped(scopes); + } + + // Create IAM API object associated with the authenticated transport. + this.iam = new Iam.Builder(httpTransport, JSON_FACTORY, credential) + .setApplicationName(indexerConfigurationProperties.getIndexerHost()) + .build(); + } + + return this.iam; + } + + private Map<String, Object> getJWTCreationPayload(TenantInfo tenantInfo) { + + Map<String, Object> payload = new HashMap<>(); + String googleAudience = indexerConfigurationProperties.getGoogleAudiences(); + if (googleAudience.contains(",")) { + googleAudience = googleAudience.split(",")[0]; + } + payload.put("target_audience", googleAudience); + payload.put("exp", System.currentTimeMillis() / 1000 + 3600); + payload.put("iat", System.currentTimeMillis() / 1000); + payload.put("iss", tenantInfo.getServiceAccount()); + payload.put("aud", JWT_AUDIENCE); + + return payload; + } +} diff --git a/provider/indexer-reference/src/main/resources/application-dev.properties b/provider/indexer-reference/src/main/resources/application-dev.properties new file mode 100644 index 0000000000000000000000000000000000000000..ea75c38d4866a1a690158a7e7efbd184c68ad58c --- /dev/null +++ b/provider/indexer-reference/src/main/resources/application-dev.properties @@ -0,0 +1,44 @@ +# +# Copyright 2020 Google LLC +# Copyright 2020 EPAM Systems, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +GOOGLE_CLOUD_PROJECT=nice-etching-277309 + +mongo-db-url=localhost:27017 +mongo-db-user= +mongo-db-password= + +indexer-host=os-indexer-dot-nice-etching-277309.uc.r.appspot.com +storage-hostname=os-storage-dot-nice-etching-277309.uc.r.appspot.com + +storage-schema-host=https://os-storage-dot-nice-etching-277309.uc.r.appspot.com/api/storage/v2/schemas +storage-query-record-host=https://os-storage-dot-nice-etching-277309.uc.r.appspot.com/api/storage/v2/query/records +storage-query-record-for-conversion-host=https://os-storage-dot-nice-etching-277309.uc.r.appspot.com/api/storage/v2/query/records:batch +storage-records-batch-size=20 + +indexer-queue-host=https://os-indexer-queue-dot-nice-etching-277309.uc.r.appspot.com/_dps/task-handlers/enqueue + +authorize-api=https://entitlements-dot-nice-etching-277309.uc.r.appspot.com/entitlements/v1 +legaltag-api=https://os-legal-dot-nice-etching-277309.uc.r.appspot.com/api/legal/v1 +crs-api=example.com + +## use below values for gcp: opendes +redis-group-host=127.0.0.1 +redis-search-host=127.0.0.1 + +google-audiences=689762842995-pv217jo3k8j803kk6gqf52qb5amos3a9.apps.googleusercontent.com + +mb-rabbitmq-uri=amqp://guest:guest@127.0.0.1:5672 diff --git a/provider/indexer-reference/src/main/resources/application.properties b/provider/indexer-reference/src/main/resources/application.properties new file mode 100644 index 0000000000000000000000000000000000000000..8690c57a906c0aae8cf6ecc5b0c282be392bbafb --- /dev/null +++ b/provider/indexer-reference/src/main/resources/application.properties @@ -0,0 +1,76 @@ +# +# Copyright 2021 Google LLC +# Copyright 2021 EPAM Systems, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +LOG_PREFIX=indexer + +server.servlet.contextPath=/api/indexer/v2 +logging.level.org.springframework.web=DEBUG +server.port=8080 +JAVA_OPTS=-Xms3072m -Xmx3072m +JAVA_GC_OPTS=-XX:+UseG1GC -XX:+UseStringDeduplication -XX:InitiatingHeapOccupancyPercent=45 + +environment=CLOUD + +#default cache settings +schema-cache-expiration=60 +index-cache-expiration=60 +elastic-cache-expiration=1440 +cursor-cache-expiration=60 +# kinds cache expiration 2*24*60 +kinds-cache-expiration=2880 +# attributes cache expiration 2*24*60 +attributes-cache-expiration=2880 + +kinds-redis-database=1 +cron-index-cleanup-threshold-days=3 +cron-empty-index-cleanup-threshold-days=7 + +gae-service=indexer +key-ring=csqp +kms-key=searchservice + +elastic-datastore-kind=searchsettings +elastic-datastore-id=indexer-service + +mongo-db-url=localhost:27017 +mongo-db-user= +mongo-db-password= + +indexer-host=os-indexer-dot-nice-etching-277309.uc.r.appspot.com +storage-hostname=os-storage-dot-nice-etching-277309.uc.r.appspot.com + +storage-schema-host=https://os-storage-dot-nice-etching-277309.uc.r.appspot.com/api/storage/v2/schemas +storage-query-record-host=https://os-storage-dot-nice-etching-277309.uc.r.appspot.com/api/storage/v2/query/records +storage-query-record-for-conversion-host=https://os-storage-dot-nice-etching-277309.uc.r.appspot.com/api/storage/v2/query/records:batch +storage-records-batch-size=20 + +indexer-queue-host=https://os-indexer-queue-dot-nice-etching-277309.uc.r.appspot.com/_dps/task-handlers/enqueue + +authorize-api=https://entitlements-dot-nice-etching-277309.uc.r.appspot.com/entitlements/v1 +legaltag-api=https://os-legal-dot-nice-etching-277309.uc.r.appspot.com/api/legal/v1 +crs-api=example.com + +## use below values for gcp: opendes +redis-group-host=127.0.0.1 +redis-search-host=127.0.0.1 + +google-audiences=689762842995-pv217jo3k8j803kk6gqf52qb5amos3a9.apps.googleusercontent.com + +mb-rabbitmq-uri=amqp://guest:guest@127.0.0.1:5672 + + + diff --git a/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/middleware/IndexFilterTest.java b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/middleware/IndexFilterTest.java new file mode 100644 index 0000000000000000000000000000000000000000..9ec5fb7a30044f7f105c9a97229cb37322f218b9 --- /dev/null +++ b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/middleware/IndexFilterTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.middleware; + +import java.io.IOException; +import java.util.Collections; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.opengroup.osdu.core.common.model.http.DpsHeaders; + +@RunWith(MockitoJUnitRunner.class) +public class IndexFilterTest { + + @InjectMocks + private IndexFilter indexFilter; + + @Mock + private DpsHeaders dpsHeaders; + + @Test + public void shouldSetCorrectResponseHeaders() throws IOException, ServletException { + HttpServletRequest httpServletRequest = Mockito.mock(HttpServletRequest.class); + HttpServletResponse httpServletResponse = Mockito.mock(HttpServletResponse.class); + FilterChain filterChain = Mockito.mock(FilterChain.class); + Mockito.when(httpServletRequest.getRequestURI()).thenReturn("https://test.com"); + Mockito.when(httpServletRequest.getMethod()).thenReturn("POST"); + Mockito.when(dpsHeaders.getCorrelationId()).thenReturn("correlation-id-value"); + + indexFilter.doFilter(httpServletRequest, httpServletResponse, filterChain); + + Mockito.verify(httpServletResponse) + .addHeader("Access-Control-Allow-Origin", Collections.singletonList("*").toString()); + Mockito.verify(httpServletResponse).addHeader("Access-Control-Allow-Headers", Collections + .singletonList( + "origin, content-type, accept, authorization, data-partition-id, correlation-id, appkey") + .toString()); + Mockito.verify(httpServletResponse).addHeader("Access-Control-Allow-Methods", + Collections.singletonList("GET, POST, PUT, DELETE, OPTIONS, HEAD").toString()); + Mockito.verify(httpServletResponse).addHeader("Access-Control-Allow-Credentials", + Collections.singletonList("true").toString()); + Mockito.verify(httpServletResponse) + .addHeader("X-Frame-Options", Collections.singletonList("DENY").toString()); + Mockito.verify(httpServletResponse) + .addHeader("X-XSS-Protection", Collections.singletonList("1; mode=block").toString()); + Mockito.verify(httpServletResponse) + .addHeader("X-Content-Type-Options", Collections.singletonList("nosniff").toString()); + Mockito.verify(httpServletResponse).addHeader("Cache-Control", + Collections.singletonList("no-cache, no-store, must-revalidate").toString()); + Mockito.verify(httpServletResponse).addHeader("Content-Security-Policy", + Collections.singletonList("default-src 'self'").toString()); + Mockito.verify(httpServletResponse).addHeader("Strict-Transport-Security", + Collections.singletonList("max-age=31536000; includeSubDomains").toString()); + Mockito.verify(httpServletResponse) + .addHeader("Expires", Collections.singletonList("0").toString()); + Mockito.verify(httpServletResponse).addHeader("correlation-id", "correlation-id-value"); + Mockito.verify(filterChain).doFilter(httpServletRequest, httpServletResponse); + } +} diff --git a/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/CronServiceImplTest.java b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/CronServiceImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..e1c53edb8ab313727f54a905a3494a74cb3f6961 --- /dev/null +++ b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/CronServiceImplTest.java @@ -0,0 +1,152 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.service; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.collect.Lists; +import java.io.IOException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import org.elasticsearch.client.RestHighLevelClient; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.opengroup.osdu.core.common.logging.JaxRsDpsLog; +import org.opengroup.osdu.core.common.model.http.DpsHeaders; +import org.opengroup.osdu.core.common.model.search.IndexInfo; +import org.opengroup.osdu.core.common.provider.interfaces.IRequestInfo; +import org.opengroup.osdu.core.common.search.IndicesService; +import org.opengroup.osdu.indexer.config.IndexerConfigurationProperties; +import org.opengroup.osdu.indexer.util.ElasticClientHandler; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.springframework.test.context.junit4.SpringRunner; + + +@RunWith(SpringRunner.class) +@PrepareForTest({RestHighLevelClient.class}) +public class CronServiceImplTest { + + @Mock + private RestHighLevelClient restHighLevelClient; + @Mock + private IndicesService indicesService; + @Mock + private ElasticClientHandler elasticClientHandler; + @Mock + private IndexerConfigurationProperties configurationProperties; + @Mock + private IRequestInfo requestInfo; + @Mock + private JaxRsDpsLog log; + @InjectMocks + private CronServiceImpl sut; + + @InjectMocks + private DpsHeaders dpsHeaders; + + @Before + public void setup() { + + when(this.requestInfo.getHeaders()).thenReturn(dpsHeaders); + + when(configurationProperties.getCronIndexCleanupThresholdDays()).thenReturn(3); + when(configurationProperties.getCronEmptyIndexCleanupThresholdDays()).thenReturn(3); + } + + @Test + public void run_cleanup_when_cron_job_runs_with_correct_pattern() throws Exception { + final String indexPattern = "tenant1-index-*"; + + IndexInfo info = IndexInfo.builder().name("tenant1-index-1.0.0").documentCount("10") + .creationDate(Long.toString(Instant.now().minus(4, ChronoUnit.DAYS).toEpochMilli())) + .build(); + + when(this.requestInfo.getPartitionId()).thenReturn("tenant1"); + when(this.elasticClientHandler.createRestClient()).thenReturn(this.restHighLevelClient); + when(this.indicesService.getIndexInfo(this.restHighLevelClient, indexPattern)) + .thenReturn(Lists.newArrayList(info)); + + this.sut.cleanupIndices(indexPattern); + + verify(this.indicesService, times(1)).deleteIndex(restHighLevelClient, "tenant1-index-1.0.0"); + verify(this.indicesService, times(1)).getIndexInfo(restHighLevelClient, indexPattern); + } + + @Test(expected = IOException.class) + public void run_cleanup_when_cron_job_runs_with_wrong_pattern() throws Exception { + IOException exception = new IOException("blah"); + when(this.elasticClientHandler.createRestClient()).thenReturn(this.restHighLevelClient); + when(this.indicesService.getIndexInfo(this.restHighLevelClient, "tenant1-test-*")) + .thenThrow(exception); + + this.sut.cleanupIndices("tenant1-test-*"); + + verify(this.indicesService, times(0)).deleteIndex(any(), any()); + } + + @Test + public void run_cleanup_when_backend_does_not_have_empty_stale_indices() throws Exception { + IndexInfo info = IndexInfo.builder().name("tenant1-index-1.0.0").documentCount("10") + .creationDate(Long.toString(Instant.now().minus(8, ChronoUnit.DAYS).toEpochMilli())) + .build(); + + when(this.requestInfo.getPartitionId()).thenReturn("tenant1"); + when(this.elasticClientHandler.createRestClient()).thenReturn(this.restHighLevelClient); + when(this.indicesService.getIndexInfo(this.restHighLevelClient, null)) + .thenReturn(Lists.newArrayList(info)); + + this.sut.cleanupEmptyStaleIndices(); + + verify(this.indicesService, times(0)).deleteIndex(restHighLevelClient, null); + verify(this.indicesService, times(1)).getIndexInfo(restHighLevelClient, null); + } + + @Test + public void run_cleanup_when_backend_have_empty_stale_indices() throws Exception { + IndexInfo info = IndexInfo.builder().name("tenant1-index-1.0.0").documentCount("0") + .creationDate(Long.toString(Instant.now().minus(8, ChronoUnit.DAYS).toEpochMilli())) + .build(); + + when(this.requestInfo.getPartitionId()).thenReturn("tenant1"); + when(this.elasticClientHandler.createRestClient()).thenReturn(this.restHighLevelClient); + when(this.indicesService.getIndexInfo(this.restHighLevelClient, null)) + .thenReturn(Lists.newArrayList(info)); + + this.sut.cleanupEmptyStaleIndices(); + + verify(this.indicesService, times(1)).deleteIndex(restHighLevelClient, "tenant1-index-1.0.0"); + verify(this.indicesService, times(1)).getIndexInfo(restHighLevelClient, null); + } + + @Test(expected = IOException.class) + public void run_cleanup_when_backend_throws_exception() throws Exception { + IOException exception = new IOException("blah"); + when(this.elasticClientHandler.createRestClient()).thenReturn(this.restHighLevelClient); + when(this.indicesService.getIndexInfo(this.restHighLevelClient, null)).thenThrow(exception); + + this.sut.cleanupEmptyStaleIndices(); + + verify(this.indicesService, times(0)).deleteIndex(any(), any()); + } +} \ No newline at end of file diff --git a/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/ElasticSettingServiceTest.java b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/ElasticSettingServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..b09fbc40ddb38ffd327be67375a4b7718d687722 --- /dev/null +++ b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/ElasticSettingServiceTest.java @@ -0,0 +1,118 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.service; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.opengroup.osdu.core.common.logging.JaxRsDpsLog; +import org.opengroup.osdu.core.common.model.http.AppException; +import org.opengroup.osdu.core.common.model.http.DpsHeaders; +import org.opengroup.osdu.core.common.model.search.ClusterSettings; +import org.opengroup.osdu.core.common.model.tenant.TenantInfo; +import org.opengroup.osdu.core.common.multitenancy.ITenantInfoService; +import org.opengroup.osdu.core.common.provider.interfaces.IElasticCredentialsCache; +import org.opengroup.osdu.core.common.provider.interfaces.IElasticRepository; +import org.opengroup.osdu.indexer.config.IndexerConfigurationProperties; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +public class ElasticSettingServiceTest { + + @Mock + private ITenantInfoService tenantInfoService; + @Mock + private IElasticRepository elasticRepository; + @Mock + private IElasticCredentialsCache elasticCredentialCache; + @Mock + private IndexerConfigurationProperties configurationProperties; + @Mock + private TenantInfo tenantInfo; + @InjectMocks + private ElasticSettingServiceImpl sut; + @Mock + private ClusterSettings clusterSettings; + @Mock + private DpsHeaders headersInfo; + + @Mock + private JaxRsDpsLog log; + + + public String GAE_SERVICE = "indexer"; + + private final String host = "db5c51c1.us-central1.gcp.cloud.es.io"; + private final int port = 9243; + private final String credentials = "name:password"; + + String cacheKey = ""; + + + @Before + public void setup() { + when(tenantInfo.getName()).thenReturn("tenant1"); + when(this.headersInfo.getPartitionId()).thenReturn("tenant1"); + when(this.tenantInfoService.getTenantInfo()).thenReturn(tenantInfo); + when(configurationProperties.getGaeService()).thenReturn("indexer"); + clusterSettings = ClusterSettings.builder().host(host).port(port) + .userNameAndPassword(credentials).build(); + cacheKey = String.format("%s-%s", GAE_SERVICE, tenantInfo.getName()); + } + + @Test + public void should_getValid_clusterSettings_fromCache() { + + when(this.elasticCredentialCache.get(cacheKey)).thenReturn(clusterSettings); + + ClusterSettings response = this.sut.getElasticClusterInformation(); + assertNotNull(response); + assertEquals(response.getHost(), host); + assertEquals(response.getPort(), port); + assertEquals(response.getUserNameAndPassword(), credentials); + } + + @Test + public void should_getValid_clusterSettings_fromCosmosDB() { + + when(this.elasticCredentialCache.get(cacheKey)).thenReturn(clusterSettings); + + when(this.elasticRepository.getElasticClusterSettings(tenantInfo)).thenReturn(clusterSettings); + + ClusterSettings response = this.sut.getElasticClusterInformation(); + assertNotNull(response); + assertEquals(response.getHost(), host); + assertEquals(response.getPort(), port); + assertEquals(response.getUserNameAndPassword(), credentials); + } + + @Test(expected = AppException.class) + public void should_throwAppException_when_tenantClusterInfo_not_found() throws AppException { + + when(this.elasticRepository.getElasticClusterSettings(tenantInfo)).thenReturn(null); + + this.sut.getElasticClusterInformation(); + + } +} diff --git a/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/IndexCopyServiceImplTest.java b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/IndexCopyServiceImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..6a438da28b492752abe329f329c0cbd85ec09977 --- /dev/null +++ b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/IndexCopyServiceImplTest.java @@ -0,0 +1,203 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.service; + +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.powermock.api.mockito.PowerMockito.when; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import org.apache.http.HttpEntity; +import org.apache.http.util.EntityUtils; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestHighLevelClient; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatchers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.opengroup.osdu.core.common.model.http.AppException; +import org.opengroup.osdu.core.common.model.http.DpsHeaders; +import org.opengroup.osdu.core.common.model.indexer.IElasticSettingService; +import org.opengroup.osdu.core.common.model.search.ClusterSettings; +import org.opengroup.osdu.core.common.provider.interfaces.IRequestInfo; +import org.opengroup.osdu.core.common.search.ElasticIndexNameResolver; +import org.opengroup.osdu.core.common.search.IndicesService; +import org.opengroup.osdu.indexer.logging.AuditLogger; +import org.opengroup.osdu.indexer.util.ElasticClientHandler; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@PrepareForTest({RestHighLevelClient.class, Response.class, RestClient.class, HttpEntity.class, + EntityUtils.class}) +public class IndexCopyServiceImplTest { + + private final String correlationId = UUID.randomUUID().toString(); + + @Mock + private HttpEntity httpEntity; + @Mock + private HttpEntity httpEntityRequest; + @Mock + private IRequestInfo requestInfo; + @Mock + private DpsHeaders headersInfo; + @Mock + private RestClient restClient; + @Mock + private RestHighLevelClient restHighLevelClient; + @Mock + private IndicesService indicesService; + @Mock + private IndexerMappingService mappingService; + @Mock + private ElasticClientHandler elasticClientHandler; + @Mock + private ElasticIndexNameResolver elasticIndexNameResolver; + @Mock + private Response response; + @Mock + private IElasticSettingService elasticSettingService; + @Mock + private AuditLogger auditLogger; + @Mock + private Map<String, String> httpHeaders; + @InjectMocks + private IndexCopyServiceImpl sut; + + private ClusterSettings commonCluster; + + private Map<String, Object> correctMap; + + @Before + public void setup() { + + commonCluster = ClusterSettings.builder().host("commonhost").port(8080) + .userNameAndPassword("username:pwd").build(); + + httpHeaders = new HashMap<>(); + httpHeaders.put(DpsHeaders.AUTHORIZATION, "testAuth"); + httpHeaders.put(DpsHeaders.CORRELATION_ID, correlationId); + when(requestInfo.getHeadersMapWithDwdAuthZ()).thenReturn(httpHeaders); + when(response.getEntity()).thenReturn(httpEntity); + + Type mapType = new TypeToken<Map<String, Object>>() { + }.getType(); + String afterFormat = "{\"properties\":{\"id\":{\"type\":\"keyword\"}}}"; + correctMap = new Gson().fromJson(afterFormat, mapType); + + restHighLevelClient = mock(RestHighLevelClient.class); + + } + + @Test(expected = IOException.class) + public void should_throwIOException_when_indexMappingNotFound() throws Exception { + IOException exception = new IOException( + "Fail to get mapping for the given index from common cluster."); + + when(this.mappingService.getIndexMapping(ArgumentMatchers.any(), ArgumentMatchers.any())) + .thenThrow(exception); + + this.sut.copyIndex("common:metadata:entity:1.0.0"); + } + + @Test(expected = IllegalArgumentException.class) + public void should_throwIllegalArgExceptionCopyIndexRequest_copyIndexTest() { + try { + this.sut.copyIndex(null); + } catch (IOException e) { + fail("Should not throw IOException but illegalArgumentException."); + } + } + + @Test + public void should_returnIndexMapping_getIndexMappingFromCommonClustertest() { + String mappingJson = "{\"common-metadata-entity-1.0.0\":{\"mappings\":{\"entity\":{\"properties\":{\"id\":{\"type\":\"keyword\"}}}}}}"; + when(elasticClientHandler.createRestClient()).thenReturn(restHighLevelClient); + try { + when(this.mappingService.getIndexMapping(ArgumentMatchers.any(), ArgumentMatchers.any())) + .thenReturn(mappingJson); + Map<String, Object> resultMap = this.sut.getIndexMappingsFromCommonCluster("test", "test"); + Assert.assertEquals(resultMap, correctMap); + } catch (Exception ignored) { + } + } + + @Test + public void should_returnClusterInfo_getCommonClusterInformationtest() { + try { + String[] correctCommonCluster = {"https://commonhost:8080", "username", "pwd"}; + + when(elasticClientHandler.createRestClient()).thenReturn(restHighLevelClient); + + when(elasticSettingService.getElasticClusterInformation()).thenReturn(commonCluster); + + String[] resultCommonCluster = this.sut.getCommonClusterInformation(); + Assert.assertEquals(correctCommonCluster[0], resultCommonCluster[0]); + Assert.assertEquals(correctCommonCluster[1], resultCommonCluster[1]); + Assert.assertEquals(correctCommonCluster[2], resultCommonCluster[2]); + } catch (IOException ignored) { + fail("Should not throw this exception " + ignored.getMessage()); + } + } + + @Test(expected = AppException.class) + public void should_throwException_failToCreateIndexInTenantCluster_createIndexInTenantClustertest() { + try { + when(elasticClientHandler.createRestClient()).thenReturn(restHighLevelClient); + when(indicesService + .createIndex(ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any(), + ArgumentMatchers.any(), ArgumentMatchers.any())).thenReturn(false); + this.sut.createIndexInTenantCluster("test", "test", "test", correctMap); + } catch (IOException ignored) { + fail("Should not throw this exception " + ignored.getMessage()); + } + } + + @Ignore + public void should_returnTaskIdResponse_reindexRequestSucceed_reindexInTenantClustertest() { + //TODO: fix the null Response from restHighLevelClient.getLowLevelClient().performRequest(). + try { + String[] correctCommonCluster = {"https://commonhost:8080", "username", "pwd"}; + Request request = new Request("POST", "/_reindex?wait_for_completion=false"); + request.setEntity(httpEntityRequest); + when(elasticClientHandler.createRestClient()).thenReturn(restHighLevelClient); + when(indicesService + .createIndex(ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any(), + ArgumentMatchers.any(), ArgumentMatchers.any())).thenReturn(false); + when(restHighLevelClient.getLowLevelClient()).thenReturn(restClient); + when(restClient.performRequest(request)).thenReturn(response); + when(response.getEntity()).thenReturn(httpEntity); + Assert.assertEquals(httpEntity, + this.sut.reindexInTenantCluster("test", "test", correctCommonCluster)); + } catch (IOException ignored) { + } + } +} diff --git a/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/IndexerSchemaServiceTest.java b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/IndexerSchemaServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..4ce6eb4e8af4a45eddbc163baad9bb9cd6c2ef12 --- /dev/null +++ b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/IndexerSchemaServiceTest.java @@ -0,0 +1,445 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.service; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyBoolean; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.MockitoAnnotations.initMocks; +import static org.powermock.api.mockito.PowerMockito.mock; +import static org.powermock.api.mockito.PowerMockito.when; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; +import org.apache.http.HttpStatus; +import org.elasticsearch.client.RestHighLevelClient; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.opengroup.osdu.core.common.logging.JaxRsDpsLog; +import org.opengroup.osdu.core.common.model.http.AppException; +import org.opengroup.osdu.core.common.model.http.RequestStatus; +import org.opengroup.osdu.core.common.model.indexer.IndexSchema; +import org.opengroup.osdu.core.common.model.indexer.OperationType; +import org.opengroup.osdu.core.common.search.ElasticIndexNameResolver; +import org.opengroup.osdu.core.common.search.IndicesService; +import org.opengroup.osdu.indexer.provider.interfaces.ISchemaCache; +import org.opengroup.osdu.indexer.service.impl.SchemaProviderImpl; +import org.opengroup.osdu.indexer.util.ElasticClientHandler; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@PrepareForTest({RestHighLevelClient.class}) +public class IndexerSchemaServiceTest { + + private final String kind = "tenant:test:test:1.0.0"; + private final String emptySchema = null; + private final String someSchema = "{\"kind\":\"tenant:test:test:1.0.0\", \"schema\":[{\"path\":\"test-path\", \"kind\":\"tenant:test:test:1.0.0\"}]}"; + + @Mock + private JaxRsDpsLog log; + @Mock + private SchemaProviderImpl schemaProvider; + @Mock + private ElasticClientHandler elasticClientHandler; + @Mock + private ElasticIndexNameResolver elasticIndexNameResolver; + @Mock + private IndexerMappingService mappingService; + @Mock + private IndicesService indicesService; + @Mock + private ISchemaCache schemaCache; + @InjectMocks + private IndexSchemaServiceImpl sut; + + @Before + public void setup() { + initMocks(this); + RestHighLevelClient restHighLevelClient = mock(RestHighLevelClient.class); + when(elasticClientHandler.createRestClient()).thenReturn(restHighLevelClient); + } + + @Test + public void should_returnNull_givenEmptySchema_getIndexerInputSchemaSchemaTest() + throws Exception { + when(schemaProvider.getSchema(any())).thenReturn(emptySchema); + + IndexSchema indexSchema = this.sut.getIndexerInputSchema(kind, false); + + Assert.assertNotNull(indexSchema); + } + + @Test + public void should_returnValidResponse_givenValidSchema_getIndexerInputSchemaTest() + throws Exception { + when(schemaProvider.getSchema(any())).thenReturn(someSchema); + + IndexSchema indexSchema = this.sut.getIndexerInputSchema(kind, false); + + Assert.assertEquals(kind, indexSchema.getKind()); + } + + @Test + public void should_returnValidResponse_givenValidSchemaWithCacheHit_getIndexerInputSchemaTest() + throws Exception { + when(schemaProvider.getSchema(any())).thenReturn(someSchema); + when(this.schemaCache.get(kind + "_flattened")).thenReturn(someSchema); + + IndexSchema indexSchema = this.sut.getIndexerInputSchema(kind, false); + + Assert.assertEquals(kind, indexSchema.getKind()); + } + + @Test + public void should_throw500_givenInvalidSchemaCacheHit_getIndexerInputSchemaTest() { + try { + String invalidSchema = "{}}"; + when(schemaProvider.getSchema(any())).thenReturn(invalidSchema); + + this.sut.getIndexerInputSchema(kind, false); + fail("Should throw exception"); + } catch (AppException e) { + Assert.assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, e.getError().getCode()); + Assert.assertEquals("An error has occurred while normalizing the schema.", + e.getError().getMessage()); + } catch (Exception e) { + fail("Should not throw exception" + e.getMessage()); + } + } + + @Test + public void should_return_basic_schema_when_storage_returns_no_schema() { + IndexSchema returnedSchema = this.sut.getIndexerInputSchema(kind, false); + + assertNotNull(returnedSchema.getDataSchema()); + assertNotNull(returnedSchema); + assertEquals(kind, returnedSchema.getKind()); + } + + @Test + public void should_create_schema_when_storage_returns_valid_schema() + throws IOException, URISyntaxException { + String kind = "tenant1:avocet:completion:1.0.0"; + String storageSchema = "{" + + " \"kind\": \"tenant1:avocet:completion:1.0.0\"," + + " \"schema\": [" + + " {" + + " \"path\": \"status\"," + + " \"kind\": \"string\"" + + " }," + + " {" + + " \"path\": \"startDate\"," + + " \"kind\": \"string\"" + + " }," + + " {" + + " \"path\": \"endDate\"," + + " \"kind\": \"string\"" + + " }," + + " {" + + " \"path\": \"type \"," + + " \"kind\": \"string\"" + + " }," + + " {" + + " \"path\": \"itemguid\"," + + " \"kind\": \"string\"" + + " }" + + " ]" + + "}"; + Map<String, OperationType> schemaMessages = new HashMap<>(); + schemaMessages.put(kind, OperationType.create_schema); + + when(this.elasticIndexNameResolver.getIndexNameFromKind(kind)) + .thenReturn(kind.replace(":", "-")); + when(this.schemaCache.get(kind)).thenReturn(null); + when(this.indicesService.isIndexExist(any(), any())).thenReturn(false); + when(this.schemaProvider.getSchema(kind)).thenReturn(storageSchema); + + this.sut.processSchemaMessages(schemaMessages); + + verify(this.mappingService, times(1)).getIndexMappingFromRecordSchema(any()); + verify(this.indicesService, times(1)).createIndex(any(), any(), any(), any(), any()); + verifyNoMoreInteractions(this.mappingService); + } + + @Test + public void should_merge_mapping_when_storage_returns_valid_schema() + throws IOException, URISyntaxException { + String kind = "tenant1:avocet:completion:1.0.0"; + String storageSchema = "{" + + " \"kind\": \"tenant1:avocet:completion:1.0.0\"," + + " \"schema\": [" + + " {" + + " \"path\": \"status\"," + + " \"kind\": \"string\"" + + " }," + + " {" + + " \"path\": \"startDate\"," + + " \"kind\": \"string\"" + + " }" + + " ]" + + "}"; + Map<String, OperationType> schemaMessages = new HashMap<>(); + schemaMessages.put(kind, OperationType.create_schema); + + when(this.elasticIndexNameResolver.getIndexNameFromKind(kind)) + .thenReturn(kind.replace(":", "-")); + when(this.schemaCache.get(kind)).thenReturn(null); + when(this.indicesService.isIndexExist(any(), any())).thenReturn(true); + when(this.schemaProvider.getSchema(kind)).thenReturn(storageSchema); + + this.sut.processSchemaMessages(schemaMessages); + + verify(this.indicesService, times(0)).createIndex(any(), any(), any(), any(), any()); + verify(this.mappingService, times(1)).createMapping(any(), any(), any(), anyBoolean()); + verifyNoMoreInteractions(this.mappingService); + } + + @Test + public void should_throw_mapping_conflict_when_elastic_backend_cannot_process_schema_changes() + throws IOException, URISyntaxException { + String kind = "tenant1:avocet:completion:1.0.0"; + String reason = String + .format("Could not create type mapping %s/completion.", kind.replace(":", "-")); + String storageSchema = "{" + + " \"kind\": \"tenant1:avocet:completion:1.0.0\"," + + " \"schema\": [" + + " {" + + " \"path\": \"status\"," + + " \"kind\": \"string\"" + + " }" + + " ]" + + "}"; + Map<String, OperationType> schemaMessages = new HashMap<>(); + schemaMessages.put(kind, OperationType.create_schema); + + when(this.elasticIndexNameResolver.getIndexNameFromKind(kind)) + .thenReturn(kind.replace(":", "-")); + when(this.schemaCache.get(kind)).thenReturn(null); + when(this.indicesService.isIndexExist(any(), any())).thenReturn(true); + when(this.schemaProvider.getSchema(kind)).thenReturn(storageSchema); + when(this.mappingService.createMapping(any(), any(), any(), anyBoolean())) + .thenThrow(new AppException(HttpStatus.SC_BAD_REQUEST, reason, "")); + + try { + this.sut.processSchemaMessages(schemaMessages); + } catch (AppException e) { + assertEquals(RequestStatus.SCHEMA_CONFLICT, e.getError().getCode()); + assertEquals("error creating or merging index mapping", e.getError().getMessage()); + assertEquals(reason, e.getError().getReason()); + } catch (Exception e) { + fail("Should not throw this exception " + e.getMessage()); + } + } + + @Test + public void should_throw_genericAppException_when_elastic_backend_cannot_process_schema_changes() + throws IOException, URISyntaxException { + String kind = "tenant1:avocet:completion:1.0.0"; + String reason = String + .format("Could not create type mapping %s/completion.", kind.replace(":", "-")); + String storageSchema = "{" + + " \"kind\": \"tenant1:avocet:completion:1.0.0\"," + + " \"schema\": [" + + " {" + + " \"path\": \"status\"," + + " \"kind\": \"string\"" + + " }" + + " ]" + + "}"; + Map<String, OperationType> schemaMessages = new HashMap<>(); + schemaMessages.put(kind, OperationType.create_schema); + + when(this.elasticIndexNameResolver.getIndexNameFromKind(kind)) + .thenReturn(kind.replace(":", "-")); + when(this.schemaCache.get(kind)).thenReturn(null); + when(this.indicesService.isIndexExist(any(), any())).thenReturn(true); + when(this.schemaProvider.getSchema(kind)).thenReturn(storageSchema); + when(this.mappingService.createMapping(any(), any(), any(), anyBoolean())) + .thenThrow(new AppException(HttpStatus.SC_FORBIDDEN, reason, "blah")); + + try { + this.sut.processSchemaMessages(schemaMessages); + } catch (AppException e) { + assertEquals(HttpStatus.SC_FORBIDDEN, e.getError().getCode()); + assertEquals("blah", e.getError().getMessage()); + assertEquals(reason, e.getError().getReason()); + } catch (Exception e) { + fail("Should not throw this exception " + e.getMessage()); + } + } + + @Test + public void should_log_and_do_nothing_when_storage_returns_invalid_schema() + throws IOException, URISyntaxException { + String kind = "tenant1:avocet:completion:1.0.0"; + String storageSchema = "{" + + " \"kind\": \"tenant1:avocet:completion:1.0.0\"" + + "}"; + Map<String, OperationType> schemaMessages = new HashMap<>(); + schemaMessages.put(kind, OperationType.create_schema); + + when(this.elasticIndexNameResolver.getIndexNameFromKind(kind)) + .thenReturn(kind.replace(":", "-")); + when(this.schemaCache.get(kind)).thenReturn(null); + when(this.indicesService.isIndexExist(any(), any())).thenReturn(true); + when(this.schemaProvider.getSchema(kind)).thenReturn(storageSchema); + + this.sut.processSchemaMessages(schemaMessages); + + verify(this.log).warning(eq("schema not found for kind: tenant1:avocet:completion:1.0.0")); + } + + @Test + public void should_invalidateCache_when_purge_schema_and_schema_found_in_cache() + throws IOException { + String kind = "tenant1:avocet:completion:1.0.0"; + Map<String, OperationType> schemaMessages = new HashMap<>(); + schemaMessages.put(kind, OperationType.purge_schema); + + when(this.elasticIndexNameResolver.getIndexNameFromKind(kind)) + .thenReturn(kind.replace(":", "-")); + when(this.indicesService.isIndexExist(any(), any())).thenReturn(true); + when(this.schemaCache.get(kind)).thenReturn("schema"); + when(this.schemaCache.get(kind + "_flattened")).thenReturn("flattened schema"); + + this.sut.processSchemaMessages(schemaMessages); + + verify(this.schemaCache, times(2)).get(anyString()); + verify(this.schemaCache, times(2)).delete(anyString()); + } + + @Test + public void should_log_warning_when_purge_schema_and_schema_not_found_in_cache() + throws IOException { + String kind = "tenant1:avocet:completion:1.0.0"; + Map<String, OperationType> schemaMessages = new HashMap<>(); + schemaMessages.put(kind, OperationType.purge_schema); + + when(this.elasticIndexNameResolver.getIndexNameFromKind(kind)) + .thenReturn(kind.replace(":", "-")); + when(this.indicesService.isIndexExist(any(), any())).thenReturn(false); + + this.sut.processSchemaMessages(schemaMessages); + + verify(this.log).warning(eq(String.format("Kind: %s not found", kind))); + } + + @Test + public void should_sync_schema_with_storage() throws Exception { + String kind = "tenant1:avocet:completion:1.0.0"; + String storageSchema = "{" + + " \"kind\": \"tenant1:avocet:completion:1.0.0\"," + + " \"schema\": [" + + " {" + + " \"path\": \"status\"," + + " \"kind\": \"string\"" + + " }" + + " ]" + + "}"; + when(this.elasticIndexNameResolver.getIndexNameFromKind(kind)) + .thenReturn(kind.replace(":", "-")); + when(this.schemaCache.get(kind)).thenReturn(null); + when(this.indicesService.isIndexExist(any(), any())).thenReturn(true); + when(this.indicesService.deleteIndex(any(), any())).thenReturn(true); + when(this.schemaProvider.getSchema(kind)).thenReturn(storageSchema); + + this.sut.syncIndexMappingWithStorageSchema(kind); + + verify(this.mappingService, times(1)).getIndexMappingFromRecordSchema(any()); + verify(this.indicesService, times(1)).isIndexExist(any(), any()); + verify(this.indicesService, times(1)).deleteIndex(any(), any()); + verify(this.indicesService, times(1)).createIndex(any(), any(), any(), any(), any()); + verifyNoMoreInteractions(this.mappingService); + } + + @Test + public void should_throw_exception_while_snapshot_running_sync_schema_with_storage() + throws Exception { + String kind = "tenant1:avocet:completion:1.0.0"; + when(this.elasticIndexNameResolver.getIndexNameFromKind(kind)) + .thenReturn(kind.replace(":", "-")); + when(this.schemaCache.get(kind)).thenReturn(null); + when(this.indicesService.isIndexExist(any(), any())).thenReturn(true); + when(this.indicesService.deleteIndex(any(), any())) + .thenThrow(new AppException(HttpStatus.SC_CONFLICT, "Index deletion error", "blah")); + + try { + this.sut.syncIndexMappingWithStorageSchema(kind); + } catch (AppException e) { + assertEquals(HttpStatus.SC_CONFLICT, e.getError().getCode()); + assertEquals("blah", e.getError().getMessage()); + assertEquals("Index deletion error", e.getError().getReason()); + } catch (Exception e) { + fail("Should not throw this exception " + e.getMessage()); + } + + verify(this.indicesService, times(1)).isIndexExist(any(), any()); + verify(this.indicesService, times(1)).deleteIndex(any(), any()); + verify(this.mappingService, never()).getIndexMappingFromRecordSchema(any()); + verify(this.indicesService, never()).createIndex(any(), any(), any(), any(), any()); + } + + @Test + public void should_return_true_while_if_forceClean_requested() throws IOException { + String kind = "tenant1:avocet:completion:1.0.0"; + when(this.elasticIndexNameResolver.getIndexNameFromKind(kind)) + .thenReturn(kind.replace(":", "-")); + when(this.indicesService.isIndexExist(any(), any())).thenReturn(true); + + assertTrue(this.sut.isStorageSchemaSyncRequired(kind, true)); + } + + @Test + public void should_return_true_while_if_forceClean_notRequested_and_indexNotFound() + throws IOException { + String kind = "tenant1:avocet:completion:1.0.0"; + when(this.elasticIndexNameResolver.getIndexNameFromKind(kind)) + .thenReturn(kind.replace(":", "-")); + when(this.indicesService.isIndexExist(any(), any())).thenReturn(false); + + assertTrue(this.sut.isStorageSchemaSyncRequired(kind, false)); + } + + @Test + public void should_return_false_while_if_forceClean_notRequested_and_indexExist() + throws IOException { + String kind = "tenant1:avocet:completion:1.0.0"; + when(this.elasticIndexNameResolver.getIndexNameFromKind(kind)) + .thenReturn(kind.replace(":", "-")); + when(this.indicesService.isIndexExist(any(), any())).thenReturn(true); + + assertFalse(this.sut.isStorageSchemaSyncRequired(kind, false)); + } +} diff --git a/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/ReindexServiceTest.java b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/ReindexServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..bf7fa01a337c13971b47efc97c5b20f96788cb9d --- /dev/null +++ b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/ReindexServiceTest.java @@ -0,0 +1,161 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.service; + +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.MockitoAnnotations.initMocks; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.when; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.opengroup.osdu.core.common.logging.JaxRsDpsLog; +import org.opengroup.osdu.core.common.model.http.DpsHeaders; +import org.opengroup.osdu.core.common.model.indexer.RecordQueryResponse; +import org.opengroup.osdu.core.common.model.indexer.RecordReindexRequest; +import org.opengroup.osdu.core.common.provider.interfaces.IRequestInfo; +import org.opengroup.osdu.indexer.config.IndexerConfigurationProperties; +import org.opengroup.osdu.indexer.util.IndexerQueueTaskBuilder; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.modules.junit4.PowerMockRunnerDelegate; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(PowerMockRunner.class) +@PowerMockRunnerDelegate(SpringRunner.class) +public class ReindexServiceTest { + + private final String cursor = "100"; + + private final String correlationId = UUID.randomUUID().toString(); + + @Mock + private IndexerConfigurationProperties configurationProperties; + @Mock + private StorageService storageService; + @Mock + private IRequestInfo requestInfo; + @Mock + private IndexerQueueTaskBuilder indexerQueueTaskBuilder; + @Mock + private JaxRsDpsLog log; + @InjectMocks + private ReindexServiceImpl sut; + + private RecordReindexRequest recordReindexRequest; + private RecordQueryResponse recordQueryResponse; + + private Map<String, String> httpHeaders; + + @Before + public void setup() { + initMocks(this); + + mockStatic(UUID.class); + + recordReindexRequest = RecordReindexRequest.builder().kind("tenant:test:test:1.0.0") + .cursor(cursor).build(); + recordQueryResponse = new RecordQueryResponse(); + + httpHeaders = new HashMap<>(); + httpHeaders.put(DpsHeaders.AUTHORIZATION, "testAuth"); + httpHeaders.put(DpsHeaders.CORRELATION_ID, correlationId); + DpsHeaders standardHeaders = DpsHeaders.createFromMap(httpHeaders); + when(requestInfo.getHeaders()).thenReturn(standardHeaders); + when(requestInfo.getHeadersMapWithDwdAuthZ()).thenReturn(httpHeaders); + when(requestInfo.getHeadersWithDwdAuthZ()).thenReturn(standardHeaders); + } + + @Test + public void should_returnNull_givenNullResponseResult_reIndexRecordsTest() { + try { + recordQueryResponse.setResults(null); + when(storageService.getRecordsByKind(any())).thenReturn(recordQueryResponse); + + String response = sut.reindexRecords(recordReindexRequest, false); + + Assert.assertNull(response); + } catch (Exception e) { + fail("Should not throw this exception" + e.getMessage()); + } + } + + @Test + public void should_returnNull_givenEmptyResponseResult_reIndexRecordsTest() { + try { + recordQueryResponse.setResults(new ArrayList<>()); + when(storageService.getRecordsByKind(any())).thenReturn(recordQueryResponse); + + String response = sut.reindexRecords(recordReindexRequest, false); + + Assert.assertNull(response); + } catch (Exception e) { + fail("Should not throw this exception" + e.getMessage()); + } + } + + @Ignore + @Test + public void should_returnRecordQueryRequestPayload_givenValidResponseResult_reIndexRecordsTest() { + try { + recordQueryResponse.setCursor(cursor); + List<String> results = new ArrayList<>(); + results.add("test1"); + recordQueryResponse.setResults(results); + + when(configurationProperties.getStorageRecordsBatchSize()).thenReturn(1); + + when(storageService.getRecordsByKind(any())).thenReturn(recordQueryResponse); + + String taskQueuePayload = sut.reindexRecords(recordReindexRequest, false); + + Assert.assertEquals("{\"kind\":\"tenant:test:test:1.0.0\",\"cursor\":\"100\"}", + taskQueuePayload); + } catch (Exception e) { + fail("Should not throw exception" + e.getMessage()); + } + } + + @Test + public void should_returnRecordChangedMessage_givenValidResponseResult_reIndexRecordsTest() { + try { + List<String> results = new ArrayList<>(); + results.add("test1"); + recordQueryResponse.setResults(results); + when(storageService.getRecordsByKind(any())).thenReturn(recordQueryResponse); + + String taskQueuePayload = sut.reindexRecords(recordReindexRequest, false); + + Assert.assertEquals(String.format( + "{\"data\":\"[{\\\"id\\\":\\\"test1\\\",\\\"kind\\\":\\\"tenant:test:test:1.0.0\\\",\\\"op\\\":\\\"create\\\"}]\",\"attributes\":{\"correlation-id\":\"%s\"}}", + correlationId), taskQueuePayload); + } catch (Exception e) { + fail("Should not throw exception" + e.getMessage()); + } + } +} diff --git a/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/StorageServiceTest.java b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/StorageServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..4b46abf2e3380742203290ead746c39dff3a4049 --- /dev/null +++ b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/StorageServiceTest.java @@ -0,0 +1,272 @@ +/* + * Copyright 2021 Google LLC + * Copyright 2021 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.service; + +import static java.util.Collections.singletonList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.powermock.api.mockito.PowerMockito.when; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatchers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.opengroup.osdu.core.common.http.IUrlFetchService; +import org.opengroup.osdu.core.common.logging.JaxRsDpsLog; +import org.opengroup.osdu.core.common.model.http.AppException; +import org.opengroup.osdu.core.common.model.http.DpsHeaders; +import org.opengroup.osdu.core.common.model.http.HttpResponse; +import org.opengroup.osdu.core.common.model.indexer.IndexingStatus; +import org.opengroup.osdu.core.common.model.indexer.JobStatus; +import org.opengroup.osdu.core.common.model.indexer.RecordInfo; +import org.opengroup.osdu.core.common.model.indexer.RecordQueryResponse; +import org.opengroup.osdu.core.common.model.indexer.RecordReindexRequest; +import org.opengroup.osdu.core.common.model.indexer.Records; +import org.opengroup.osdu.core.common.provider.interfaces.IRequestInfo; +import org.opengroup.osdu.indexer.config.IndexerConfigurationProperties; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +public class StorageServiceTest { + + @Mock + private IUrlFetchService urlFetchService; + @Mock + private JobStatus jobStatus; + @Mock + private JaxRsDpsLog log; + @Mock + private IRequestInfo requestInfo; + @Mock + private IndexerConfigurationProperties configurationProperties; + @InjectMocks + private StorageServiceImpl sut; + + private List<String> ids; + private static final String RECORD_ID1 = "tenant1:doc:1dbf528e0e0549cab7a08f29fbfc8465"; + private static final String RECORDS_ID2 = "tenant1:doc:15e790a69beb4d789b1f979e2af2e813"; + + @Before + public void setup() { + + String recordChangedMessages = + "[{\"id\":\"tenant1:doc:1dbf528e0e0549cab7a08f29fbfc8465\",\"kind\":\"tenant1:testindexer1528919679710:well:1.0.0\",\"op\":\"purge\"}," + + + "{\"id\":\"tenant1:doc:15e790a69beb4d789b1f979e2af2e813\",\"kind\":\"tenant1:testindexer1528919679710:well:1.0.0\",\"op\":\"create\"}]"; + + when(this.requestInfo.getHeadersMap()).thenReturn(new HashMap<>()); + when(this.requestInfo.getHeaders()).thenReturn(new DpsHeaders()); + + Type listType = new TypeToken<List<RecordInfo>>() { + }.getType(); + + List<RecordInfo> msgs = (new Gson()).fromJson(recordChangedMessages, listType); + jobStatus.initialize(msgs); + ids = Arrays.asList(RECORD_ID1, RECORDS_ID2); + + when(configurationProperties.getStorageRecordsBatchSize()).thenReturn(20); + } + + @Test + public void should_return404_givenNullData_getValidStorageRecordsTest() + throws URISyntaxException { + + HttpResponse httpResponse = mock(HttpResponse.class); + Mockito.when(httpResponse.getBody()).thenReturn(null); + + when(this.urlFetchService.sendRequest(ArgumentMatchers.any())).thenReturn(httpResponse); + + should_return404_getValidStorageRecordsTest(); + } + + @Test + public void should_return404_givenEmptyData_getValidStorageRecordsTest() + throws URISyntaxException { + + String emptyDataFromStorage = "{\"records\":[],\"notFound\":[]}"; + + HttpResponse httpResponse = mock(HttpResponse.class); + Mockito.when(httpResponse.getBody()).thenReturn(emptyDataFromStorage); + + when(this.urlFetchService.sendRequest(ArgumentMatchers.any())).thenReturn(httpResponse); + + should_return404_getValidStorageRecordsTest(); + } + + @Test + public void should_returnOneValidRecords_givenValidData_getValidStorageRecordsTest() + throws URISyntaxException { + + String validDataFromStorage = "{\"records\":[{\"id\":\"testid\", \"version\":1, \"kind\":\"tenant:test:test:1.0.0\"}],\"notFound\":[\"invalid1\"], \"conversionStatuses\": []}"; + + HttpResponse httpResponse = mock(HttpResponse.class); + Mockito.when(httpResponse.getBody()).thenReturn(validDataFromStorage); + + when(this.urlFetchService.sendRequest(ArgumentMatchers.any())).thenReturn(httpResponse); + Records storageRecords = this.sut.getStorageRecords(ids); + + assertEquals(1, storageRecords.getRecords().size()); + } + + @Test + public void should_logMissingRecord_given_storageMissedRecords() throws URISyntaxException { + + String validDataFromStorage = "{\"records\":[{\"id\":\"tenant1:doc:1dbf528e0e0549cab7a08f29fbfc8465\", \"version\":1, \"kind\":\"tenant:test:test:1.0.0\"}],\"notFound\":[]}"; + + HttpResponse httpResponse = mock(HttpResponse.class); + Mockito.when(httpResponse.getBody()).thenReturn(validDataFromStorage); + + when(this.urlFetchService.sendRequest(any())).thenReturn(httpResponse); + Records storageRecords = this.sut.getStorageRecords(ids); + + assertEquals(1, storageRecords.getRecords().size()); + verify(this.jobStatus).addOrUpdateRecordStatus(singletonList(RECORDS_ID2), IndexingStatus.FAIL, + HttpStatus.NOT_FOUND.value(), + "Partial response received from Storage service - missing records", + "Partial response received from Storage service: tenant1:doc:15e790a69beb4d789b1f979e2af2e813"); + } + + @Test + public void should_returnValidJobStatus_givenFailedUnitsConversion_processRecordChangedMessageTest() + throws URISyntaxException { + String validDataFromStorage = "{\"records\":[{\"id\":\"tenant1:doc:15e790a69beb4d789b1f979e2af2e813\", \"version\":1, \"kind\":\"tenant:test:test:1.0.0\"}],\"notFound\":[],\"conversionStatuses\":[{\"id\":\"tenant1:doc:15e790a69beb4d789b1f979e2af2e813\",\"status\":\"ERROR\",\"errors\":[\"crs conversion failed\"]}]}"; + + HttpResponse httpResponse = mock(HttpResponse.class); + Mockito.when(httpResponse.getBody()).thenReturn(validDataFromStorage); + + when(this.urlFetchService.sendRequest(any())).thenReturn(httpResponse); + Records storageRecords = this.sut.getStorageRecords(singletonList(RECORDS_ID2)); + + assertEquals(1, storageRecords.getRecords().size()); + verify(this.jobStatus) + .addOrUpdateRecordStatus(RECORDS_ID2, IndexingStatus.WARN, HttpStatus.BAD_REQUEST.value(), + "crs conversion failed", String + .format("record-id: %s | %s", "tenant1:doc:15e790a69beb4d789b1f979e2af2e813", + "crs conversion failed")); + } + + @Test + public void should_returnValidResponse_givenValidRecordQueryRequest_getRecordListByKind() + throws Exception { + + RecordReindexRequest recordReindexRequest = RecordReindexRequest.builder() + .kind("tenant:test:test:1.0.0").cursor("100").build(); + + HttpResponse httpResponse = new HttpResponse(); + httpResponse.setBody(new Gson().toJson(recordReindexRequest, RecordReindexRequest.class)); + + when(this.urlFetchService.sendRequest(ArgumentMatchers.any())).thenReturn(httpResponse); + + RecordQueryResponse recordQueryResponse = this.sut.getRecordsByKind(recordReindexRequest); + + assertEquals("100", recordQueryResponse.getCursor()); + assertNull(recordQueryResponse.getResults()); + } + + @Test + public void should_returnValidResponse_givenValidKind_getSchemaByKind() throws Exception { + + String validSchemaFromStorage = "{" + + " \"kind\": \"tenant:test:test:1.0.0\"," + + " \"schema\": [" + + " {" + + " \"path\": \"msg\"," + + " \"kind\": \"string\"" + + " }," + + " {" + + " \"path\": \"references.entity\"," + + " \"kind\": \"string\"" + + " }" + + " ]," + + " \"ext\": null" + + "}"; + String kind = "tenant:test:test:1.0.0"; + + HttpResponse httpResponse = new HttpResponse(); + httpResponse.setResponseCode(HttpStatus.OK.value()); + httpResponse.setBody(validSchemaFromStorage); + + when(this.urlFetchService.sendRequest(ArgumentMatchers.any())).thenReturn(httpResponse); + + String recordSchemaResponse = this.sut.getStorageSchema(kind); + + assertNotNull(recordSchemaResponse); + } + + @Test + public void should_returnNullResponse_givenAbsentKind_getSchemaByKind() throws Exception { + + String kind = "tenant:test:test:1.0.0"; + + HttpResponse httpResponse = new HttpResponse(); + httpResponse.setResponseCode(HttpStatus.NOT_FOUND.value()); + + when(this.urlFetchService.sendRequest(ArgumentMatchers.any())).thenReturn(httpResponse); + + String recordSchemaResponse = this.sut.getStorageSchema(kind); + + assertNull(recordSchemaResponse); + } + + @Test + public void should_returnOneValidRecords_givenValidData_getValidStorageRecordsWithInvalidConversionTest() + throws URISyntaxException { + + String validDataFromStorage = "{\"records\":[{\"id\":\"testid\", \"version\":1, \"kind\":\"tenant:test:test:1.0.0\"}],\"notFound\":[\"invalid1\"],\"conversionStatuses\": [{\"id\":\"testid\",\"status\":\"ERROR\",\"errors\":[\"conversion error occurred\"] } ]}"; + + HttpResponse httpResponse = mock(HttpResponse.class); + Mockito.when(httpResponse.getBody()).thenReturn(validDataFromStorage); + + when(this.urlFetchService.sendRequest(ArgumentMatchers.any())).thenReturn(httpResponse); + Records storageRecords = this.sut.getStorageRecords(ids); + + assertEquals(1, storageRecords.getRecords().size()); + + assertEquals(1, storageRecords.getConversionStatuses().get(0).getErrors().size()); + + assertEquals("conversion error occurred", + storageRecords.getConversionStatuses().get(0).getErrors().get(0)); + } + + private void should_return404_getValidStorageRecordsTest() { + try { + this.sut.getStorageRecords(ids); + fail("Should throw exception"); + } catch (AppException e) { + assertEquals(HttpStatus.NOT_FOUND.value(), e.getError().getCode()); + } catch (Exception e) { + fail("Should not throw this exception" + e.getMessage()); + } + } +} diff --git a/provider/indexer-reference/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/provider/indexer-reference/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000000000000000000000000000000000..ca6ee9cea8ec189a088d50559325d4e84ff8ad09 --- /dev/null +++ b/provider/indexer-reference/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file